什么是 CSS Modulse ?
在做 vue 等 Node 项目时,经常会有一项:是否启用 CSS Modules,如下图所示: 
那么,什么是 CSS Modules 呢?使用它有什么意义呢?
CSS Modules,直接强译,就是 css 模块的意思。
当 web 研发越来越多时,为了应对 CSS 的凌乱,于是人们想到了用这种方法。 比如有人在 home.vue 中,直接在文件中行内写了一行代码
<style> .btn{font-size:1rem;} </style> 接下来,这位作者,又在 about.vue 中,同样在文件中行内写了这么一个相同的 css 定义:
<style> .btn{font-size:0.4rem;} </style> 那么,这两个代码最后编译会不会冲突?有没有办法规避? 需要知道,vue 这样的框架在诞生,背后很大目的就是为了解决中后台开发者不熟悉 css 甚至是切图这些而生,CSS Modules 则能规避这个问题,他会将两个.btn类分别生成.home_btn和.about_btn,从而规避。 当然,背后的逻辑会比这个复杂得多,我们慢慢展开讲解。
为什么引入 CSS Modeules
或者可以这么说,CSS Modules 为我们解决了什么痛点。针对以往我写网页样式的经验,具体来说可以归纳为以下几点:
全局样式冲突
过程是这样的:你现在有两个模块,分别为 A、B,你可能会单独针对这两个模块编写自己的样式,例如 a.css 、b.css ,看一下代码
// A.js import './a.css' const html = '<h1 class="text">module A</h1>' // B.js import './b.css' const html = '<h1 class="text">module B</h1>' 下面是样式:
/* a.css */ .text { color: red; } /* b.css */ .text { color: blue; } 导入到入口 APP 中
// App.js import A from './A.js' import B from './B.js' element.innerTHML = 'xxx' 由于样式是统一加载到入口中,因此实际上的样式合在一起(这里暂定为 mix.css )显示顺序为:
/* mix.css */ /* a.css */ .text { color: red; } /* b.css */ .text { color: blue; } 根据 CSS 的 Layout 规则,因此后面的样式会覆盖掉前面的样式声明,最终有效的就是 text 的颜色为 blue 的那条规则,这就是全局样式覆盖,同理,这在 js 中也同样存在,因此就引入了模块化,在 js 中可以用立即执行函数表达式来隔离出不同的模块
var moduleA = (function(document, undefined){ // your module code })(document) var moduleB = (function(document, undefined){ // your module code })(document) 而在 css 中要想引入模块化,那么就只能通过namespace来实现,而这个又会带来新的问题,这个下面会讲到。
嵌套层次过深的选择器
为了解决全局样式的冲突问题,就不得不引入一些特地命名namespac来区分scope,但是往往有些namespace命名得不够清晰,就会造成要想下一个样式不会覆盖,就要再加一个新的namespace来进行区分,最终可能一个元素最终的显示样式类似如以下:
.widget .table .row .cell .content .header .title { padding: 10px 20px; font-weight: bold; font-size: 2rem; } 在上一个元素的显示上使用了 7 个选择器,总结起来会有以下问题:
- 根据 CSS 选择器的解析规则可以知道,层级越深,比较的次数也就越多。当然在更多的情况下,可能嵌套的层次还会更深,另外,这里单单用了类选择器,而采用类选择器的时候,可能对整个网页的渲染影响更重。
- 增加了不必要的字节开销
- 语义混乱,当文档中出现过多的 content、title 以及 item 这些通用的类名时,你可能要花上老半天才知道它们到底是用在哪个元素上
- 可扩展性不好,约束越多,扩展性越差 [注] CSS 的渲染规则可以参看这篇文章探究 CSS 解析原理 https://juejin.im/entry/5a123c55f265da432240cc90
会带来代码的冗余
由于 CSS 不能使用类似于 js 的模块化的功能,可能你在一个 css 文件中写了一个公共的样式类,而你在另外一个 css 也需要这样一个样式,这时候,你可能会多写一次,类似于这样的
/* a.css */ .modal { position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 1; background-color: rgba(0, 0, 0, 0.7); } .text { color: red; } /* b.css */ .modal { position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 1; background-color: rgba(0, 0, 0, 0.7); } .text { color: blue; } 那么在合并成 app.css 的时候,就会被编写两遍,虽然样式不会被影响,但是这样实际上也是一种字节浪费,当然,上述的这种情况完全是可以通过公用全局样式来达到目的,但是,这种代码重复通常是在不知情的情况下发生的。
一些解决方案
针对上述的一些问题,也有一些解决方案,具体如下:
CSS 预处理器(Sass/Less 等),Sass,Less 的用法这里不再赘述,如果不清楚,可以自己查阅相关资料去了解一下,其中 sass 的使用文档在这里可以看到 http://code.z01.com/sass
CSS 预处理器最大的好处就是可以支持模块引入,用 js 的方式来编写 CSS,解决了部分 scope 混乱以及代码冗余的问题,但是也不能完全避免。同时,也没有解决全局样式的冲突问题
一个 SASS 的的文件是这样的:
/* app.sass */ @import './reset' @import './color' @import './font' 可以实际上编译之后,终究还是一个文件,因此不可避免的会出现冲突样式
BEM ( Block Element Modifier )
There are only two hard problems in Computer Science: cache invalidation and naming things Phil Karlton
BEM就是为了解决命名冲突以及更好的语义化而生的。
BEM 名词解释 Block:逻辑和页面功能都独立的页面组件,是一个可复用单元,特点如下:
- 可以随意嵌套组合
- 可以放在任意页面的任何位置,不影响功能和外观
- 可复用,界面可以有任意多个相同 Block 的实例
- Element:Block 的组成部分,依赖 Block 存在(出了 Block 就不能用)
- [可选]定义 Block 和 Element 的外观及行为,就像 HTML 属性一样,能让同一种 Block 看起来不一样
** 命名规则 **
Block作为最小的可复用单元,任意嵌套不会影响功能和外观,命名可以为header、menu等等
<style> .header { color: #042; } </style> <div class="header">...</div> Element依附 Block 存在,没有单独的含义,命名上语义尽量接近于 Block,比如 title、item 之类
<style> .header { color: #042; } .header__title { color: #042; } </style> <div class="header"> <h1 class="header__title">Header</h1> </div> Modifier是一个元素的状态显示,例如active、current、selected`
<style> .header--color-black { color: #000; } .header__title--color-red { color: #f00; } </style> <div class="header header--color-black"> <h1 class="header__title"> <span class="header__title--color-red">Header</span> </h1> </div> [说明]
- Block 完全独立,可以嵌套,一个 header 是一个 Block,header 下的搜索框也可以是一个 Block
- 不可能出现
Block__Element-father__Element-son_Modifer这种类名的写法,BEM 只有三级 - Modifier 可以加在 Block 和 Element 上面
- Modifier 作为一个额外的类名加载 Block 和 Element 上面,只是为了改变状态,需要保留原本的 class
一个完整的示例
<form class="form form--theme-xmas form--simple"> <input class="form__input" type="text" /> <input class="form__submit form__submit--disabled" type="submit" /> </form> .form { } .form--theme-xmas { } .form--simple { } .form__input { } .form__submit { } .form__submit--disabled { } 参考链接:
BEM 解决了模块复用、全局命名冲突等问题,配合预处理 CSS 使用时,也能得到一定程度的扩展,但是它依然有它的问题:
- 对于嵌套过深的层次在命名上会给需要语义化体现的元素造成很大的困难
- 对于多人协作上,需要统一命名规范,这同样也会造成额外的 effort
CSS Modules
说了这么多,终于要到正文了
什么是 CSS Modules
根据 CSS Modules 的repo上的话来说是这样的:
CSS files in which all class names and animation names are scoped locally by default.
所以 CSS Modules 并不是一个正式的声明或者是浏览器的一个实现,而是通过构建工具( webpack or Browserify )来使所有的 class 达到 scope 的一个过程。
CSS Modules 解决了什么问题
- 全局命名冲突,因为 CSS Modules 只关心组件本身,只要保证组件本身命名不冲突,就不会有这样的问题,一个组件被编译之后的类名可能是这样的:
/* App.css */ .text { color: red; } /* 编译之后可能是这样的 */ .App__text___3lRY_ { color: red; } 命名唯一,因此保证了全局不会冲突。
- 模块化
可以使用composes来引入自身模块中的样式以及另一个模块的样式:
.serif-font { font-family: Georgia, serif; } .display { composes: serif-font; font-size: 30px; line-height: 35px; } 应用到元素上可以这样使用:
import type from "./type.css"; element.innerHTML = `<h1 class="${type.display}"> This is a heading </h1>`; 之后编译出来的模板可能是这样的:
<h1 class="Type__display__0980340 Type__serif__404840"> Heading title </h1> 从另一个模块中引入,可以这样写:
.element { composes: dark-red from "./colors.css"; font-size: 30px; line-height: 1.2; } - 解决嵌套层次过深的问题
因为 CSS Modules 只关注与组件本身,组件本身基本都可以使用扁平的类名来写,类似于这样的:
.root { composes: box from "shared/styles/layout.css"; border-style: dotted; border-color: green; } .text { composes: heading from "shared/styles/typography.css"; font-weight: 200; color: green; } CSS Modules 怎么用
CSS Modules 不局限于你使用哪个前端库,无论是 React、Vue 还是 Angular,只要你能使用构建工具进行编译打包就可以使用。
下面我使用webpack为例,一步一步引入 CSS Modules.
构建最初始的应用
. ├── build │ └── bundle.js ├── index.html ├── node_modules ├── package-lock.json ├── package.json ├── src │ ├── index.js │ └── styles └── webpack.config.js index.js 作为程序入口,styles 文件夹存放样式文件,配合 webpack.config.js 作为 webpack 配置文件。
// index.js var html = `<div class="header"> <h2 class="title">CSS Modules</h2> </div>` document.getElementById('container').innerHTML = html; 样式文件:
/* global.css */ * { margin: 0; padding: 0; } .container { padding: 20px; } /* index.css */ .header { font-size: 32px; } .title { border-bottom: 1px solid #ccc; padding-bottom: 20px; } 模板文件:
<!-- index.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>css modules</title> </head> <body> <div id="container" class="container"></div> <script src="./build/bundle.js"></script> </body> </html> 全局安装依赖,配置执行脚本:
npm install webpack webpack-cli --save-dev package.json
"scripts": { "build": "npx webpack && open index.html" } 在控制台执行npm run build, 得到的结果为:
> [email protected] build /Users/yhhu/Documents/coding/css-modules-demo > npx webpack && open index.html Hash: 5810d2ecd760c08cc078 Version: webpack 4.17.1 Time: 78ms Built at: 2018-08-26 15:09:31 Asset Size Chunks Chunk Names bundle.js 3.97 KiB main [emitted] main Entrypoint main = bundle.js [./src/index.js] 196 bytes {main} [built] 加入样式以及 loaders
package.json 中加入能够处理 css 的 loader
module: { rules: [ { test: /\.js/, loader: 'babel-loader', include: __dirname + '/src', exclude: __dirname + '/src/styles' }, { test: /\.css$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { } } ] } ] } index.js 中引入两个 CSS 文件
// index.js import './styles/global.css' import './styles/index.css' const html = `<div class="header"> <h2 class="title">CSS Modules</h2> </div>` document.getElementById('container').innerHTML = html; 编译之后的执行结果为:

在浏览器中显示为:

提取公有样式
可以看到打包之后的build目录下只有一个bundle.js,我们现在要把样式文件提取出来
./build/ └── bundle.js - 安装依赖
npm install --save-dev mini-css-extract-plugin - 修改
webpack.config.js
var MiniCssExtractPlugin = require("mini-css-extract-plugin"); modules: { rules: [ // { // test: /\.css$/, // use: [ // { loader: "style-loader" }, // { // loader: "css-loader", // options: { // } // } // ] // }, { test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: './build/styles' } }, { loader: "css-loader", options: { } } ] } ] }, plugins: [ new MiniCssExtractPlugin({ filename: "[name].css", chunkFilename: "[id].css" }) ], - 在模板中引入样式文件
<!-- index.html --> <!DOCTYPE html> <head> <link rel="stylesheet" href="./build/main.css"> </head> <body> <div id="container" class="container"></div> <script src="./build/bundle.js"></script> </body> - 编译打包

可以看到有main.css生成
开启 css modules 功能
默认在css-loader中是不开启css modules功能的,要开启可以设置modules: true即可,更多可以参看官方css-loader使用方法修改webpack.config.js,如下:
{ test: /\.css$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: './build/styles' } }, { loader: "css-loader", options: { modules: true } } ] } 修改index.js文件中的引用方式:
import './styles/global.css' import Index from './styles/index.css' const html = `<div class=${Index.header}> <h2 class=${Index.title}>CSS Modules</h2> </div>` document.getElementById('container').innerHTML = html; 可以看到,之前都是直接import一个css文件,而现在改成了导出一个对象的形式,我们可以把Index对象打印出来,看看具体是些什么东西:

直接对应我们引用的方式,然后我们再看看生成出来的main.css中具体有哪些东西:
* { margin: 0; padding: 0; } ._2BQ9qrIFipNbLIGEytIz5Q { padding: 20px; } ._3Ukt9LHwDhphmidalfey-S { font-size: 32px; } ._3XpLkKvmw0hNfJyl8yU3i4 { border-bottom: 1px solid #ccc; padding-bottom: 20px; } 合成一个文件之后,所有的类名都经过了哈希转换,因此确保了类名的唯一性,我们再看看浏览器中inspector中的样式应用,如下:

事实上,container样式我们是不需要转换的,因为我是把它固定写死在了容器上,那我们应该怎么做呢?
全局作用域
要想一个类名不需要被装换,那么可以使用:global(className)来进行包装,这样的类不会被转换,会被原样输出,下面我们修改global.css
/* global.css */ * { margin: 0; padding: 0; } :global(.container) { padding: 20px; } 我们再来看看main.css

就可以发现.container类没有被转换
定义哈希类名
CSS Modules 默认是以[hash:base64]来进行类名转换的,可辨识度不高,因此我们需要自定义
开启自定义,可以使用一个配置参数localIdentName,具体配置如下:
{ loader: "css-loader", options: { modules: true, localIdentName: '[path][name]__[local]--[hash:base64:5]' } } 
类名组合
如果我们实现类似于Sass的继承功能,我们需要怎么做呢? CSS Modules 中提供了composes关键字让我们来继承另外一个类,修改index.css如下:
.red { color: red; } .header { font-size: 32px; } .title { composes: red; border-bottom: 1px solid #ccc; padding-bottom: 20px; } 我们增加了一个red的类名,在title中实现继承,编译之后的结果为:

发现多了一个src-styles-index__red--1ihPk的类名,正是我们上面继承的那个类
除了在自身模块中继承,我们还可以继承其他文件中的 CSS 规则,具体如下:
我们再styles文件夹下新建一个color.css
/* color.css */ .red { color: red; } .blue { color: blue; } 然后在index.css文件中导入
/* index.css */ .red { color: red; } .header { font-size: 32px; } .title { color: green; composes: blue from './color.css'; composes: red; border-bottom: 1px solid #ccc; padding-bottom: 20px; } 最终我们会发现文字的颜色为绿色,可见自身模块声明优先级最高,如果把自身申明的color去掉,那么自身引入和从其他文件引入的相同申明又该如何显示呢?
答案是自身引入的声明的优先级会比较高。

总结
至此,所有的 CSS Modules 用法就已经介绍完毕了,至于后续的还有如何应用于React、Vue以及Angular中,相信掌握了上面的内容之后就可以知道怎么写了,如何与预处理器一起使用相信问题也不大。
