开发者工具中的 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 窗格。在 Styles 窗格中,您可以查看应用于特定元素的规则,还可以修改规则并实时查看页面上的更改。

在去年之前,对使用 CSSOM API 修改的 CSS 规则的支持非常有限:您只能查看已应用的规则,但无法修改它们。去年,我们的一个主要目标是允许使用“样式”窗格修改 CSS-in-JS 规则。有时,我们还会将 CSS-in-JS 样式称为“构建的”,以指明它们是使用 Web API 构建的。

我们来详细了解一下开发者工具中的样式编辑功能。

DevTools 中的样式修改机制

DevTools 中的样式修改机制

在 DevTools 中选择某个元素后,系统会显示样式窗格。样式窗格会发出一个名为 CSS.getMatchedStylesForNode 的 CDP 命令,以获取适用于该元素的 CSS 规则。CDP 是 Chrome DevTools Protocol 的缩写,这是一个 API,可让 DevTools 前端获取有关被检查网页的更多信息。

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

您可能会问,为什么需要再次解析 CSS?这里的问题是,出于性能方面的原因,浏览器本身并不关心 CSS 规则的来源位置,因此不会存储这些位置。但 DevTools 需要源代码位置才能支持 CSS 编辑。我们不希望普通 Chrome 用户付出性能代价,但希望 DevTools 用户能够访问源代码位置。这种重新解析方法可同时满足这两种用例,并且缺点极少。

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

现在,我们来看看编辑功能。还记得 CSS.getMatchedStylesForNode 会返回每个规则的源代码位置吗?这对于编辑至关重要。当您更改规则时,DevTools 会发出另一个 CDP 命令,该命令会实际更新网页。该命令包含要更新的规则 fragment 的原始位置,以及该 fragment 需要更新的新文本。

在后端,处理修改调用时,DevTools 会更新目标样式表。它还会更新其维护的样式表来源的副本,并更新更新后的规则的来源位置。作为对修改调用的响应,DevTools 前端会获取刚刚更新的文本片段的更新后位置。

这说明了为什么在 DevTools 中修改 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 实例,并提取 DevTools 所需的其他元数据:

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);
}

在此代码段中,我们看到 CSSOMStyleSheetText 在内部调用 CollectStyleSheetRules。如果样式表不是内嵌样式表或资源样式表,系统会调用 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 规则与文本版本不同步,则编辑将无法正常运行。为此,我们引入了所谓的探针,以便浏览器在发生样式表变异时通知 DevTools 的后端部分。然后,在下次调用 CSS.getMatchedStylesForNode 时,系统会同步经过更改的样式表。

完成所有这些工作后,CSS-in-JS 编辑功能已经可以正常使用,但我们希望改进界面,以指明是否构建了样式表。我们在 CDP 的 CSS.CSSStyleSheetHeader 中添加了一个名为 isConstructed 的新属性,前端会使用该属性来正确显示 CSS 规则的来源:

可构造的样式表

总结

我们在本博文中介绍了与 CSS-in-JS 相关的相关用例(DevTools 不支持),并介绍了支持这些用例的解决方案。此实现的有趣之处在于,我们能够通过使 CSSOM CSS 规则具有常规源文本来利用现有功能,从而避免在 DevTools 中完全重新构建样式编辑功能。

如需了解更多背景信息,请参阅我们的设计提案或 Chromium 跟踪 bug,其中列出了所有相关补丁。

下载预览渠道

不妨考虑将 Chrome Canary 版开发者版Beta 版用作默认开发浏览器。通过这些预览版渠道,您可以使用最新的 DevTools 功能、测试尖端的 Web 平台 API,并帮助您在用户发现问题之前发现网站上的问题!

与 Chrome DevTools 团队联系

您可以使用以下选项讨论与 DevTools 相关的新功能、更新或任何其他内容。