你曾经花多少时间在纠结CSS类名上?BEM、SMACSS、ITCSS,这些命名约定存在的理由只有一个:CSS没有原生作用域。为一个组件写的样式规则,会应用到页面上任何匹配的元素,不管你愿不愿意。
于是有了两种应对方案。要么靠严谨的命名规范,把类名写成card__title这种形式;要么靠构建工具在编译时把选择器重写成哈希字符串,比如CSS Modules或者CSS-in-JS。这两种方案都能用,但都不是平台本身在替你解决这个问题。
现在,平台出手了。
@scope在Chrome 118、Firefox 128和Safari 17.4中均已发布,浏览器支持已相当广泛,不需要任何polyfill。它接收一个作用域根——标记子树边界的那个选择器——以及一个可选的作用域限制——标记作用域在哪里停止的选择器。写在@scope块内的规则,只对作用域根的后代元素生效,并且不会穿透到作用域限制所标记的区域内。
基本写法很直观。当你写下@scope (.card)之后,.title只会匹配位于.card内部的class为title的元素。卡片外面的.title不会受到影响,嵌套在其他组件内部的卡片里的.title同样不受干扰。你不需要再写成.card .title,选择器本身就隐式地限定了作用域。font-size和font-weight的设置干净地归属于这个card组件。
作用域的下边界,才是@scope真正区别于普通后代选择器的关键。假设你的卡片组件里可以包含一个推荐证言子组件,而这个推荐证言自己也有一个.title。你想要卡片样式不要泄漏进推荐证言里。@scope (.card) to (.testimonial)就能做到:.title只在位于.card内部、同时不处于该.card内部的.testimonial之中时才匹配。没有下边界时,后代选择器会匹配整个子树里的每一个.title,包括那个推荐证言的。你最多能借助.card .title:not(.testimonial *)这种逃逸写法,然后还要在组件里的每一条规则上都正确地重复它。@scope的做法是:在前奏里一次性声明边界,块中所有规则自动继承这一限定。
实际的BEM替换效果非常直观。传统的BEM写法中,你需要定义.card、.card__header、.card__title、.card__body、.card__footer,每个类名都承担了记载作用域关系的信息。切换到@scope之后,HTML中的类名直接变成.header、.title、.body、.footer,去掉了card__前缀。命名约定此前是作用域关系的说明书,而@scope让这种关系变成了结构本身。
还有一个值得了解的细节,大多数@scope的讲解文章都把它讲反了。那就是邻近性打破平局的规则。当同一个元素同时被多个作用域覆盖时,选择器特异性不再单独决定胜负,离元素最近的祖先作用域所定义的样式将胜出。这个机制让嵌套组件的样式控制变得出人意料地自然,组件之间的隔离不再是依靠全局的命名约定或工具链,而是在CSS层叠本身的机制中完成。
从BEM到CSS Modules,整个前端社区用十多年时间建立的工作方法,本质都是弥补CSS没有作用域的缺憾。当@scope进入稳定支持的阶段,这些补充性的工作方法有机会从必要条件退化为可选方案。CSS作为一种语言,此刻拿回了本该属于它的结构能力。
热门跟贴