您是否注意到,Chrome 开发者工具的样式标签页中的 CSS 属性最近看起来更精致了?这些更新在 Chrome 121 到 128 之间推出,是我们解析和呈现 CSS 值的方式显著改进的结果。在本文中,我们将详细介绍这一转变的技术细节,即从正则表达式匹配系统转向更强大的解析器。
让我们将当前的开发者工具与之前的版本进行比较:

差距相当大,对吧?以下是主要增强功能的详细说明:
color-mix。一个实用的预览,直观地展示了color-mix函数中的两个颜色实参。pink。命名颜色pink的可点击颜色预览。点击该图标即可打开颜色选择器,轻松进行调整。var(--undefined, [fallback value])。改进了对未定义变量的处理,未定义的变量会灰显,而有效的后备值(在本例中为 HSL 颜色)会显示为可点击的颜色预览。hsl(…):hsl颜色函数的另一个可点击颜色预览,可让您快速访问颜色选择器。177deg:一个可点击的角度时钟,可让您以交互方式拖动和修改角度值。var(--saturation, …):指向自定义属性定义的可点击链接,方便您轻松跳转到相关声明。
差异非常明显。为此,我们必须让开发者工具比以前更好地理解 CSS 属性值。
这些预览功能不是已经可用了吗?
虽然这些预览图标可能看起来很熟悉,但它们并不总是能一致地显示,尤其是在复杂的 CSS 语法中,如上例所示。即使在它们确实有效的情况下,也往往需要付出巨大努力才能使其正常运行。
这是因为,自开发者工具问世以来,值分析系统一直在不断发展。不过,它无法跟上 CSS 最近推出的令人惊叹的新功能,以及随之而来的语言复杂性增加。该系统需要全面重新设计才能跟上发展步伐,而这正是我们所做的!
CSS 属性值的处理方式
在开发者工具中,样式标签页中属性声明的渲染和修饰过程分为两个不同的阶段:
- 结构分析。此初始阶段会剖析属性声明,以识别其底层组件及其关系。例如,在声明
border: 1px solid red中,它会将1px识别为长度,将solid识别为字符串,将red识别为颜色。 - 正在渲染。在结构分析的基础上,渲染阶段会将这些组件转换为 HTML 表示形式。这会通过互动元素和视觉提示丰富显示的房源文本。例如,颜色值
red会呈现为可点击的颜色图标,点击该图标会显示颜色选择器,以便轻松修改。
正则表达式
以前,我们依靠正则表达式 (regex) 来剖析属性值,以便进行结构分析。我们维护了一个正则表达式列表,用于匹配我们认为需要修饰的属性值。例如,有些表达式与 CSS 颜色、长度、角度、更复杂的子表达式(例如 var 函数调用)等相匹配。我们从左到右扫描文本以进行值分析,不断从列表中寻找与下一段文本匹配的第一个表达式。
虽然这种方法在大多数情况下都有效,但无效的情况越来越多。多年来,我们收到了大量 bug 报告,其中指出匹配结果不准确。在修复这些问题的过程中(有些修复很简单,有些则相当复杂),我们不得不重新思考我们的方法,以控制技术债务。我们来看看其中一些问题!
匹配 color-mix()
/color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g
这与其语法相符:
color-mix(<color-interpolation-method>, [<color> && <percentage [0,100]>?]#{2})
尝试运行以下示例,直观呈现匹配结果。
const re = /color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g;
// it works - simpler example
const simpler = re.exec('color-mix(in srgb, pink, hsl(127deg 100% 50%))');
console.table(simpler.groups);
re.exec('');
// it doesn't work - complex example
const complex = re.exec('color-mix(in srgb, pink, var(--undefined, hsl(127deg var(--saturation, 100%) 50%)))');
console.table(complex.groups);

简单的示例可以正常运行。不过,在更复杂的示例中,<firstColor> 匹配项为 hsl(177deg var(--saturation,<secondColor> 匹配项为 100%) 50%)),这完全没有意义。
我们知道这是一个问题。毕竟,CSS 作为一种形式语言并不常规,因此我们已经加入了特殊处理来应对更复杂的函数参数,例如 var 函数。不过,如第一张屏幕截图所示,这在某些情况下仍然无法正常运行。
匹配 tan()
其中一个报告的比较搞笑的 bug 与三角函数 tan() 有关。我们用于匹配颜色的正则表达式包含一个子表达式 \b[a-zA-Z]+\b(?!-),用于匹配具名颜色(例如 red 关键字)。然后,我们检查了匹配的部分是否确实是具名颜色,您猜怎么着,tan 也是具名颜色!因此,我们错误地将 tan() 表达式解读成了颜色。
匹配 var()
我们再来看一个示例,即 var() 函数,其后备值包含其他 var() 引用:var(--non-existent, var(--margin-vertical))。
我们的 var() 正则表达式会很乐意匹配此值。不过,它会在第一个右括号处停止匹配。因此,上述文本会匹配为 var(--non-existent, var(--margin-vertical)。这是正则表达式匹配的典型限制。需要匹配括号的语言从根本上来说不是正则语言。
改用 CSS 解析器
当使用正则表达式的文本分析停止工作时(因为分析的语言不规则),有一个规范的下一步:使用解析器来解析更高级别的语法。对于 CSS,这意味着使用解析器来解析上下文无关语言。事实上,DevTools 代码库中已经存在这样的解析器系统:CodeMirror 的 Lezer,它是 CodeMirror 中语法突出显示等功能的基础,CodeMirror 是您在源代码面板中找到的编辑器。Lezer 的 CSS 解析器可让我们为 CSS 规则生成(非抽象)语法树,并且可供我们使用。胜利。

不过,我们发现,直接从基于正则表达式的匹配迁移到基于解析器的匹配是不可行的:这两种方法的工作方向相反。使用正则表达式匹配部分值时,开发者工具会从左到右扫描输入,反复尝试从有序模式列表中找到最早的匹配项。使用语法树时,匹配会从下往上开始,例如,先分析调用的参数,然后再尝试匹配函数调用。您可以将其视为对算术表达式求值,其中您首先考虑带括号的表达式,然后考虑乘法运算符,最后考虑加法运算符。在这种框架下,基于正则表达式的匹配对应于从左到右对算术表达式求值。我们真的不想从头开始重写整个匹配系统:有 15 个不同的匹配器和渲染器对,其中包含数千行代码,这使得我们不太可能在单个里程碑中交付它。
因此,我们想出了一个解决方案,让我们能够进行增量更改,我们将在下文中更详细地介绍该解决方案。简而言之,我们保留了两个阶段的方法,但在第一阶段,我们尝试自下而上匹配子表达式(从而打破正则表达式流程),在第二阶段,我们自上而下呈现。在这两个阶段,我们都可以使用现有的基于正则表达式的匹配器和渲染器,几乎无需更改,因此能够逐个迁移它们。
第 1 阶段:自下而上匹配
第一阶段或多或少完全按照其名称所描述的那样进行。我们按照从下到上的顺序遍历树,并尝试在访问的每个语法树节点处匹配子表达式。为了匹配特定的子表达式,匹配器可以像在现有系统中一样使用正则表达式。从版本 128 开始,我们实际上仍然会在少数情况下使用,例如匹配长度。或者,匹配器可以分析以当前节点为根的子树的结构。这样,它就可以同时捕获语法错误并记录结构信息。
请考虑上面的语法树示例:

对于此树,我们的匹配器将按以下顺序应用:
hsl(177degvar(--saturation, 100%) 50%):首先,我们找到hsl函数调用的第一个实参,即色相角。我们使用角度匹配器来匹配它,以便用角度图标来修饰角度值。hsl(177degvar(--saturation, 100%)50%):其次,我们使用 var 匹配器发现var函数调用。对于此类调用,我们主要想做两件事:- 查找变量的声明并计算其值,然后向变量名称添加链接和弹出式窗口,以便分别连接到它们。
- 如果计算出的值是颜色,则使用颜色图标修饰调用。 实际上还有第三个,但我们稍后再谈。
hsl(177deg var(--saturation, 100%) 50%):最后,我们匹配hsl函数的调用表达式,以便用颜色图标对其进行修饰。
除了搜索要修饰的子表达式之外,我们实际上还会在匹配过程中运行另一项功能。请注意,在第 2 步中,我们提到要查找变量名称的计算值。事实上,我们更进一步,将结果向上传播到树中。不仅要为变量设置,还要为后备值设置!可以保证,在访问 var 函数节点时,其子节点已事先访问过,因此我们已经知道可能出现在后备值中的任何 var 函数的结果。因此,我们能够轻松且低成本地用 var 函数的结果实时替换这些函数,从而轻松回答诸如“此 var 调用的结果是否为颜色?”之类的问题,就像我们在第 2 步中所做的那样。
第 2 阶段:自上而下渲染
在第二阶段,我们反向操作。我们从第 1 阶段获取匹配结果,然后按照从上到下的顺序遍历树,将其渲染为 HTML。对于每个访问过的节点,我们都会检查它是否匹配,如果匹配,则调用匹配器的相应渲染程序。我们通过为文本节点添加默认匹配器和渲染器,避免了对仅包含文本的节点(例如 NumberLiteral“50%”)进行特殊处理。渲染器只是输出 HTML 节点,这些节点组合在一起后,会生成属性值及其修饰的表示。

对于示例树,属性值的呈现顺序如下:
- 访问
hsl函数调用。匹配成功,因此调用颜色函数渲染程序。它会执行两项操作:- 使用动态替换机制计算任何
var实参的实际颜色值,然后绘制颜色图标。 - 以递归方式呈现
CallExpression的子级。这会自动处理函数名称、括号和逗号(这些只是文本)的呈现。
- 使用动态替换机制计算任何
- 访问
hsl调用的第一个参数。匹配成功,因此调用角度渲染器,该渲染器会绘制角度图标和角度文本。 - 访问第二个实参,即
var调用。匹配成功,因此调用 var renderer,输出如下:- 开头的文本
var(。 - 变量名称,并使用指向变量定义的链接或灰色文本颜色(表示未定义)对其进行修饰。它还会向变量添加一个弹出式窗口,以显示其值的相关信息。
- 逗号,然后以递归方式呈现后备值。
- 右括号。
- 开头的文本
- 访问
hsl调用的最后一个参数。它不匹配,因此只需输出其文本内容。
您是否注意到,在此算法中,渲染器完全控制着匹配节点的子节点的渲染方式?以递归方式渲染子项是主动进行的。正是这一技巧,让我们得以从基于正则表达式的渲染逐步迁移到基于语法树的渲染。对于与旧版正则表达式匹配器匹配的节点,相应的渲染器可以按其原始形式使用。在语法树术语中,它将负责渲染整个子树,并且其结果(HTML 节点)可以干净地插入到周围的渲染过程中。这样,我们就可以成对移植匹配器和渲染器,并逐一替换它们。
渲染器控制其匹配节点的子节点的渲染,还有一个很酷的功能,就是让我们能够推断所添加图标之间的依赖关系。在上面的示例中,hsl 函数生成的颜色显然取决于其色相值。这意味着,颜色图标显示的颜色取决于角度图标显示的角度。如果用户通过该图标打开角度编辑器并修改角度,我们现在可以实时更新颜色图标的颜色:
如上例所示,我们也将此机制用于其他图标配对,例如 color-mix() 及其两个颜色通道,或从其后备返回颜色的 var 函数。
性能影响
在深入研究此问题以提高可靠性并解决长期存在的问题时,我们预计性能会有所下降,因为我们开始运行一个成熟的解析器。为了测试这一点,我们创建了一个基准,该基准会渲染大约 3, 500 个属性声明,并在 M1 机器上以 6 倍节流对基于正则表达式的版本和基于解析器的版本进行了性能分析。
正如我们所料,在这种情况下,基于解析的方法比基于正则表达式的方法慢 27%。基于正则表达式的方法耗时 11 秒完成渲染,而基于解析器的方法耗时 15 秒完成渲染。
考虑到新方法带来的优势,我们决定采用新方法。
致谢
我们衷心感谢 Sofia Emelianova 和 Jecelyn Yeen 在编辑本文时提供的宝贵帮助!
下载预览渠道
考虑将 Chrome Canary、开发者版或 Beta 版用作默认开发浏览器。通过这些预览渠道,您可以抢先体验最新的开发者工具功能、测试前沿的 Web 平台 API,并在用户发现问题之前及时发现网站上的问题!
与 Chrome 开发者工具团队联系
您可以通过以下方式讨论新功能、更新或与开发者工具有关的任何其他内容。
- 请通过 crbug.com 向我们提交反馈和功能请求。
- 在开发者工具中,依次点击 更多选项 > 帮助 > 报告开发者工具问题,即可报告开发者工具问题。
- 在 Twitter 上发帖并提及 @ChromeDevTools。
- 在“开发者工具的新变化”YouTube 视频或“开发者工具提示”YouTube 视频中发表评论。