您是否注意到 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 属性值的处理方式
在开发者工具中,在 Styles 标签页中渲染和装饰属性声明的过程分为两个不同阶段:
- 结构分析。这一初始阶段会分析属性声明,以确定其基础组件及其关系。例如,在
border: 1px solid red
声明中,它会将1px
识别为长度,将solid
识别为字符串,并将red
识别为颜色。 - 渲染。渲染阶段基于结构分析,将这些组成部分转换为 HTML 表示。这样可通过互动元素和视觉提示来丰富显示的房源文字。例如,颜色值
red
使用可点击的颜色图标呈现,当用户点击该图标时,系统会显示一个颜色选择器,方便您进行修改。
正则表达式
之前,我们依靠正则表达式来分析属性值以进行结构分析。我们维护了一个正则表达式列表,以匹配我们考虑装饰的属性值位。例如,有些表达式与 CSS 颜色、长度、角度以及更为复杂的子表达式(如 var
函数调用等)相匹配。我们从左到右扫描文本以进行值分析,不断从列表中查找与下一段文本匹配的第一个表达式。
虽然大多数情况下这都行得通,但未能持续增长的案例数量却不减。这些年来,我们收到大量错误报告,但发现匹配时并不完全准确。在我们修复问题(一些修复简单,其他修复)时,我们不得不重新思考如何规避技术债务。我们来看看其中的一些问题!
正在匹配“color-mix()
”
我们用于 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,这意味着适用于与上下文无关的语言的解析器。实际上,开发者工具代码库中已经存在此类解析器系统:CodeMirror 的 Lezer,它是 CodeMirror 中语法突出显示(可在 Sources 面板中找到的编辑器)的基础。Lezer 的 CSS 解析器让我们能够为 CSS 规则生成(非抽象)语法树,并且可供我们使用。胜利。
但开箱即用,我们发现从基于正则表达式的匹配直接迁移到基于解析器的匹配是不可行的:两种方法的运作方向相反。使用正则表达式匹配值片段时,开发者工具会从左到右扫描输入,反复尝试从有序模式列表中查找最早的匹配项。使用语法树时,匹配将自下而上开始,例如,先分析调用的参数,然后再尝试匹配函数调用。您可以将其视为对算术表达式求值,在这种情况下,您首先考虑的是带圆括号的表达式,然后是乘法运算符,最后是加法运算符。在此框架中,基于正则表达式的匹配从左到右评估算术表达式。我们真的不想从头开始重写整个匹配系统:有 15 个不同的匹配器和渲染器对,有数千行代码,这使得我们不太可能在单个里程碑中推出该系统。
因此,我们想出了一个能够进行增量更改的解决方案,我们将在下文中详细介绍。简而言之,我们保留了两阶段方法,但在第一阶段,我们尝试自下而上匹配子表达式(从而破坏正则表达式流程),在第二阶段,我们自上而下进行渲染。在这两个阶段,我们都可以使用现有的基于正则表达式的匹配器和渲染(几乎没有变化),因此能够将它们逐个迁移。
第 1 阶段:自下而上的匹配
第一阶段或多或少完全按照封面上的内容进行。我们按从下到上的顺序遍历规则树,并尝试匹配我们访问的每个语法树节点上的子表达式。如要匹配特定的子表达式,匹配器可以像在现有系统中一样使用正则表达式。实际上,从版本 128 开始,在少数情况下(例如匹配长度时),我们仍会这样做。或者,匹配器可以分析位于当前节点的子树结构。这样一来,它便可以同时捕获语法错误和记录结构信息。
请参考上面的语法树示例:
对于此树,匹配器将按以下顺序应用:
hsl(
177deg
var(--saturation, 100%) 50%)
:首先,我们发现hsl
函数调用的第一个参数,即色调角度。我们将其与角度匹配器进行匹配,以便使用角度图标装饰角度值。hsl(177deg
var(--saturation, 100%)
50%)
:其次,我们了解了带有 var 匹配器的var
函数调用。对于此类调用,我们主要想做两件事: <ph type="x-smartling-placeholder">- </ph>
- 查找该变量的声明并计算其值,然后分别在变量名称中添加一个链接和一个弹出式窗口以连接到它们。
- 如果计算值为颜色,请使用颜色图标装饰该调用。 实际上还有第三点,但我们稍后会说到这一点。
hsl(177deg var(--saturation, 100%) 50%)
:最后,我们匹配hsl
函数的调用表达式,以便使用颜色图标来装饰该函数。
除了搜索要修饰的子表达式之外,实际上,在匹配过程中,我们还会运行另一个特征。请注意,在第 2 步中,我们提到要查找变量名称的计算值。事实上,我们会更进一步,将结果传播到树状结构。而且不仅要针对变量,对后备值也应如此!可以保证在访问 var
函数节点时,已事先访问了其子项,因此我们已经知道可能出现在后备值中的任何 var
函数的结果。因此,我们能够轻松、经济地将 var
函数替换为其结果,从而轻松回答“此 var
的结果会调用一种颜色吗?”之类的问题,就像我们在第 2 步中所做的那样。
第 2 阶段:自上而下呈现
在第二阶段,我们会反转方向。获取阶段 1 的匹配结果,我们通过按从上到下的顺序遍历树,将树呈现到 HTML 中。对于每个访问的节点,我们检查它是否匹配,如果匹配,则调用匹配器的相应渲染程序。我们通过为文本节点添加默认的匹配器和渲染程序,避免对仅包含文本的节点(例如 NumberLiteral
“50%”)进行特殊处理。渲染程序只是输出 HTML 节点,这些节点组合在一起后生成属性值的表示形式,包括其装饰。
对于示例树,属性值的呈现顺序如下:
- 访问
hsl
函数调用。匹配,因此调用颜色函数渲染程序。它会执行以下两项操作: <ph type="x-smartling-placeholder">- </ph>
- 使用即时替换机制为任何
var
参数计算实际颜色值,然后绘制颜色图标。 - 以递归方式渲染
CallExpression
的子项。该函数会自动呈现函数名称、括号和逗号(这些只是文本)。
- 使用即时替换机制为任何
- 访问
hsl
调用的第一个参数。因此,您可以调用角度渲染程序,它会绘制角度图标和角度的文本。 - 访问第二个参数,即
var
调用。匹配成功,因此请调用 var renderer,它会输出以下内容: <ph type="x-smartling-placeholder">- </ph>
- 开头的文本
var(
。 - 变量名称,并使用指向变量定义的链接或用灰色文本来表明未定义。此外,它还向变量添加了一个弹出式窗口,以显示有关其值的信息。
- 先添加英文逗号,然后以递归方式呈现后备值。
- 右括号。
- 开头的文本
- 访问
hsl
调用的最后一个参数。不匹配,因此只输出其文本内容。
您是否注意到,在此算法中,渲染完全控制了匹配节点的子节点的渲染方式。以递归方式渲染子项是主动式的。这一技巧使我们能够逐步从基于正则表达式的呈现迁移到基于语法树的呈现。对于与旧版正则表达式匹配器匹配的节点,可以以原始形式使用相应的渲染器。用语法树的术语来说,它负责渲染整个子树,并且其结果(HTML 节点)可干净地插入到周围的渲染进程中。这让我们可以选择成对移植匹配器和渲染器,并逐一更换它们。
渲染程序控制其匹配节点的子项渲染的另一个很酷的功能是,它使我们能够推断要添加的图标之间的依赖关系。在上面的示例中,hsl
函数生成的颜色显然取决于其色相值。这意味着颜色图标显示的颜色取决于角度图标显示的角度。如果用户通过该图标打开角度编辑器并修改角度,我们现在能够实时更新颜色图标的颜色:
如上例所示,我们还将此机制用于其他图标配对,例如 color-mix()
及其两个颜色通道,或 var
函数(从其回退返回颜色)。
性能影响
在深入研究此问题以提高可靠性并解决长期存在的问题时,考虑到我们已经开始运行成熟的解析器,我们预计性能会有所下降。为了对此进行测试,我们创建了一个基准测试,该基准可呈现大约 3, 500 个属性声明,并在 M1 计算机上使用 6 倍节流对基于正则表达式的版本和基于解析器的版本进行了分析。
正如我们所预料的那样,对于这种情况,基于解析的方法比基于正则表达式的方法慢 27%。基于正则表达式的方法进行渲染需要 11 秒,基于解析器的方法需要 15 秒进行渲染。
考虑到这种新方法带来的成效,我们决定继续前行。
致谢
我们衷心感谢 Sofia Emelianova 和 Jecelyn Yeen 为编辑此博文提供了宝贵帮助!
下载预览渠道
请考虑将 Chrome Canary、开发者版或 Beta 版用作您的默认开发浏览器。通过这些预览渠道,您可以访问最新的开发者工具功能,测试先进的网络平台 API,并在用户之前发现您网站上的问题!
与 Chrome 开发者工具团队联系
使用以下选项讨论博文中的新功能和变更,或与开发者工具相关的任何其他内容。
- 请通过 crbug.com 提交建议或反馈。
- 使用更多选项报告开发者工具问题 >帮助 >在开发者工具中报告开发者工具问题。
- 请发送电子邮件至 @ChromeDevTools。
- 请对我们的开发者工具新功能 YouTube 视频或开发者工具提示 YouTube 视频发表评论。