CSS 原生作用域

scope 的历史

其实把 css 属性限制在 DOM 的某些元素作用范围之内并不是什么新奇的想法,在 <style> 标签上设置 scope 属性是为了解决这个问题的一种尝试. 但它已经被弃用了, 所以现在我们根本看不到它的影子(当然在 Vue SFC 中还是很常见的,因为 Vue 的很多语法本身就是借鉴了原生 HTML/CSS 的一些功能设计,这里指的是没有使用任何框架的情况)。

<div>
  <style scope>p{color: #f00}</style>
  <p>Red Text</p>
</div>

<p>Normal Text</p>

现在前端框架会通过在 css 属性前面加上一些特殊的属性选择器来实现这个效果。但是这需要框架来处理这件事,并且不总是那么稳定。后来组件以自定义元素和 shadow-DOM 的形式进入浏览器。事实上,shadow-DOM 中的 CSS 都是带有作用域的,但是它也不允许这部分样式被外部使用。

原生 scope 仍在讨论

一开始遗弃 scope 的确切原因并不是很明细。有人说这是浏览器不想实现它,有人说这是为了等 Web 组件成为一种流行趋势,然后再重新评估 CSS scope 的需求. 但无论真实情况如何,CSS 的作者似乎仍然对 scope 感兴趣。

@scope

@scope 规则是把 CSS 作用域引入浏览器的最新尝试. 它在 CSS Cascading and Inheritance Level 6 规范的 工作草稿 中有对其相关的描述. 虽然它现在还不可用,但是不难看出还是有很多人对它非常期待,并且在大肆宣传它!


它的工作方式很简单: 首先我们需要定义我们的规则将会在哪里应用。它可以使用任意 CSS 选择器,但是为了准确标识出来,以下会使用 outer-component inner-component 来表示一个作用范围.

@scope (output-component) {
  p { color: red}
}

在此范围块内编写的任何规则都将仅适用于选择器描述的元素内。

<p>This text is black</p>
<outer-component>
  <p>This text is red</p>
</outer-component>

我们还可以描述这个范围的限制, 用另一个选择器告诉浏览器范围不应该应用于某个子树。

@scope (outer-component) to (inner-component) {
   p { color: red; }
}
<outer-component>
  <p>This text is red</p>
  <inner-component>
    <p>This text is black</p>
  </inner-component>
</outer-component>

为什么要宣传一个还没有成为正式的草案?

上面那种具有嵌套自定义元素的例子在实际开发中非常常见,让样式只适用于某个特定节点内部而不使用节点名称来作为选择器前缀已经很有用了,但是仍然会有一些边界情况,添加一个优先级比较低的边界条件用于防止样式泄露到嵌套组件中的需求变得非常必要,特别是现代的前端环境中,它不断从一个整体逐渐转变成小型、便携和可互换的组件。

如果你正在为一些普通的 JS 组件编写纯 CSS,你希望你可以编写类似于 Svelte、Vue 这样的框架允许你做的事情:编写一堆规则,它们值适用于组件内部。比如说像下面这样在一个没有任何组件或自定义元素的情况下可能生效的示例:

th { background: black; color: white; }
@scope (table.vertical) to (table) {
  th::after { content: ':' }
  th { all: initial; }
}

同样的,对于框架的作者来说,原生 CSS scope 作用域也会大大降低他们需要处理的工作,因为他们不再需要为选择器添加前缀,也不用在将 ID 或类添加到它们应用的元素中。比如:

component_css = `
  @scope ([data-component="${component.name}"]) to ([data-component]) {
    ${component.styles}
  }
`

以上.