链接:https://www.bram.us/2025/05/04/css-parser-extensions-pitch
作者:Bram.us
原标题:Polyfilling CSS with CSS Parser Extensions
四月,我参加了 BlinkOn,这是为 Chromium 开源项目中 Web 平台贡献者举办的会议。在会上,我做了关于“CSS Parser Extensions”的演讲,这是一个我提出的彻底解决 CSS polyfilling 问题的疯狂想法。
如果你不知道,polyfill CSS 特性极其困难,主要是因为 CSS 解析器会丢弃它不理解的内容。那么,如果作者们不必编写自己的解析器和级联来 polyfill CSS 特性,而是可以教会解析器一些新技巧呢?
⚠️ 注意!⚠️ :这是一个个人想法。目前还没有任何官方内容……暂时如此。
我演讲的目标(幻灯片、录音)是吸引在场的一些工程师,并获取他们对这个我过去两年一直在酝酿的疯狂想法的意见。接下来的步骤将是编写一个合适的解释器,然后将其提交给 CSS 工作组以寻求更广泛的兴趣。完成这项工作可能需要数年时间,如果它最终能完成的话。
在采用新的 CSS 特性时,Web 开发者经常告诉我,他们暂时不会使用某个特性,因为该特性尚未获得跨浏览器支持。在他们的组织内部,通常仍然存在一种期望,即网站在每个浏览器中的外观必须完全一致。或者,他们只是希望只编写一次代码——他们知道这些代码在各种浏览器(包括这些浏览器的一些旧版本)中都能正常运行。
因此,新 CSS 功能的采用——包括那些非常适合渐进增强的功能——被阻止,直到该功能在 Baseline 中广泛可用。假设平均互操作性实现时间为±1.5 年,这意味着一个 CSS 功能在首次在浏览器中发布后,需要 4 年时间才能获得更广泛的采用。
*(当然也有一些例外,还有许多其他因素会影响一个功能的(非)采用,但通常情况下就是这样。)*
典型功能发布的时间线。从功能在第一个浏览器中发布到功能成为 Baseline Widely Available,至少需要 4 年时间。
为了加速新 CSS 特性的采用,可以创建 polyfill。例如,容器查询的 polyfill 已经证明了其价值。然而,这个 polyfill——如同其他任何 CSS polyfill 一样——并非完美,存在一些限制。此外,该 polyfill 约±65%的代码专门用于解析 CSS 并从中提取必要信息,如属性值和容器 at 规则——这显得有些荒谬。
CSS Parser Extensions 旨在通过允许作者扩展 CSS 解析器以支持新语法、属性、关键字等,来消除这些限制并简化信息收集。通过直接接入 CSS 解析器,CSS polyfill 的编写变得更加容易,减少了大小和性能开销,并变得更加健壮。
*Philip Walton 在演讲《The Dark Side of Polyfilling CSS》中清晰地阐述了这个问题。建议观看此演讲以深入理解该问题。以下是问题陈述的简略版本。*
当开发者为一个 CSS 特性创建 polyfill 时,他们无法依赖 CSS 解析器提供关于他们想要 polyfill 的声明等信息。这是因为 CSS 解析器会丢弃它无法成功解析的规则和声明。因此,polyfill 需要自行收集并重新处理样式表,以获取他们想要 polyfill 的特性的标记。
虽然这看起来像执行这三个步骤一样简单,但实际上比看起来要复杂得多。
每个步骤都有其自身的挑战和限制,详情如下,并且 Philip Walton 在 2016 年(!)的这句话很好地总结了这一点:
如果你从未尝试过自己编写 CSS polyfill,那么你可能从未体验过这种痛苦。
– Philip Walton, 《CSS Polyfill 的阴暗面》, 2016 年 12 月
收集所有样式本身已经具有挑战性,因为作者需要从各种来源中搜集这些样式:
document.styleSheets
document.adoptedStyleSheets
在收集了所有这些样式表的引用后,工作就完成了,因为作者还需要留意这些来源中的任何变化。
有了所有样式表后,作者可以继续解析样式表的内容。这听起来很简单,但已经带来了挑战,因为在许多情况下,他们无法访问来自 CORS 保护源的样式表内容。
如果他们确实能够访问样式表的内容,作者需要手动对内容进行标记化和解析,重复了用户代理已经完成的工作。
他们对源代码释放的自定义 CSS 解析器也必须与整个 CSS 语法兼容。例如,当用户代理(UA)推出像 CSS 嵌套这样的功能时,polyfill 的 CSS 解析器也需要支持它。因此,用于 CSS polyfill 的 CSS 解析器需要不断追赶以支持最新的语法。
在解析样式后,作者必须确定需要将样式应用到哪些元素上。例如,对于声明,这基本上意味着他们需要编写自己的级联规则。他们还需要实现 CSS 功能,如媒体查询,并将其考虑在内。哦,还有 Shadow DOM,这使事情变得更加复杂。
如果,与其让 polyfill 作者编写自己的 CSS 解析器和级联,他们可以教解析器一些新技巧呢?
例如:通过 CSS.parser
让作者使用 JavaScript 访问 CSS 解析器——这样他们就可以扩展它,支持新的语法、属性、关键字和函数。
* CSS 关键词: CSS.parser.registerKeyword(…)
* CSS 函数: CSS.parser.registerFunction(…)
* CSS 语法: CSS.parser.registerSyntax(…)
* CSS 声明: CSS.parser.registerProperty(…)
在 CSS 解析器中注册这些功能之一后,解析器将不再丢弃与其相关的令牌,作者可以像解析器从未丢弃它们一样使用该功能。
例如,当注册一个不受支持的 CSS 属性 + 语法时,解析器将保留声明,并且该属性会出现在诸如 window.getComputedStyle()
的内容中。使用 CSS.supports()
/ @supports()
的功能检查也会通过。
除了这些注册之外,一些实用函数也应提供给作者。例如,获取元素指定样式的方法、将长度计算为其表示的像素值的方法、确定哪些注册已经完成的方法等。
*⚠️ 这些示例应该能让你了解 CSS Parser Extensions 可以实现的功能。这里的语法并非一成不变,它是我在探索可能性时想出来的。*
random
在以下示例中,不存在的 random
关键字被注册。每当 CSS 引擎解析该关键字时,它将返回一个随机值。
CSS.parser
.registerKeyword('random:<number>', {
caching\_mode: CSS.parser.caching\_modes.PER\_MATCH,
invalidation: CSS.parser.invalidation.NONE,
})
.computeTo((match) => {
return Math.random();
});
;
替换仅在样式表中每次出现时发生一次,这由 caching\_mode
和 invalidation
选项控制。
light-dark()
以下代码片段为强大的 light-dark()
提供了兼容性补丁。它是一个根据元素使用的 color-scheme
返回两个传入颜色之一的函数。当 color-scheme
为 light
时,使用第一个值;否则返回第二个值。
CSS.parser
.registerFunction(
'light-dark(light:<color>, dark:<color>):<color>',
{ invalidation: ['color-scheme'] }
)
.computeTo((match, args) => {
const { element, property, propertyValue } = match;
const colorScheme =
CSS.parser.getSpecifiedStyle(element)
.getPropertyValue('color-scheme');
if (colorScheme == 'light') return args.light;
return args.dark;
})
);
因为返回值依赖于 color-scheme
值,所以 color-scheme
属性被列为会导致失效的属性。
at-rule()
以下代码片段为神奇的 at-rule()
函数提供了补丁,该函数允许您检测@规则。它根据检查返回一个 <boolean>
。
CSS.parser
.registerFunction('at-rule(keyword:<string>):<boolean>', {
caching\_mode: CSS.parser.computation\_modes.GLOBAL,
})
.computeTo((match, args) => {
switch (args.keyword) {
case '@view-transition':
return ("CSSViewTransitionRule" in window);
case '@starting-style':
return ("CSSStartingStyleRule" in window);
// …
default:
return false;
}
})
;
由于检测只需执行一次,因此检查结果可以全局缓存。
此处排除了自定义函数。也许应该添加这些函数,也许不应该。
size
CSS size
属性是一个全新的属性,最近才被确定。它仍需进行规范制定和实现,并将作为一次性设置 width
和 height
的简写方式。
该属性会使用标准特性进行注册。除了用于确定其计算值的 computeTo
方法外, onMatch
方法还会返回一个声明块,用于在检测到使用该属性的声明时进行替换。
CSS.parser
.registerProperty('size', {
syntax: '[<length-percentage [0,∞]> | auto]{1,2}',
initialValue: 'auto',
inherits: false,
percentages: 'inline-size'
animatable: CSS.parser.animation\_types.BY\_COMPUTED\_VALUE,
})
.computeTo(…)
.onMatch((match, computedValue) => {
const { element, specifiedValue } = match;
return {
'width': computedValue[0],
'height': computedValue[1] ?? computedValue[0],
};
});
;
scroll-timeline
这是注册属性的另一个示例,即 scroll-timeline
属性。注册和匹配可以分开进行,它还表明可以在匹配时存储一些数据以供后续使用。这里是一个 ResizeObserver
,它被添加到匹配的元素中,并在稍后移除。
CSS.parser.registerProperty('scroll-timeline', { … });
CSS.parser
.matchProperty('scroll-timeline')
// No .computeTo … so it would just return the declared value
.onMatch(parserMatch => {
const resizeObserver = new ResizeObserver((entries) => {
// …
});
resizeObserver.observe(parserMatch.element);
parserMatch.data.set('ro', resizeObserver);
})
.onElementUnmatch(parserMatch => {
const resizeObserver = parserMatch.data.get('ro');
resizeObserver.disconnect();
})
;
也可以注册一个语法以供以后使用。
CSS.parser
.registerSyntax(
'<single-animation-timeline>',
'auto | none | <dashed-ident> | <scroll()> | <view()>'
)
;
CSS.parser
.registerProperty('animation-timeline', {
syntax: '<single-animation-timeline>#',
initialValue: 'auto',
inherits: false,
animatable: CSS.parser.ANIMATABLE\_NO,
})
.onMatch(…);
position: fixed / visual
在 w3c/csswg-drafts#7475 中,我建议对 position: fixed
进行扩展,允许您指示元素应固定到哪个对象。
position: fixed / layout
= 当前行为,将与 position: fixed
相同)position: fixed / visual
= 固定到视觉视口,即使在放大时也是如此position: fixed / visual-unzoomed
= 相对于未缩放的视觉视口定位用于 polyfill 的代码可能如下所示:
// Register syntaxes used by the polyfill.
CSS.parser.registerSyntax('<position>', 'static | relative | absolute | sticky | fixed');
CSS.parser.registerSyntax('<position-arg>', 'layout | visual | visual-unzoomed');
// Extend the existing `position` property registration, only overriding certain parts.
// The non-overriden parts remain untouched
const positionWithArgRegistration = CSS.parser
.registerProperty('position', {
extends: 'position',
syntax: '<position> [/ arg:<position-arg>]?',
})
// No .computeTo … so the syntax will compute individually
;
const cssPositionFixed =
positionWithArgRegistration
.with('position', 'fixed') // Only `position: fixed`
.with('arg') // Any arg value
.onMatch((match) => {
const { element, specifiedValue } = match;
const { position, arg } = specifiedValue;
const styles = CSS.parser.getSpecifiedStyle(element);
const visualViewport = determineVisualViewport();
switch (arg) {
case 'layout':
return {
position: 'fixed',
};
case 'visual':
return {
position: 'fixed',
bottom: (() => {
if (styles.bottom.toString() != 'auto') {
return styles.bottom.add(CSS.px(visualViewport.height));
}
})(),
};
case 'visual-unzoomed':
return {
position: 'fixed',
// @TODO: change all other properties
};
}
})
;
window.visualViewport.addEventListener('resize', () => {
cssPositionFixed.triggerMatch();
});
通过允许 polyfill 作者扩展用户代理自带的 CSS 解析器,他们不再需要收集所有样式、自行解析样式表或确定何时将样式应用于元素。由此产生的 polyfill 将更易于编写、体积更小、性能更快,并且更加健壮和高效。
借助由 CSS 解析器扩展支持的强大 CSS polyfill,CSS 功能的采用不再受限于跨浏览器广泛支持的基线,从而提高了采用率。
此外,这也将使浏览器厂商更容易对功能进行原型设计,因为它需要的前期投入更少。
为了实现这一点,执行操作的时机至关重要。你不想在像素管道的样式-布局-绘制步骤之间运行阻塞的 JavaScript。这是需要仔细考虑的事情。也许这应该被建模为一个观察者?
目前未包含的是选择器的 polyfill。我还没有对此进行任何思考,因此一旦深入研究后可以添加。我最初的猜测是,像 :has-interest
这样的选择器可以很容易地进行 polyfill,但伪元素的 polyfill 会稍微困难一些,因为你还需要为这些元素修改 DOM。
此外,并非所有的 CSS 特性都可以进行 polyfill。比如视图过渡(View Transitions)这样的功能。
最终,这个想法的成败取决于所有浏览器厂商的支持。如果其中一家(主要)浏览器厂商不支持,那么这个项目将会失败。
自《可扩展 Web 宣言》 The Extensible Web Manifesto 发布已有 12 年,Philip Walton 分享 CSS polyfill 的难度也有 9 年了,然而自那时以来,情况似乎并没有太大改变。
为了尝试推动这一进展,我的下一步是撰写一份详细的解释文档,并将其提交给 CSS 工作组,以寻求更广泛的关注。我在谷歌的一些同事对此表示感兴趣,并提供了帮助,我也知道 Brian 对此同样感兴趣……所以也许会有更多人(来自其他浏览器厂商)也会感兴趣。
不过,这里要设定一下预期:不要指望这能很快实现。即使最终能够实现,这也需要数年时间,我希望它能成功。
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有