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。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值