`:is()` 和 `:where()` 怎么用:合并重复选择器与主动降权的两种新思路

:is():where() 今年一月随 Chrome 88 落地之后,从"草案阶段观望"变成了"可以放心用的新特性"。这两个选择器解决的是两类不同的问题::is() 用来合并一批写法重复、结构类似的选择器,减少选择器序列本身的重复书写;:where() 做的事情表面上跟 :is() 一样,都能合并选择器,但它多做了一件事——把合并出来的整条规则权重直接清零,不管括号里塞了多少层选择器。这个差异决定了两者的适用场景完全不重叠,混着用容易踩坑。

先看 :is() 解决的重复书写问题

在这两个选择器出现之前,样式表里处理"一批选择器结构相似、只是末尾不同"的场景,除了老老实实用逗号分组,几乎没有别的原生手段——想精简都得靠预处理器的嵌套语法或者 @extend 去减少源码层面的重复,编译产物本质上还是逗号分组。:is():where() 是浏览器原生解析层面提供的合并能力,不依赖构建工具,这也是它们值得单独拿出来讲的原因。

样式表里经常能看到这类写法:

1.article h2,
2.article h3,
3.article h4 {
4  margin-top: 32px;
5  font-weight: 600;
6}
7
8.sidebar a:hover,
9.sidebar a:focus,
10.sidebar a:active {
11  color: #1677ff;
12}

这两组规则的共同点是前缀相同、只有末尾的选择器在变化,用逗号分组写没问题,但选择器一多,重复的前缀会占掉大半篇幅,改动前缀(比如把 .article 换成 .post-content)还得在每一行都改一次。:is() 可以把这类"前缀相同、后缀不同"的选择器合并成一条:

1.article :is(h2, h3, h4) {
2  margin-top: 32px;
3  font-weight: 600;
4}
5
6.sidebar a:is(:hover, :focus, :active) {
7  color: #1677ff;
8}

括号里可以放任意选择器列表,包括标签、类、伪类,甚至复合选择器。团队里最近整理表单校验样式的时候用得比较多,原来的写法是:

1.form-item.is-error .form-label,
2.form-item.is-warning .form-label {
3  color: #f5222d;
4}

改成 :is() 之后:

1.form-item:is(.is-error, .is-warning) .form-label {
2  color: #f5222d;
3}

这条改动本身不影响权重比较的结果——因为原来那两条规则权重完全一样,合并前后都是同一个层级。:is() 首先带来的收益是维护成本:以后要加一个新状态类(比如 .is-pending),只需要在括号里加一项,不用再复制一整行选择器。

:is() 的权重计算:按参数里最高的算

:is() 本身不计权重,但括号里的参数要按最具体的那个计算,这一点很容易被忽略,因为直觉上会觉得"反正只是合并写法,权重应该跟拆开写一样"。拆开写确实一样,但如果括号里混进了不同权重级别的选择器,情况就不一样了:

1:is(.article-content, #legacy-content) h2 {
2  color: #333;
3}

这条规则的权重不是按"平均"或者"最低"来算,而是按括号里权重最高的那一项算——这里是 #legacy-content,所以整条规则的权重等价于 #legacy-content h2,也就是一个 ID 加一个标签的权重。如果原本写这行代码的人只是想图省事,把新旧两套内容区域的类名合并写一下,权重却因为括号里混进一个 ID 被意外拔高了,可能会导致这条规则突然压过了本该赢的其他业务样式,而写的人完全没意识到自己动了权重。

排查这类问题时,:is() 参数列表要重点看有没有混入不同类型的选择器。团队里现在的约定是::is() 括号内尽量保持同一类型(要么全是标签,要么全是类,不要 ID 和类混用),这样权重结果是可预期的,不需要每次都去心算"这一堆里最高的是哪个"。

:where():权重直接归零

:where() 语法和 :is() 完全一样,唯一的区别是它不管括号里写了什么,最终整条规则的权重都按零计算:

1:where(.article-content, #legacy-content) h2 {
2  color: #333;
3}

这一条和上面 :is() 的例子看起来几乎一样,权重结果却完全相反——不管括号里放的是类还是 ID,:where() 包裹的部分权重恒为零,整条规则的权重只剩后面 h2 这一个标签,也就是 (0, 0, 1)。这意味着几乎任何一条带类名的业务样式,都能轻松盖过这条规则。

这个特性乍看有点反直觉——选择器写复杂了,权重反而没了。但正因为这样,:where() 才特别适合用在"我想合并一批选择器省事,但又不想让这条规则参与权重竞争"的场景。团队里正好有一处能直接受益:组件库默认样式和业务覆盖之间长期存在的优先级拉锯。

:where() 给组件库默认样式主动降权

之前维护公共样式库的时候,遇到的常见问题是:写一条基础样式给按钮定个默认内边距和文字色,业务方想要覆盖,要么去数权重再决定要不要多套一层类名,要么就直接甩一个 !important 上去图省事。归根结底是因为普通类选择器的权重是固定的 (0, 1, 0),业务方要覆盖,至少也得写出同等或更高的权重才行,谁都不想每次改个按钮颜色还要回去看基础样式那边选择器写了几层。

:where() 包一层之后,这个问题从"业务方想办法覆盖"变成"基础样式主动放弃权重":

1:where(.btn) {
2  padding: 8px 16px;
3  border-radius: 4px;
4  color: #333;
5  background: #f5f5f5;
6}
7
8:where(.btn-primary) {
9  background: #1677ff;
10  color: #fff;
11}

这两条规则权重都是零。业务代码里写一个最普通的 .btn { color: red; }(权重 (0, 1, 0))就能直接覆盖,不需要多堆一层父级类名,也不需要 !important。这跟以前"谁的选择器权重更高谁赢"的思路反过来了——组件库这一方主动把权重让出去,业务方永远处于权重占优的一侧,不用操心组件库内部选择器到底写了几层。

这个思路对团队现有的 Element UI 样式覆盖场景也有启发,虽然 Element UI 自己短期内不会改造成 :where() 包裹(那是组件库作者才能决定的事),但公司内部维护的基础样式库、公共组件库完全可以照着这个模式重写。之前排查过一次典型场景:业务组件想统一改全站按钮的圆角,原来的写法是:

1.btn {
2  border-radius: 4px;
3}

业务页面想在某个场景下改成直角,得写:

1.dialog-footer .btn {
2  border-radius: 0;
3}

多套一层父级类名才能压过去。如果基础样式一开始就用 :where() 包起来:

1:where(.btn) {
2  border-radius: 4px;
3}

业务页面直接写 .btn { border-radius: 0; } 就够了,不需要额外套父级选择器,样式覆盖不再依赖"业务方要不要多写一层嵌套"这种脆弱的手段。

权重清零不等于规则消失,要留意层叠顺序

:where() 权重归零之后有一个容易被误解的地方——权重是零,不代表这条规则就一定输给同层级的其他规则。如果两条规则权重都是零(都被 :where() 包裹),或者一条是 :where() 包裹、权重为零的规则,恰好碰上另一条同样权重为零的标签选择器,这时候比的还是源码顺序,跟正常的层叠规则完全一致:

1:where(.btn) {
2  color: #333;
3}
4
5h2 {
6  color: #000;
7}

这两条规则如果同时命中一个 <h2 class="btn">,权重分别是 (0, 0, 0)(0, 0, 1)——注意标签选择器 h2 本身权重是 (0, 0, 1),比 :where() 清零之后的 (0, 0, 0) 还高,h2 那条反而会赢。这个例子说明一件事::where() 包裹的选择器权重比正常的标签选择器还要低,用它包装基础样式的时候要清楚,连一个孤零零的标签选择器都能覆盖过它,这正是它设计出来的目的——尽量把自己放在权重竞争的最底层。

还有一个细节,:where():is() 内部如果包含伪元素(比如 ::before),会被判定为无效选择器,整条规则失效,浏览器目前的实现里伪元素不允许出现在这两个函数的参数列表里。这个限制在写深色模式或者状态样式合并的时候容易碰到,比如想把下面两条合并:

1.card::before { content: ""; }
2.card.is-active::before { content: ""; }

不能写成 :is(.card, .card.is-active)::before——这个写法本身是合法的,因为伪元素在括号外面,只是选择器参数里的 .card.is-active 部分是普通类组合,不涉及伪元素进括号,能正常工作。真正不合法的是把伪元素本身塞进括号内,比如 .card:is(::before, ::after) 这种写法,浏览器会直接判定整条规则无效并丢弃。

兼容性:这一年还不能完全放心

Chrome 88 一月发布之后,:is():where() 在 Chromium 系(Chrome、Edge、新版 Opera)已经可以直接用,不需要前缀。但 Safari 这边情况要复杂一点::is() 在 Safari 14 里是通过 -webkit- 前缀形式 :-webkit-any() 提供的一个更早期的实现,语法和权重计算跟标准的 :is() 不完全一致,不能直接当作等价替代;:where() 在 Safari 里今年上半年还没有稳定支持。Firefox 这边 :is() 支持得比较早,:where() 也已经跟上。

公司这边面向 C 端用户的页面,Safari 还是绕不开的兼容对象,:where() 这种全新的选择器目前只敢用在明确知道目标浏览器范围的场景,比如内部管理后台、只需要兼容 Chrome 和 Edge 的场景。移动端 H5 如果要覆盖 iOS Safari,:is():where() 都还不能放心地写进主链路样式,得配一条降级方案:

1/* 降级写法,兼容不支持 :is()/:where() 的浏览器 */
2.article h2,
3.article h3,
4.article h4 {
5  margin-top: 32px;
6}
7
8/* 支持的浏览器走这条,重复的规则会被后面这条覆盖,结果一致 */
9.article :is(h2, h3, h4) {
10  margin-top: 32px;
11}

这个降级思路利用的是"浏览器遇到不认识的伪类,会让整条选择器失效、忽略这条规则"这个特性——不支持 :is() 的浏览器会跳过第二条规则,只认第一条;支持的浏览器两条都认得,因为选择器和声明完全一样,覆盖结果没有区别,纯粹是多写一份兜底。IE11 这边完全不认 :is()/:where(),公司还有几个后台页面要兼容 IE11,这类页面暂时只能维持老老实实的逗号分组写法,等这批页面淘汰或者团队彻底放弃 IE11 支持之后再迁移过去。

和已有的选择器组合方式对比

团队里之前处理"合并重复选择器"这类需求,常见的做法是 Sass 的 @each 或者占位符 %placeholder@extend

1%heading-margin {
2  margin-top: 32px;
3  font-weight: 600;
4}
5
6.article h2 {
7  @extend %heading-margin;
8}
9.article h3 {
10  @extend %heading-margin;
11}
12.article h4 {
13  @extend %heading-margin;
14}

这种写法编译出来的 CSS 其实还是三条独立规则用逗号连起来,权重也是普通的类加标签叠加,跟手写逗号分组没有本质区别,只是源码层面看起来不重复。跟 :is() 比起来,@extend 需要预处理器支持,编译产物体积可能因为选择器合并规则的方式而变得难以预测(Sass 官方文档也提到 @extend 在复杂场景下容易生成意料之外的选择器组合);:is() 是浏览器原生支持,不需要编译步骤,产出的 CSS 直接就是最终形态,选择器就是写的那一行,不会被工具悄悄改写成别的样子。

:where() 这个降权能力则是 Sass、Less 这类预处理器完全没有对应机制的——预处理器工作在编译期,只能操作选择器的文本组合,没办法在编译产物里"让某条规则的权重归零",这是纯粹的浏览器运行时特性,属于 :is()/:where() 相对预处理器方案的独有优势。之前想通过预处理器实现类似效果,只能靠命名约定或者控制嵌套层级去把权重维持在低位,做不到像 :where() 这样直接把权重清零、彻底退出竞争。

:where()!important 不是同一件事的两个方向

团队里以前处理"基础样式和业务样式打架"的问题,常见手段是反过来加 !important——业务方发现自己覆盖不了组件库默认样式,图省事直接在自己这条规则后面补一个 !important。这种做法能解决眼前的问题,但代价是权重竞赛升级了一个维度,以后如果基础样式那边也要针对某个场景做例外处理,唯一的办法是也用 !important,再往后两边都在叠 !important,回到了最原始的"谁的优先级更高"的老问题上,只是竞争的武器从选择器权重换成了 !important 的数量。

:where() 处理的是同一类问题,但方向完全相反:不是业务方去"想办法压过"基础样式,而是基础样式主动"退出竞争"。两种思路的效果在个别场景下看起来相似——业务方都能覆盖成功,但长期的可维护性差别很大。用 !important 解决覆盖问题,团队里以后每个人都要记住"这条规则被 important 过,想再改要用更高级别的手段";用 :where() 解决覆盖问题,业务方只需要记住"这是基础样式,正常写选择器就能盖过去",不需要额外的心智负担。

之前在旧代码里遇到过一个典型场景可以类比:公共表格组件的默认行高样式因为历史原因用了 !important

1.table-row {
2  height: 40px !important;
3}

后来某个业务页面需要一种紧凑模式,行高改成 32px,想覆盖这条规则,唯一的办法是也写 !important

1.table-row.is-compact {
2  height: 32px !important;
3}

这条业务规则权重是两个类,比 .table-row 高,但因为对方带了 !important,普通权重比较完全不起作用,只能跟着加。如果组件库最初就用 :where() 包一层:

1:where(.table-row) {
2  height: 40px;
3}

业务方写 .table-row.is-compact { height: 32px; } 这种普通规则就完全够用,不需要引入 !important 这一层额外的竞争维度。这也是为什么团队后来整理公共组件库时,把这类历史遗留的 !important 挨个筛查了一遍——能用 :where() 主动降权替代的,比继续叠加 !important 更值得优先处理,处理完之后业务方覆盖这条样式,写普通选择器就行,不用再纠结要不要跟着补一个 !important

表单和列表场景里 :is() 带来的实际收益

除了标题和状态类合并,团队里另一个用得比较多的场景是表单校验样式和列表分隔线。原来处理"多种校验状态共享同一批样式",写法通常是这样:

1.form-item.is-error input,
2.form-item.is-error select,
3.form-item.is-error textarea {
4  border-color: #f5222d;
5}
6
7.form-item.is-warning input,
8.form-item.is-warning select,
9.form-item.is-warning textarea {
10  border-color: #faad14;
11}

两种状态、三种表单控件,一共要写六行选择器,改一个状态颜色要动三处。用 :is() 两层嵌套合并:

1.form-item:is(.is-error, .is-warning) :is(input, select, textarea) {
2  border-color: var(--form-item-border-color);
3}

这里配合前面提到的 CSS 变量思路,把颜色也抽成变量,状态类只负责切变量的值:

1.form-item.is-error {
2  --form-item-border-color: #f5222d;
3}
4
5.form-item.is-warning {
6  --form-item-border-color: #faad14;
7}

原来的六行选择器压缩成了一条,权重也还是可预期的——:is() 内部两组都是同一类型的选择器(都是状态类、都是表单控件标签),不会出现前面提到的"参数类型混杂导致权重被意外拔高"的情况。列表分隔线是另一个例子,原来用 :not(:last-child) 处理"除最后一项都要下边框",如果同时还要排除某个特殊状态的列表项:

1.list-item:not(:last-child):not(.is-placeholder) {
2  border-bottom: 1px solid #eee;
3}

这条写法本身没问题,但如果排除条件从一个变成三四个,:not() 一层套一层会变得很难读。:is() 可以把多个排除条件先合并再取反:

1.list-item:not(:is(:last-child, .is-placeholder, .is-loading)) {
2  border-bottom: 1px solid #eee;
3}

比连续堆叠多个 :not() 更容易在视觉上看出"这些情况都要被排除"这层意思,加新的排除条件也只需要在 :is() 括号里追加一项,不用再多写一层 :not()

从组件库设计角度看这两个选择器的定位差异

站在写基础样式库的角度重新看 :is():where(),会发现它们其实服务于组件库设计里两个不同的目标。:is() 解决的是"选择器怎么写更简洁",属于书写效率层面的改进,用不用它、合并成一条还是保留分组写法,纯粹是代码整洁度的取舍,不影响样式覆盖关系是否成立——因为它的权重按最具体的参数计算,跟拆开写没有本质区别(前提是参数类型一致)。:where() 解决的是"这条规则该不该参与权重竞争",这是设计基础样式库时需要提前想清楚的架构问题:一套要被广泛复用、又要保证业务方能轻松覆盖的默认样式,用不用 :where() 包裹,从一开始就决定了未来业务方覆盖这条样式的难度。

这个差异决定了两者不能互相替代着用。如果把 :where() 当成"只是另一种合并写法"随手用在业务代码里,反而可能制造出新的问题——业务代码里权重普遍在 (0, 1, 0)(0, 2, 0) 之间,如果某条业务规则图省事套了层 :where(),权重意外降到零,很容易被别的、原本权重更低的规则意外压过去,排查起来会比正常的权重打架更让人困惑,因为大部分人还没养成"看到 :where() 就默认权重是零"这根弦。团队内部现在的约定是:where() 只用在明确要给"基础样式、默认值"降权的场景,业务代码内部的选择器合并优先用 :is(),避免两种用途混着来。

DevTools 里怎么看这两个新选择器的实际权重

Chrome 88 更新了 Elements 面板对 :is():where() 的展示方式,选中一个元素之后,Styles 面板里如果某条规则命中是通过 :is():where() 匹配的,会正常展示这条规则、并且照常在被覆盖时画删除线,这一点跟普通选择器的展示逻辑一致,不需要用别的方式单独排查。真正需要留意的是权重数值本身——DevTools 面板目前不会额外标注"这条规则因为用了 :where() 权重被清零"这类说明文字,想确认一条用了这两个新选择器的规则实际权重是多少,还是得靠手算,规则是前面讲的那两条::is() 按参数里最具体的算,:where() 恒为零。

排查过一次典型的疑惑:团队里刚开始试点 :where() 包裹基础按钮样式那阵子,有同事反馈"业务页面里明明没写更具体的选择器,为什么 :where(.btn) 里定义的圆角还是被覆盖了"。打开 DevTools 一看,业务页面里恰好有一条更早期遗留的规则 button { border-radius: 0; },标签选择器权重是 (0, 0, 1),而 :where(.btn) 权重是 (0, 0, 0),哪怕只是一个孤零零的标签选择器,也稳赢权重为零的规则。这次排查最后归因到"忘记了 :where() 包裹之后权重比任何正常选择器都低,包括标签选择器",也印证了前面强调的那句话——用 :where() 包裹之后,权重比看起来还要更低一级,连最基础的标签选择器都能压过它,这是它被设计出来的目的,不是缺陷。

Firefox 这边的开发者工具在 88 版本前后也跟进了 :is()/:where() 的解析支持,规则面板同样会正常展示这两种选择器命中的规则,角标数值也会正确反映出 :where() 清零之后的实际权重,比 Chrome 目前的表现更直观一点——不需要额外手算,角标直接显示 (0,0,0)。团队里如果对某条规则的实际权重把握不准,会习惯性地在 Firefox 里再核对一遍,比单纯在 Chrome 里靠心算更可靠。

从存量项目往新写法迁移的顺序

团队里存量的样式代码量不小,不可能一次性把所有逗号分组的选择器都换成 :is(),也不可能把所有基础样式一下子套上 :where(),迁移只能按优先级分批做。目前定的顺序是:先处理新写的代码,新功能里遇到需要合并重复选择器的场景,直接用 :is(),不再照抄老的逗号分组写法;旧代码暂时不动,除非顺手改到这一块、或者这段选择器本身正在制造覆盖问题,才顺带重构成新写法,不专门抽时间做批量替换。

:where() 这边更谨慎一些,因为它改变的是权重语义,不是单纯的书写方式,贸然把一批存量基础样式套上 :where(),可能会导致某些原本靠权重"苟且"生效的业务规则突然被更低权重的规则反超,属于有回归风险的改动。团队里的做法是只在明确知道调用方是谁、覆盖关系可控的场景先试点——内部管理后台的公共表单组件是第一批,因为使用方范围小、出问题能很快定位到具体页面;公司主站面向 C 端用户的基础样式库暂时保持原样,等这批试点跑完一段时间、确认没有意外的覆盖回归,再考虑扩大范围。

这个节奏跟兼容性限制也是配套的——反正 Safari 和 IE11 那边这两个新选择器还没法完全放心用,主站需要覆盖全量浏览器的部分本来就得保留降级方案,不如把试点范围先收在能确定浏览器环境的内部系统里,等生态和兼容性都跟上以后,再把这套思路铺开到需要兼容更广泛浏览器的项目中。

响应式断点场景下 :is() 的另一种用法

团队里做移动端适配,经常要在多个断点下重复同一批选择器的写法,比如导航栏在窄屏和宽屏下都需要处理链接的 hover/focus 态,只是触发的媒体查询条件不同:

1@media (min-width: 768px) {
2  .nav-link:hover,
3  .nav-link:focus {
4    color: #1677ff;
5  }
6}
7
8@media (max-width: 767px) {
9  .nav-link:hover,
10  .nav-link:focus {
11    color: #1677ff;
12    text-decoration: underline;
13  }
14}

两个媒体查询块内部都有一段几乎相同的选择器,只是声明内容有细微差异。用 :is() 合并选择器本身:

1@media (min-width: 768px) {
2  .nav-link:is(:hover, :focus) {
3    color: #1677ff;
4  }
5}
6
7@media (max-width: 767px) {
8  .nav-link:is(:hover, :focus) {
9    color: #1677ff;
10    text-decoration: underline;
11  }
12}

这种场景收益不算特别大,因为真正重复的是整个媒体查询块,:is() 只能精简块内部的选择器部分。但如果项目里恰好有大量类似"同一个组件在多个断点下样式微调、选择器本身却完全一致"的场景,精简下来的量级还是可观的,尤其是伴随一堆 :hover:focus:focus-visible 这类交互状态伪类反复出现的场景,用 :is() 收拢一层能明显减少视觉噪音。

顺带提一句,:focus-visible 这个伪类今年在 Chrome 里也逐渐能用了,用来区分"键盘聚焦"和"鼠标点击聚焦",跟 :is() 搭配起来处理无障碍相关的交互态样式也很顺手:

1.nav-link:is(:hover, :focus-visible) {
2  color: #1677ff;
3}

这样鼠标点击不会触发这条高亮样式(避免点击瞬间的视觉抖动),只有键盘 Tab 切换聚焦或者鼠标悬停时才生效,是无障碍适配里一个实际能落地的小改进,跟本文的选择器权重话题关系不大,但因为经常和 :is() 一起出现在同一行代码里,顺带记一笔。

和 BEM 命名习惯搭配时要注意的地方

团队里样式命名一直沿用 BEM 风格的约定,:is()/:where() 这两个新选择器和 BEM 搭配使用时,有一点需要留意:BEM 本身追求的是"用命名表达层级关系,选择器尽量扁平",如果不加分辨地把 :is() 用来省事合并跨组件、跨层级的选择器,反而可能违背 BEM 一开始想解决的问题。比如:

1:is(.card__title, .dialog__title, .panel__title) {
2  font-weight: 600;
3}

这条规则表面上"合并了三个组件的标题样式",看起来很精简,但实际上把三个本该独立维护的组件耦合到了同一条规则里——以后想单独调整 .dialog__title 的字重,会发现改动会牵连另外两个组件,这跟 BEM 命名法"每个组件的样式该是独立的、不相互影响"的初衷是矛盾的。:is() 更适合用在同一个组件内部、状态和结构确定属于同一个语义单元的场景(比如前面表单校验的例子,几种校验状态本来就是同一个表单项的不同状态),而不是用来"发现选择器长得像就合并"。

这条经验是团队在 code review 时补充的一条检查项:看到 :is() 括号里的参数分别来自不同组件命名空间(比如 .card__.dialog__ 这类不同前缀混在一起),会被要求说明这几个组件是否真的需要共享同一条样式规则,如果只是巧合地文字重复,应该保留各自独立的规则,即便这样会多写几行。选择器精简和组件边界清晰,这两件事发生冲突的时候,团队里目前的判断是优先保住组件边界清晰,精简选择器只在同一个组件或者同一语义单元内部去做。

实际用下来的取舍

这两个选择器目前在团队项目里用得还比较克制,主要有两个原因。一是兼容性没有完全跟上,业务代码库里能确定目标浏览器不含 Safari 早期版本、不含 IE11 的部分才会用,公共基础样式因为要面向所有项目,暂时还没有大规模替换成 :where() 包裹的写法,只在内部管理后台这类可控环境里先试点。二是团队里还在熟悉这两个新语法的心智模型——:is() 权重按参数里最高的算、:where() 权重恒为零,这两条规则乍一看容易记混,写 code review 意见的时候有必要专门指出"这条用的是 :is() 还是 :where(),权重预期是不是符合写的人的本意"。

:is() 目前主要用来精简那些前缀重复、只有末尾标签或伪类不同的选择器,收益是维护成本降低,重复率高的地方改一次就够;:where() 目前只在内部管理后台的基础组件样式库里小范围试点,把按钮、表单控件这类高频复用组件的默认样式包一层,验证业务页面能不能顺畅地用普通类名覆盖,不需要额外堆权重。等 Safari 那边的支持情况更稳定一些,覆盖的浏览器范围能放宽,这条思路应该会推广到更多面向 C 端用户的样式模块里去。

这两个选择器解决的问题本质,其实是给"选择器"这件事增加了一个新的自由度——以前选择器只能描述"匹配哪些元素",权重完全由匹配过程中出现的类型和数量决定,写选择器的人没法单独控制权重这个维度;:where() 第一次让权重可以脱离选择器结构本身,变成一个可以主动设定的值。这对写基础样式库、写需要长期被别人复用的公共样式,是一个此前没有过的手段。团队里接下来准备做的一件事,是把这条思路写进内部的样式规范文档里,明确"新建的公共组件基础样式默认用 :where() 包裹"这条约定,等团队对这套心智模型都熟悉了之后,作为默认写法固定下来,不用每次都单独讨论要不要用。

另外一件短期内会推进的事,是把公司内部几个 lint 工具规则里补一条针对选择器权重的检查——目前团队用 stylelint 做样式代码规范检查,社区里已经有维护者在讨论要不要新增一条规则,用来识别":is() 参数列表里混用了不同类型选择器导致权重被意外拔高"这类隐患,如果这条规则今年内能落地,团队会优先接入,把前面提到的几类容易被忽略的权重陷阱,从人工 code review 里挪一部分到自动化检查上,减少纯靠肉眼判断的成本。

这两个选择器加进来之后,团队处理选择器权重问题时,手上多了两种不同方向的工具:一种是精简写法、不改变权重结果,用在选择器重复度高的地方;另一种是主动放弃权重、把覆盖的主动权交给使用方,用在基础样式和组件库这类需要长期被复用的代码里。分清楚该用哪一种,比记住它们各自的语法细节更重要。