组件间样式隔离的几种方案

CSS 即层叠样式表(Cascading Style Sheets)是一种样式表语言,它没有作用域的概念,引入即全局生效的,但一个样式是否起作用由多个因素共同决定,比如:

  • 重要程度
  • 优先级
  • 样式加载顺序 所以当我们在页面中使用时可能会遇到组件间的样式互相影响的情况,特别是引入了多个组件库或者类名命名不规范时,我们就需要对样式进行隔离,这样就可以避免样式冲突。下面我介绍几种方案:

类名添加特定的前缀

一般来说,我们会在组件内部使用一个特定的前缀,避免组件之间的样式冲突。比如 antd 的组件内部的样式,都会使用ant-前缀,element-ui 的组件内部的样式,都会使用el-前缀。 由于原生 css 的功能太弱鸡了, 我们在实际开发中一般使用 css 预处理框架如 less, sass 等,对于这种我们也可以使用一个类似的功能。

对于 less

// button.less
@name: v-;

.@{name}button {
  background-color: green;
}

// 编译为
// .v-button {
//   background-color: green;
// }

重写前缀

@import "button.less";
@name: k-;

// 编译为
// .k-button {
//   background-color: green;
// }

对于 sass

sass 目前版本支持与 less 类似的写法,但是 Sass 团队不鼓励继续使用@import规则。 并计划在未来几年逐步淘汰它,作为替代方案,他们推荐使用@use规则。详细原因请参考https://sass-lang.com/documentation/at-rules/import

/* button.scss */
$name: v-;

@mixin configure($name: $name) {
  @if $name {
    $name: $name !global;
  }
}

@mixin styles {
  .#{$name}button {
    background-color: green;
  }
}

重写前缀

@use "./button.scss";

@include button.configure($name: k-);

@include button.styles;

CSS in JS

CSS-in-JS 就是将应用的 CSS 样式写在 JavaScript 文件里面, 这样你就可以在 CSS 中使用一些属于 JS 的诸如模块声明,变量定义,函数调用和条件判断等语言特性来提供灵活的可扩展的样式定义。CIJ 还没有形成真正的标准,但在接口 API 设计、功能或是使用体验上,不同的实现方案越来越接近,其中最受欢迎的解决方案是 styled-components(styled-components 本身是为 React 设计的,可以使用 vue-styled-components 替代),它删除了组件和样式之间的映射。这意味着当你定义你的样式时,你实际上是在创建一个普通的 React 组件,它附加了你的样式。并为你的样式生成唯一的类名,CSS-in-JS在 VUE 中用的较少,因为 VUE 本身提供了类似的组件隔离样式的解决方案,但是在 React 中,它是一个很好的解决方案。

// The Button from the last section without the interpolations
const Button = styled.button`
  color: palevioletred;
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;

// A new component based on Button, but with some override styles
const TomatoButton = styled(Button)`
  color: tomato;
  border-color: tomato;
`;

render(
  <div>
    <Button>Normal Button</Button>
    <TomatoButton>Tomato Button</TomatoButton>
  </div>
);

Scoped CSS

在 VUE 中有Scoped CSS 的概念,当 <style> 标签有 scoped 属性时,它的 CSS 只作用于当前组件中的元素。这类似于 Shadow DOM 中的样式封装。通过对组件添加数据属性,然后在 style 中使用属性选择器让组件的样式只作用于组件。它通过使用 PostCSS 来实现以下转换:

<style scoped>
  .example {
    color: red;
  }
</style>

<template>
  <div class="example">hi</div>
</template>

转换结果

<style>
  .example[data-v-f3f3eg9] {
    color: red;
  }
</style>

<template>
  <div class="example" data-v-f3f3eg9>hi</div>
</template>

详情参考https://VUE-loader.vuejs.org/zh/guide/scoped-css.html#scoped-css

CSS Modules

CSS Modules 是一个流行的,用于模块化和组合 CSS 的系统.

VUE 3 原生支持了CSS Modules,通过在你的 <style> 上添加 module 特性, 这个 module 特性指引 Vue Loader 作为名为 $style 的计算属性,向组件注入 CSS Modules 局部对象。它将类名编译成一个独一无二的哈希字符串,来保证样式只在组件内生效。

<template>
  <p :class="$style.red">This should be red</p>
  <p :class="$style.red">This should be red</p>
  <p :class="$style.bold">This should be bold</p>
</template>

<style module>
  .red {
    color: red;
  }
  .bold {
    font-weight: bold;
  }
</style>

转换结果

<template>
  <p class="_red_1cpg3_4">This should be red</p>
  <p class="_red_1cpg3_4">This should be red</p>
  <p class="_bold_1cpg3_7">This should be bold</p>
</template>

<style>
  ._red_1cpg3_4 {
    color: red;
  }
  ._bold_1cpg3_7 {
    font-weight: bold;
  }
</style>

使用 shadow DOM

不同与 VUE,React, Web 提供了一个标准的组件模型 Web Components,它将标元素、样式和行为封装起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起, 如下面使用 Web Components 创建一个 button

customElements.define(
  "my-button",
  class extends HTMLElement {
    constructor() {
      super();

      const shadow = this.attachShadow({
        mode: "open",
      });

      const wrapper = document.createElement("button");
      wrapper.innerText = "Button";
      const style = document.createElement("style");
      style.textContent = `
      button {
        color: #0B8BF4;
        border-radius: 4px;
      }
    `;
      shadow.appendChild(style);
      shadow.appendChild(wrapper);
    }
  }
);

原子化 CSS

原子化 CSS 是一种 CSS 的架构方式,它倾向于预先定义小巧且用途单一的 class,并且会以视觉效果进行命名。然后扫描代码中的 class 按照使用到的 class 样式提取出来,并且把它们放到一个单独的文件中。

<div class="m-0 text-red"></div>
.m-0 {
  margin: 0;
}
.text-red {
  color: red;
}

在使用这种方案时,组件会很少需要使用到自定义的样式,所以也就不用关心样式冲突的问题了。