样式隔离演进史

样式隔离的演进历程: 从命名约定到 Web 标准

计算机科学中有两大难题, 缓存失效和命名冲突, 早期 Web 开发中, 随着组件的增多, 为不同样式取一个不重复的合适命名变得愈发复杂,样式隔离的需求也由此产生

命名约定

2000 年代, 一些开发人员使用命名约定进行人工隔离, 例如 BEM (Block__Element--Modifier), OOCSS, SMACSS 等.

此类方案都专注于将样式抽象为可复用的名称, 但远没有原子化那样精细, 也是最容易上手的隔离方案.

这种隔离方案历史悠久, 但并未被完全抛弃, Bootstrap, Ant Design 等主流库都有此隔离方案参与或部分参与.

动态样式

通过操作 DOM Api, 进行动态样式插入. 此种方案生成将生成较为复杂的选择器以避免命名冲突. 由于经常需要生成涵盖各种状态的大量样式, 或者根据状态变化大量修改样式, 其性能也会损失严重.

框架的样式隔离

一些前端框架内置了样式隔离的方案, 可以在不改变原有样式的情况下, 实现样式的范围化.

属性隔离 (例如 Angular 和 Vue)

为应用样式的组件添加唯一标记例如 data-v-*_ngcontent, 并在构建或渲染时为样式选择器附加该属性.

全路径选择器 (例如 Riot)

从文档根开始, 生成直到应用样式的组件的全路径样式选择器, 确保样式仅匹配该路径下的元素.

这种方案的优势是简单, 无需额外的构建步骤, 但是它的劣势是性能问题, 因为它需要在运行时遍历 DOM 树, 并且需要在运行时生成样式选择器, 全路径选择也容易比属性隔离生成更繁琐的样式.

CSS 模块

CSS 模块是一种将 CSS 类名局部化的技术, 它要求 CSS 文件以 .module.css 结尾, 通过将类名进行哈希, 生成不易重复的新类名.

得益于构建工具的支持, 我们也可以在不改变原有类名的情况下, 实现样式的范围化, 并且能够在构建阶段将类别转换到确定的对象.

因此每一个 CSS 模块都包含: 一个样式表, 需要插入到文档. 一个 JS 对象, 用于映射类名.

与命名约定的结合

即使使用了 CSS 模块, 仍有很多应用在使用 BEM 命名约定来规范模块里的类名. 因为 BEM 能够清晰地表达组件的结构和状态. 使用 BEM 命名可以轻松区分不同的元素和修饰符, 不使用类名又容易将样式传递到子元素.

Web 标准 @scope 规则

@scope 是一种新的 CSS 规则, 它允许你定义一个精确的作用域.

@scope (scope root) {
}
@scope (scope root) to (scope limit) {
}

@scope 也可以内联到 style 元素内, 使用方式非常自由.

<div>
  <style>
    @scope {
    }
  </style>
</div>

可惜此规则的浏览器兼容性过低, 尚未得到广泛支持. 对于不支持的设备, 你可以尝试 这个 Polyfill.

Shadow DOM

Shadow DOM 允许为元素创建独立的子树,其内部样式与主文档完全隔离.

仅有 CSS 变量, 部分 CSS 规则和 ::part 选择器能够穿透 Shadow DOM.

Shadow DOM 虽然是一个很好的解决方案, 它总是被主流工具所忽略, 有很多项目都会添加会影响 Shadow DOM 的样式, 例如使用全局通配符样式:

* {
  border: 0;
}

这将会破坏声明了边框样式的 Web 组件的样式. 却成为了很多库的默认行为.

理想的做法是仅针对特定元素重置样式, 例如:

input,
textarea,
select,
button,
fieldset,
iframe {
  border: 0;
}
在 GitHub 上编辑