CSS Modules 是什么?
官网文档的介绍如下:
A CSS Modules is a CSS file in which all class names and animation names are scoped locally by default.
所有的类名和动画名称默认都有各自的作用域的 CSS
文件。CSS Modules 并不是 CSS 官网的规范,也不是浏览器的个性,而是应用一些构建工具,比方 webpack
,对 CSS
类名和选择器限定作用域的一种形式(相似命名空间)
本文来介绍一下 CSS Modules
的简略应用,以及 CSS Modules
的实现原理(CSS-loader 中的实现)
CSS Modules 的简略应用
我的项目搭建以及配置
新建一个我的项目,本文的 Demo
npx create-react-app learn-css-modules-reactcd learn-css-modules-react# 显示 webpack 的配置yarn eject
看到 config/webpack.config.js
,默认状况下,React 脚手架搭建进去的我的项目,只有 .module.css
反对模块化,如果是本人搭建的话,能够反对 .css 文件的后缀等
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)// using the extension .module.css{ test: cssModuleRegex, use: getStyleLoaders({ importLoaders: 1, sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment, modules: { getLocalIdent: getCSSModuleLocalIdent, }, }),}
其中 getStyleLoaders 函数,能够看到 css-loader 的配置
const getStyleLoaders = (cssOptions, preProcessor) => { const loaders = [ // ... { loader: require.resolve('css-loader'), options: cssOptions, }, // ... ]; // ... return loaders;};
咱们就基于这个环境当做示例进行演示
部分作用域
之前的款式
首先,咱们将 App.css
批改成 App.module.css
,而后导入 css,并设置(这里有个小知识点,实际上 CSS Modules 举荐的命名是驼峰式,次要是这样的话,应用对象 style.className
就能够拜访。如果是如下,就须要 styles['***-***'])
import styles from './App.module.css';// ...<header className={styles['App-header']}></header>
就会依据特定的规定生成相应的类名
这个命名规定是能够通过 CSS-loader 进行配置,相似如下的配置:
module: { loaders: [ // ... { test: /\.css$/, loader: "style-loader!css-loader?modules&localIdentName=[path][name]---[local]---[hash:base64:5]" }, ]}
全局作用域
默认状况下,咱们发现,在 css modules 中定义的类名必须得通过相似设置变量的形式给 HTML
设置(如上示例所示)
那么我能像其余的 CSS
文件一样间接应用类名(也就是一般的设置办法),而不是编译后的哈希字符串么?
应用 :global
的语法,就能够申明一个全局的规定
:global(.App-link) { color: #61dafb;}
这样就能够在 HTML 中间接跟应用一般的 CSS 一样了
但这里感觉就是 CSS Modules 给开发者留的一个后门,咱们这样的 CSS,还是间接放在一般 .css 文件中比拟好,我了解这就是 React 为什么看待 .css 和 .module.css 不同后缀进行不同的解决的起因
Class 的组合
在 CSS Modules 中,一个选择器能够继承另一个选择器的规定,这称为 "组合"("composition")
比方,咱们定义一个 font-red,而后在 .App-header 中应用 composes: font-red;
继承
.font-red { color: red;}.App-header { composes: font-red; /* ... */}
输出其余的模块
不仅仅能够同一个文件中的,还能够继承其余文件中的 CSS 规定
定义一个 another.module.css
.font-blue { color: blue;}
在 App.module.css 中
.App-header { /* ... */ composes: font-blue from './another.module.css'; /* ... */}
应用变量
咱们还能够应用变量,定义一个 colors.module.css
@value blue: #0c77f8;
在 App.module.css 中
@value colors: "./colors.module.css";@value blue from colors;.App-header { /* ... */ color: blue;}
应用小结
总体而言,CSS Modules 的应用偏简略,上手十分的快,接下来咱们看看 Webpack 中 CSS-loader
是怎么实现 CSS Modules
的
CSS Modules 的实现原理
从 CSS Loader 开始讲起
看 lib/processCss.js
中
var pipeline = postcss([ ... modulesValues, modulesScope({ // 依据规定生成特定的名字 generateScopedName: function(exportName) { return getLocalIdent(options.loaderContext, localIdentName, exportName, { regExp: localIdentRegExp, hashPrefix: query.hashPrefix || "", context: context }); } }), parserPlugin(parserOptions)]);
次要看 modulesValues
和 modulesScope
办法,实际上这两个办法又是来自其余两个包
var modulesScope = require("postcss-modules-scope");var modulesValues = require("postcss-modules-values");
postcss-modules-scope
这个包次要是实现了 CSS Modules 的款式隔离(Scope Local)以及继承(Extend)
它的代码比较简单,根本一个文件实现,源码能够看这里,这里会用到 postcss 解决 AST 相干,咱们大抵理解它的思维即可
默认的命名规定
实际上,如果你没有设置任何的规定时候会依据如下进行命名
// 生成 Scoped name 的办法(没有传入的时候的默认规定)processor.generateScopedName = function (exportedName, path) { var sanitisedPath = path.replace(/\.[^\.\/\\]+$/, '').replace(/[\W_]+/g, '_').replace(/^_|_$/g, ''); return '_' + sanitisedPath + '__' + exportedName;};
这种写法在很多的源码中咱们都能够看到,当前写代码的时候也能够采纳
var processor = _postcss2['default'].plugin('postcss-modules-scope', function (options) { // ... return function (css) { // 如果有传入,则采纳传入的命名规定 // 否则,采纳默认定义的 processor.generateScopedName var generateScopedName = options && options.generateScopedName || processor.generateScopedName; } // ...})
前置常识—— postcss 遍历款式的办法
css ast
次要有 3 种父类型
- AtRule: @xxx 的这种类型,如 @screen,因为上面会提到变量的应用
@value
- Comment: 正文
- Rule: 一般的 css 规定
还有几个个比拟重要的子类型:
- decl: 指的是每条具体的 css 规定
- rule:作用于某个选择器上的 css 规定汇合
不同的类型进行不同的遍历
- walk: 遍历所有节点信息,无论是 atRule、rule、comment 的父类型,还是
rule
、decl
的子类型 - walkAtRules:遍历所有的 atRule
- walkComments:遍历正文
- walkDecls
- walkRules
作用域款式的实现
// Find any :local classes// 找到所有的含有 :local 的 classescss.walkRules(function (rule) { var selector = _cssSelectorTokenizer2['default'].parse(rule.selector); // 获取 selector var newSelector = traverseNode(selector); rule.selector = _cssSelectorTokenizer2['default'].stringify(newSelector); // 遍历每一条规定,如果匹配到则将类名等转换成作用域名称 rule.walkDecls(function (decl) { var tokens = decl.value.split(/(,|'[^']*'|"[^"]*")/); tokens = tokens.map(function (token, idx) { if (idx === 0 || tokens[idx - 1] === ',') { var localMatch = /^(\s*):local\s*\((.+?)\)/.exec(token); if (localMatch) { // 获取作用域名称 return localMatch[1] + exportScopedName(localMatch[2]) + token.substr(localMatch[0].length); } else { return token; } } else { return token; } }); decl.value = tokens.join(''); });});
css.walkRules 遍历所有节点信息,无论是 atRule、rule、comment 的父类型,还是 rule
、 decl
的子类型,获取 selector
// 递归遍历节点,找到指标节点function traverseNode(node) { switch (node.type) { case 'nested-pseudo-class': if (node.name === 'local') { if (node.nodes.length !== 1) { throw new Error('Unexpected comma (",") in :local block'); } return localizeNode(node.nodes[0]); } /* falls through */ case 'selectors': case 'selector': var newNode = Object.create(node); newNode.nodes = node.nodes.map(traverseNode); return newNode; } return node;}
walkDecls
遍历每一条规定,生成相应的 Scoped Name
// 生成一个 Scoped Namefunction exportScopedName(name) { var scopedName = generateScopedName(name, css.source.input.from, css.source.input.css); exports[name] = exports[name] || []; if (exports[name].indexOf(scopedName) < 0) { exports[name].push(scopedName); } return scopedName;}
对于实现 composes 的组合语法,有点相似,不再赘述
postcss-modules-values
这个库的次要作用是在模块文件之间传递任意值,次要是为了实现在 CSS Modules 中可能应用变量
它的实现也是只有一个文件,具体查看这里
查看所有的 @value
语句,并将它们视为局部变量或导入的,最初保留到 definitions
对象中
/* Look at all the @value statements and treat them as locals or as imports */// 查看所有的 @value 语句,并将它们视为局部变量还是导入的css.walkAtRules('value', atRule => { // 相似如下的写法 // @value primary, secondary from colors if (matchImports.exec(atRule.params)) { addImport(atRule) } else { // 解决定义在文件中的 相似如下 // @value primary: #BF4040; // @value secondary: #1F4F7F; if (atRule.params.indexOf('@value') !== -1) { result.warn('Invalid value definition: ' + atRule.params) } addDefinition(atRule) }})
如果是导入的,调用的 addImport 办法
const addImport = atRule => { // 如果有 import 的语法 let matches = matchImports.exec(atRule.params) if (matches) { let [/*match*/, aliases, path] = matches // We can use constants for path names if (definitions[path]) path = definitions[path] let imports = aliases.replace(/^\(\s*([\s\S]+)\s*\)$/, '$1').split(/\s*,\s*/).map(alias => { let tokens = matchImport.exec(alias) if (tokens) { let [/*match*/, theirName, myName = theirName] = tokens let importedName = createImportedName(myName) definitions[myName] = importedName return { theirName, importedName } } else { throw new Error(`@import statement "${alias}" is invalid!`) } }) // 最初会依据这个生成 import 的语法 importAliases.push({ path, imports }) atRule.remove() }}
否则则间接 addDefinition,两个的思维大抵我了解都是找到响应的变量,而后替换
// 增加定义const addDefinition = atRule => { let matches while (matches = matchValueDefinition.exec(atRule.params)) { let [/*match*/, key, value] = matches // Add to the definitions, knowing that values can refer to each other definitions[key] = replaceAll(definitions, value) atRule.remove() }}
总结
CSS Modules 并不是 CSS 官网的规范,也不是浏览器的个性,而是应用一些构建工具,比方 webpack,对 CSS 类名和选择器限定作用域的一种形式(相似命名空间)。通过 CSS Modules,咱们能够实现 CSS 的部分作用域,Class 的组合等性能。最初咱们晓得 CSS Loader 实际上是通过两个库进行实现的。其中, postcss-modules-scope
—— 实现CSS Modules 的款式隔离(Scope Local)以及继承(Extend)和 postcss-modules-values
——在模块文件之间传递任意值
参考
- 开发 postcss 插件
- CSS Modules
- CSS Modules 用法教程