CSS Modules 的用法

2022/10/1 css

# 为什么需要

在写一个简单页面的时候可能就是直接写 CSS 来处理样式,如果页面比较多还很复杂的话直接使用原生的 CSS 相对来说就比较麻烦了,所以就有使用 Less、Sass 等预处理器方案来处理诸如嵌套以及变量等问题。 但是预处理器最终生成的 CSS 文件和直接写的 CSS 没有区别,只是对过程更加友好。所以还是有一些比较明显的问题不是很好解决:

  • 全局样式污染 —— CSS 选择器都是全局生效的,只要加载到页面中就会根据对应的权重规则的进行生效。如果定义的选择器比较简单就可能会被其他元素匹配到,导致样式被污染。
  • 选择器复杂,命名复杂 —— 为了避免上述问题,在写样式的时候要尽可能带上专有的前缀,有的时候还要附加父组件的选择器

# 是什么

为了解决上述问题,就有了 CSS-in-JS 或者 scope 等各种方案。由于 CSS 的选择器的设定就是全局生效,所以各种方案都是通过行内样式或者一些方式模拟 scope 来实现的,CSS Modules (opens new window) 就是通过给选择器生成唯一的名称来实现 scope 的一种方案。

A CSS Module is a CSS file in which all class names and animation names are scoped locally by default.

CSS Modules 的基本原理很简单,就是对一个文件中的不同 local 选择器按照一定规则进行转换,保证它的唯一性(相同名称的选择器转换后的名称也是相同的),由于不会与其他文件的选择器重名,所以可以被当作局部作用域。 CSS Modules 本身不是新的语法或者浏览器的实现,只是对构建过程的处理来处理 modules,对不同的构建工具都提供了插件以支持 CSS Modules 的编译,比如 Webpack 的 CSS Loader (opens new window) 插件可以通过 modules 选项开启,Vite (opens new window) 对以 .module.css 为后缀名的文件都作为 CSS Modules 进行处理的。

# 怎么用

CSS Modules 使用起来很简单,只有几个实用的规则来增强功能,默认情况下就和原生 CSS 一样写就行了。

.className {
  color: green;
}
1
2
3
import styles from "./style.css";
// import { className } from "./style.css";

element.innerHTML = '<div class="' + styles.className + '">';
1
2
3
4

# 选择器命名

根据 CSS Modules 的官方文档,推荐以 camelCase(驼峰式)的命名方式定义类名,而不是 kebab-casing(短横线隔开式),因为不能直接 style.class-name,当然也可以用 style['class-name'] 的形式,只是直接用 style.className 的形式会更简洁。 当然,如果还是习惯在样式文件中以 kebab-casing 的形式定义选择器,但是也想要用的时候可以直接是 style.className 的形式,这个时候就需要构建工具的支持,Webpack 中的 exportLocalsConvention (opens new window) 或 Vite 的 localsConvention (opens new window) 配置项都可以简单的配置后即生效。

# Exceptions

:global 处理的选择器不会被转换,所以最后的结果还是定义时候的选择器名,是会被作用到全局作用域的;用 :local 处理的选择器会被转换,所以最后会生成一个新的选择器名以产生局部作用域。 构建工具一般情况下默认都是 :local,可以通过修改 Webpack 的 mode 或 Vite 的 scopeBehaviour 来调整默认的作用域。

# Composition

Composition 就是一个选择器可以把另一个选择器的规则组合到自身来。组合可以是同一个 CSS 文件的不同选择器之间,也可以是不同 CSS 文件的选择器之间。不同 CSS 文件之间的组合可以大致的起到模块化的作用。

.className {
  color: green;
  background: red;
}

/* 和当前模块的选择器组合 */
.otherClassName {
  composes: className;
  color: yellow;
}

/* 和其他模块的选择器组合 */
.otherClassName {
  composes: className from "./style.css";
}

/* 和global的选择器组合 */
.otherClassName {
  composes: globalClassName from global;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

一个选择器里面可以有多条 composes 规则,应该要避免多个选择器中为同一属性定义不同的值,这样会使得结果变得不可预测。同时在不同文件之间组合的时候注意不要造成循环依赖。 CSS Modules 只会对 className 以及 id 进行转换,其他的比如属性选择器,标签选择器都不进行处理,推荐尽量使用 className。由于不用担心类名重复,className 可以在基本语意化的前提下尽量简单一点儿。

# 定制 CSS Module 默认行为

正如之前描述的,CSS Module 是通过构建工具处理的,所以任何需要改变 CSS Module 默认行为的操作都是在构建工具中进行修改,比如上述 选择器命名 中就需要构建工具的支持,不同构建工具提供的配置项也会存在一些差异,要根据项目所用的构建工具自行配置。

# vs code 扩展

如果是使用 vs code 作为编辑器的可以安装 CSS Modules (opens new window) 扩展方便在 js 中提示选择器。如果给构建工具开启了 kebab-casingcamelCase 的转换,扩展需要进行配置以启用对应功能。

{
  "cssModules.camelCase": true
}
1
2
3

# Less 中使用

写的时候和直接使写 Less 没有什么区别,文件名一般为 *.module.less。 用的时候和直接使用 Less 的区别在于用的时候不是直接 import 就行了,而是以 js module 的形式进行 import,得到的结果是 Less 文件中定义的所有 local 选择器(包括嵌套的 local 选择器)平铺到最外层的一个名称映射的对象,用的地方再使用对象中映射的名称。整体和直接用 css 区别不大,主要就是要注意嵌套的 local 选择器会被提升到最外层,所以用的时候也是直接用的,和 CSS Module 的原理也是相符的。

# 全局样式

/* 定义全局样式 */
:global(.text) {
  font-size: 16px;
}

/* 定义多个全局样式 */
:global {
  .footer {
    color: #ccc;
  }
  .sider {
    font-size: 16px;
    .title {
      background: #ebebeb;
    }
  }
}
.item {
  font-size: 14px;

  /* 定义样式中的全局样式 */
  :global(.text) {
    font-size: 16px;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.text {
  font-size: 16px;
}
.footer {
  color: #ccc;
}
.sider {
  font-size: 16px;
}
.sider .title {
  background: #ebebeb;
}
._item_1a4t3_15 {
  font-size: 14px;
}
._item_1a4t3_15 .text {
  font-size: 16px;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

从结果 CSS 中可以看到,如果是直接在外层的 global 中定义的样式编译后也是直接在最外层的,如果是在某个 local 中定义的全局样式编译后也是和外层的 local 相关联的。 所以不是说用 global 定义的就能随便用,只是说不会被当做 local 转换掉,但是层级关系还是有的。这也是为什么不直接用 CSS 而要用 less 的原因之一。

# 参考