开发者工具中的 CSS-in-JS 支持

亚历克斯·鲁登科 (Alex Rudenko)
Alex Rudenko

本文介绍了自 Chrome 85 开始推出的开发者工具中的 CSS-in-JS 支持,总体而言,什么是 CSS-in-JS,以及它与开发者工具长期支持的常规 CSS 有何不同。

什么是 CSS-in-JS?

CSS-in-JS 的定义相当模糊。从广义上讲,这是一种使用 JavaScript 管理 CSS 代码的方法。例如,这可能意味着 CSS 内容使用 JavaScript 定义,而最终的 CSS 输出由应用即时生成。

在开发者工具中,CSS-in-JS 是指使用 CSSOM API 将 CSS 内容注入页面。常规 CSS 使用 <style><link> 元素注入,并且具有静态来源(例如 DOM 节点或网络资源)。相比之下,CSS-in-JS 通常没有静态来源。这种情况的一种特殊情况是,可以使用 CSSOM API 更新 <style> 元素的内容,这会导致来源与实际的 CSS 样式表不同步。

如果您使用任何 CSS-in-JS 库(例如 styled-componentEmotionJSS),该库可能会在后台使用 CSSOM API 注入样式,具体取决于开发模式和浏览器。

我们来看一些示例,了解如何使用 CSSOM API 注入样式表(类似于 CSS-in-JS 库的作用)。

// Insert new rule to an existing CSS stylesheet
const element = document.querySelector('style');
const stylesheet = element.sheet;
stylesheet.replaceSync('.some { color: blue; }');
stylesheet.insertRule('.some { color: green; }');

您也可以创建一个全新的样式表

// Create a completely new stylesheet
const stylesheet = new CSSStyleSheet();
stylesheet.replaceSync('.some { color: blue; }');
stylesheet.insertRule('.some { color: green; }');

// Apply constructed stylesheet to the document
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];

开发者工具中的 CSS 支持

在开发者工具中,处理 CSS 时最常用的功能是 Styles 窗格。在样式窗格中,您可以查看哪些规则应用于特定元素,还可以修改这些规则,并实时查看页面上的更改。

去年之前,对使用 CSSOM API 修改的 CSS 规则的支持非常有限:您只能看到已应用的规则,但无法对其进行修改。我们去年的主要目标是允许使用“Styles”窗格修改 CSS-in-JS 规则。有时,我们也将 CSS-in-JS 样式称为“constructed”,以表示这些样式是使用 Web API 构建的。

我们来深入了解一下开发者工具中样式修改工作的细节。

DevTools 中的样式修改机制

DevTools 中的样式修改机制

当您在开发者工具中选择元素时,系统会显示 Styles 窗格。Styles 窗格会发出一个名为 CSS.getMatchedStylesForNode 的 CDP 命令,以获取应用到该元素的 CSS 规则。CDP 代表 Chrome 开发者工具协议,是一个可让开发者工具前端获取有关所检查页面的更多信息的 API。

调用后,CSS.getMatchedStylesForNode 会识别文档中的所有样式表,并使用浏览器的 CSS 解析器解析这些样式表。然后,它会构建一个索引,将每条 CSS 规则与样式表来源中的某个位置相关联。

您可能会问,为什么需要再次解析 CSS?这里的问题是,出于性能原因,浏览器本身并不关注 CSS 规则的来源位置,因此不会存储这些规则。但是,开发者工具需要源位置以支持 CSS 修改。我们不希望普通的 Chrome 用户因性能而付出代价,但我们希望开发者工具用户能够访问源位置。这种重新解析方法可以同时满足这两种使用情形,但弊端微乎其微。

接下来,CSS.getMatchedStylesForNode 实现会要求浏览器的样式引擎提供与给定元素匹配的 CSS 规则。最后,该方法会将样式引擎返回的规则与源代码相关联,并提供关于 CSS 规则的结构化响应,以便开发者工具知道规则的哪一部分是选择器或属性。它允许开发者工具独立修改选择器和属性。

现在来看看如何进行修改。还记得 CSS.getMatchedStylesForNode 会返回每条规则的源位置吗?这对于编辑至关重要。当您更改规则时,开发者工具会发出另一个实际更新页面的 CDP 命令。该命令包含要更新的规则的片段的原始位置,以及需要更新片段的新文本。

在后端,处理 edit 调用时,开发者工具会更新目标样式表。它还会更新其维护的样式表来源的副本,以及更新后规则的来源位置。为响应修改调用,开发者工具前端会获取刚刚更新的文本 fragment 的更新位置。

这解释了为什么无法在开发者工具中修改 CSS-in-JS:CSS-in-JS 未在任何位置存储实际的源,以及 CSS 规则存在于浏览器内存中的 CSSOM 数据结构中

我们如何添加对 CSS-in-JS 的支持

因此,为了支持修改 CSS-in-JS 规则,我们决定最好的解决方案是为构造的样式表创建一个来源,此来源可使用上述现有机制进行修改。

第一步是构建源文本。浏览器的样式引擎会将 CSS 规则存储在 CSSStyleSheet 类中。如前所述,该类就是您可以从 JavaScript 创建其实例的类。构建源文本的代码如下所示:

String InspectorStyleSheet::CollectStyleSheetRules() {
  StringBuilder builder;
  for (unsigned i = 0; i < page_style_sheet_->length(); i++) {
    builder.Append(page_style_sheet_->item(i)->cssText());
    builder.Append('\n');
  }
  return builder.ToString();
}

它会迭代在 CSSStyleSheet 实例中找到的规则,并从中构建单个字符串。创建 InspectorStyleSheet 类的实例时,系统会调用此方法。InspectorStyleSheet 类封装一个 CSSStyleSheet 实例,并提取开发者工具所需的其他元数据:

void InspectorStyleSheet::UpdateText() {
  String text;
  bool success = InspectorStyleSheetText(&text);
  if (!success)
    success = InlineStyleSheetText(&text);
  if (!success)
    success = ResourceStyleSheetText(&text);
  if (!success)
    success = CSSOMStyleSheetText(&text);
  if (success)
    InnerSetText(text, false);
}

在此代码段中,我们看到在内部调用 CollectStyleSheetRulesCSSOMStyleSheetText。如果样式表不是内嵌的,也不是资源样式表,系统会调用 CSSOMStyleSheetText。基本上,这两个代码段已经允许对使用 new CSSStyleSheet() 构造函数创建的样式表进行基本修改。

一种特殊情况是,与已使用 CSSOM API 更改的 <style> 标记相关联的样式表。在本例中,样式表包含源文本和源中不存在的其他规则。为处理这种情况,我们引入了一种方法来将这些额外的规则合并到源文本中。在这里,顺序很重要,因为 CSS 规则可以插入到原始源文本的中间。例如,假设原始 <style> 元素包含以下文本:

/* comment */
.rule1 {}
.rule3 {}

然后,该页面使用 JS API 插入一些新规则,生成规则顺序如下:.rule0、.rule1、.rule2、.rule3、.rule4。合并操作后生成的源文本应如下所示:

.rule0 {}
/* comment */
.rule1 {}
.rule2 {}
.rule3 {}
.rule4 {}

保留原始注释和缩进在编辑过程中非常重要,因为规则的源文本位置必须精确。

CSS-in-JS 样式表的另一个特别之处在于,网页可以随时更改这些样式表。如果实际的 CSSOM 规则与文字版本不同步,修改就会无法正常运行。为此,我们引入了所谓的“探测”,让浏览器能够在样式表发生变化时通知开发者工具的后端部分。随后,系统会在下一次调用 CSS.getMatchedStylesForNode 时同步变更后的样式表。

完成上述所有部分后,CSS-in-JS 编辑已经可以正常运行,但我们想要改进界面来指示是否已构建样式表。我们在 CDP 的 CSS.CSSStyleSheetHeader 中添加了一个名为 isConstructed 的新属性,前端可以使用该属性正确显示 CSS 规则的来源:

可构造的样式表

总结

回顾一下我们的案例,我们探讨了开发者工具不支持的 CSS-in-JS 相关用例,并介绍了支持这些用例的解决方案。此实现的有趣之处在于,我们能够通过使 CSSOM CSS 规则具有常规源文本来利用现有功能,而无需在开发者工具中完全重新设计样式修改。

如需了解更多背景信息,请查看我们的设计方案或 Chromium 跟踪 bug,该 bug 引用了所有相关补丁。

下载预览渠道

不妨考虑将 Chrome Canary 版开发者版Beta 版用作默认开发浏览器。通过这些预览渠道,您可以使用最新的开发者工具功能,测试先进的网络平台 API,并先于用户发现您网站上的问题!

与 Chrome 开发者工具团队联系

使用以下选项讨论博文中的新功能和变化,或讨论与开发者工具有关的任何其他内容。

  • 请通过 crbug.com 向我们提交建议或反馈。
  • 使用开发者工具中的更多选项   了解详情   > Help > Report a DevTools issues,报告开发者工具问题。
  • 您可以前往 @ChromeDevTools 发 Twitter 微博。
  • 请在 YouTube 视频或“开发者工具提示”YouTube 视频中留言说明“开发者工具的新变化”。