CSS Module样式隔离
问题
前端模块化之后,各个模块往往独立开发,合并打包后存在许多同名样式,样式互相覆盖,带来严重的样式污染。
JS是怎么做的
JS中同样存在重名问题,同一个文件里的变量一般不会重名,但是不同文件里就很可能重名,因此webpack打包时会把不同文件打包到不同的闭包里。
源码
比如有两个js文件,message.js和notification.js,各自有一个show方法:
// message.js
const show = () => {
alert('message show')
}
// notification.js
const show = () => {
alert('notification show')
}
// index.js
import { show as mShow } from './message';
import { show as nShow} from './natification';
mShow()
nShow()
产物
用webpack以index.js为入口进行打包,查看打包产物,首先定义一个__webpack_modules__的变量,是一个对象,包含3个key分别是./src/index.js,./src/message.js和./src/natification.js,对应的value则是上面3个文件对应的代码
/******/ var __webpack_modules__ = ({
/***/ "./src/index.js":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _message__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./message */ \"./src/message.js\");\n/* harmony import */ var _natification__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./natification */ \"./src/natification.js\");\n\n\n\n(0,_message__WEBPACK_IMPORTED_MODULE_0__.show)()\n;(0,_natification__WEBPACK_IMPORTED_MODULE_1__.show)()\n\n//# sourceURL=webpack://js-demo/./src/index.js?");
/***/ }),
/***/ "./src/message.js":
/*!************************!*\
!*** ./src/message.js ***!
\************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ show: () => (/* binding */ show)\n/* harmony export */ });\nconst show = () => {\n\talert('message show')\n}\n\n//# sourceURL=webpack://js-demo/./src/message.js?");
/***/ }),
/***/ "./src/natification.js":
/*!*****************************!*\
!*** ./src/natification.js ***!
\*****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ show: () => (/* binding */ show)\n/* harmony export */ });\nconst show = () => {\n\talert('notification show')\n}\n\n//# sourceURL=webpack://js-demo/./src/natification.js?");
/***/ })
接下来定义了__webpack_require__方法, 并提供了缓存,如果已经引入过了,就优先读缓存:
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
如果没有引入过,就从上面定义的__webpack_modules__变量中获取,并存放到缓存里,其中__webpack_require__里执行的
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
正是__webpack_modules__里的函数的入参数
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// ... 这里省略
})
简化一下流程,伪代码是这样:
var _message__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__('src/message.js')
var _natification__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__('src/natification.js')
_message__WEBPACK_IMPORTED_MODULE_0__.show()
_natification__WEBPACK_IMPORTED_MODULE_1__.show()
我们看到,message.js和notification.js两个模块被分别引入到变量_message__WEBPACK_IMPORTED_MODULE_0__ 和 natification__WEBPACK_IMPORTED_MODULE_1_ 里,调用的时候加上前缀,实现了同名方法的隔离。
CSS Module
原生CSS
我们修改一下message和notification两个文件,分别引入css样式
js-demo
|- /src
|- index.jsx
|- message
|- index.jsx
|- index.css
|- natification
|- index.jsx
|- index.css
message是红色背景色
// index.jsx
import './index.css';
export const show = () => {
return <div className='body'>message show</div>
}
// index.css
.body {
background: red;
}
natification是蓝色背景色
// index.js
import './index.css';
export const show = () => {
return <div className='body'>natification show</div>
}
// index.css
.body {
background: blue;
}
因为两个css中的.body类同名,最终展示的颜色完全取决于index.jsx中两个文件导入的顺序,后倒入颜色覆盖前面的
import { show as mShow } from './message';
import { show as nShow} from './natification';
这种情况下是两个全蓝色,并不是预期的一红一蓝。
开启CSS Module
CSS Module 指的是所有的类名和动画名称默认都有各自作用域的CSS文件,是在构建步骤中对CSS类名和选择器限定作用域的一种方式。webpack构建中,可以在css-loader中开启module支持:
module: {
rules: [
{
test: /\.css$/i,
use: [{
loader: 'style-loader',
}, {
loader: 'css-loader',
options: {
modules: true,
},
}],
},
]
}
然后把index.css修改为in dex.module.css,同时修改引用方式为styles.body
import styles from'./index.module.css';
export const show = () => {
return <div className={styles.body}>message show -- red {styles.body}</div>
}
经过上面的修改后实现一红一蓝的效果。我们可以看到,styles.body本身等于一个字符串,是一个hash值。
产物
最终的大包产物里,css类名已经被替换成了hash值,通过hash值,有效避免了类名重复。
/*!************************************************************************************************************!*\
!*** css ./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[0].use[2]!./src/message/index.module.css ***!
\************************************************************************************************************/
.Yvq5I4p7EPEoZfFgN7z7 {
color: #fff;
width: 200px;
height: 200px;
background: red;
margin: 10px;
}
/*!*****************************************************************************************************************!*\
!*** css ./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[0].use[2]!./src/natification/index.module.css ***!
\*****************************************************************************************************************/
.hMIGfNGHuFjRZ_yQXKSb {
color: #fff;
width: 200px;
height: 200px;
background: blue;
margin: 10px;
}
从类名到hash
CSS转换AST
css的转换一般通过postcss把css转换成语法树,例如上面的代码转换后:
postcss和postcss-modules
我们可以通过postcss + postcss-modules来查看转换过程,首先安装postcss和postcss-modules
npm install --save-dev postcss postcss-modules
然后增加postcss.config.js的配置文件
module.exports = {
plugins: [
require('postcss-modules')({})
]
};
最后执行命令
npx postcss ./src/message/index.module.css -o output.css
执行完成后,查看目录结构
js-demo
|- /src
|- index.jsx
|- message
|- index.jsx
|- index.module.css
+ |- index.module.css.json
|- natification
|- index.jsx
|- index.module.css
+ |- output.css
查看这里新增的index.module.css.json文件
{"body":"_body_kmgaf_1"}
生成一个json文件维护了原本的className和生成的唯一class之间的对应关系。
postcss-modules-scope
css-loader内部也是通过postcss进行转换,并且内部依赖了postcss-modules-scope来实现:
"dependencies": {
"postcss-modules-extract-imports": "^3.0.0",
"postcss-modules-local-by-default": "^4.0.4",
"postcss-modules-scope": "^3.1.1",
"postcss-modules-values": "^4.0.0",
},
主要的转换逻辑在postcss-modules-scope里,通过walkRules对class,id等进行转换
const plugin = (options = {}) => {
return {
postcssPlugin: "postcss-modules-scope",
Once(root, { rule }) {
const exports = Object.create(null);
root.walkRules((rule) => {
let parsedSelector = selectorParser().astSync(rule);
rule.selector = traverseNode(parsedSelector.clone()).toString();
}
}
}
}
主要是找到里面的类名,id这些,重新赋值成hash值,然后输出exports。但是post-modules-scope需要声明:local
:local(.body) {
color: #fff;
width: 200px;
height: 200px;
background: red;
margin: 10px;
}
经过转换后得到
._Users_sunflower_Documents_TestSpace_js_demo_src_message_index_module__body {
color: #fff;
width: 200px;
height: 200px;
background: red;
margin: 10px;
}
:export {
body: _Users_sunflower_Documents_TestSpace_js_demo_src_message_index_module__body;
}
为了更方便的使用css-module,postcss-modules-local-by-default插件提供了默认转换成css-module的支持,即保持原来的.body的写法也能转换成css。