CSS Module呈现的背景
咱们晓得,Javascript倒退到当初呈现了泛滥模块化标准,比方AMD、CMD、 Common JS、ESModule等,这些模块化标准可能让咱们的JS实现作用域隔离。但CSS却并没有这么侥幸,倒退到当初却始终没有模块化标准,因为CSS是 依据选择器去全局匹配元素的,所以如果你在页面的两个不同的中央定义了一个雷同的类名,先定义的款式就会被后定义的笼罩掉。因为这个起因,CSS的命名抵触始终困扰着前端人员。
这种现状是前端开发者不能承受的,所以CSS社区也诞生了各种各样的CSS模块化解决方案(这并不是标准),比方:
- 命名办法: 人为约定命名规定
- scoped: vue中常见隔离形式
- CSS Module: 每个文件都是一个独立的模块
- CSS-in-JS: 这个常见于react、 JSX中
当初来看CSS Module
是目前最为风行的一种解决方案,它可能与CSS预处理器搭配应用在各种框架中。
如果这篇文章有帮忙到你,❤️关注+点赞❤️激励一下作者,文章公众号首发,关注 前端南玖
第一工夫获取最新文章~
CSS Module
CSS Module的风行源于React社区,它取得了社区的迅速采纳,前面因为Vue-cli对其集成后开箱即用的反对,将其推到了一个新高度。
部分作用域
在w3c 标准中,CSS 始终是「全局失效的」。在传统的 web 开发中,最为头痛的莫过于解决 CSS 问题。因为全局性,明明定义了款式,但就是不失效,起因可能是被其余款式定义所强制笼罩。
产生部分作用域的惟一办法就是为款式取一个举世无双的名字,CSS Module
也就是用这个办法来实现作用域隔离的。
在CSS Module中能够应用:local(className)
来申明一个部分作用域的CSS规定。
:local(.qd_btn) { border-radius: 8px; color: #fff;}:local(.qd_btn):nth(1) { color: pink;}:local(.qd_title) { font-size: 20px;}
CSS Module
会对:local()
蕴含的选择器做localIdentName
规定解决,也就是为其生成一个惟一的选择器名称,以达到作用域隔离的成果。
以上css通过编译后会生成这样的代码:
这里的:export
是CSS Module为解决导出而新增的伪类,前面再进行介绍
全局作用域
当然CSS Module也容许应用:global(className)
来申明一个全局作用域的规定。
:global(.qd_text) { color: chocolate;}
而对于:global()
蕴含的选择器CSS Module
则不会做任何解决,因为CSS规定默认就是全局的。
或者很多了会好奇咱们在开发过程如同很少应用到:local()
,比方在vue中,咱们只有在style标签上加上module
就能主动达到作用域隔离的成果。
是的,为了咱们开发过程不便,postcss-modules-local-by-default
插件曾经默认帮咱们解决了这一步,只有咱们开启了CSS模块化,外面的CSS在编译过程会默认加上:local()
。
Composing(组合)
组合的意思就是一个选择器能够继承另一个选择器的规定。
继承以后文件内容
:local(.qd_btn) { border-radius: 8px; color: #fff;}:local(.qd_title) { font-size: 20px; composes: qd_btn;}
继承其它文件
Composes 还能够继承内部文件中的款式
/* a.css */:local(.a_btn) { border: 1px solid salmon;}
/** default.css **/.qd_box { border: 1px solid #ccc; composes: a_btn from 'a.css'}
编译后会生成如下代码:
导入导出
从下面的这些编译后果咱们会发现有两个咱们平时没用过的伪类::import
、:export
。
CSS Module 外部通过ICSS
来解决CSS的导入导出问题,对应的就是下面两个新增的伪类。
Interoperable CSS (ICSS) 是规范 CSS 的超集。
:import
语句:import
容许从其余 CSS 文件导入变量。它执行以下操作:
- 获取并解决依赖项
- 依据导入的令牌解析依赖项的导出,并将它们匹配到
localAlias
- 在以后文件中的某些中央(如下所述)查找和替换应用
localAlias
依赖项的exportedValue
.
:export
一个:export
块定义了将要导出给消费者的符号。能够认为它在性能上等同于以下 JS:
module.exports = { "exportedKey": "exportedValue"}
语法上有以下限度:export
:
- 它必须在顶层,但能够在文件中的任何地位。
- 如果一个文件中有多个,则将键和值组合在一起并一起导出。
- 如果
exportedKey
反复某个特定项,则最初一个(按源程序)优先。 - An
exportedValue
能够蕴含对 CSS 申明值无效的任何字符(包含空格)。 exportedValue
不须要援用an ,它已被视为文字字符串。
以下是输入可读性所须要的,但不是强制的:
- 应该只有一个
:export
块 - 它应该位于文件的顶部,但在任何
:import
块之后
CSS Module原理
大略理解完CSS Module语法后,咱们能够再来看看它的外部实现,以及它的外围原理 —— 作用域隔离。
一般来讲,咱们平时在开发中应用起来没有这么麻烦,比方咱们在vue我的项目中可能做到开箱即用,最次要的插件就是css-loader
,咱们能够从这里动手一探到底。
这里大家能够思考下,css-loader
次要会依赖哪些库来进行解决?
咱们要晓得,CSS Module
新增的这些语法其实并不是CSS 内置语法,那么它就肯定须要进行编译解决
那么编译CSS咱们最先想到的是哪个库?
postcss对吧?它对于CSS就像Babel对于javascript
能够装置css-loader
来验证一下:
跟咱们预期的统一,这里咱们能看到几个以postcss-module
结尾的插件,这些应该就是实现CSS Module的外围插件。
从下面这些插件名称应该能看出哪个才是实现作用域隔离的吧
- Postcss-modules-extract-imports:导入导出性能
- Postcss-modules-local-by-default:默认部分作用域
- Postcss-modules-scope:作用域隔离
- Posts-modules-values:变量性能
编译流程
整个流程大体上跟Babel编译javascript相似:parse ——> transform ——> stringier
与Babel不同的是,PostCSS本身只包含css分析器,css节点树API,source map生成器以及css节点树拼接器。
css的组成单元是一条一条的款式规定(rule),每一条款式规定又蕴含一个或多个属性&值的定义。所以,PostCSS的执行过程是,先css分析器读取css字符内容,失去一个残缺的节点树,接下来,对该节点树进行一系列转换操作(基于节点树API的插件),最初,由css节点树拼接器将转换后的节点树从新组成css字符。期间可生成source map表明转换前后的字符对应关系。
CSS在编译期间也是须要生成AST得,这点与Babel解决JS一样。
AST
PostCSS的AST次要有以下这四种:
- rule: 选择器结尾
#main { border: 1px solid black;}
- atrule: 以
@
结尾
@media screen and (min-width: 480px) { body { background-color: lightgreen; }}
- decl: 具体款式规定
border: 1px solid black;
- comment: 正文
/* 正文*/
与Babel相似,这些咱们同样能够应用工具来更清晰地理解CSS 的 AST:
- Root: 继承自 Container。AST 的根节点,代表整个 css 文件
- AtRule: 继承自 Container。以 @ 结尾的语句,外围属性为 params,例如:
@import url('./default.css')
,params 为url('./default.css')
- Rule: 继承自 Container。带有申明的选择器,外围属性为 selector,例如:
.color2{}
,selector为.color2
- Comment: 继承自 Node。规范的正文/ 正文 / 节点包含一些通用属性:
- type:节点类型
- parent:父节点
- source:存储节点的资源信息,计算 sourcemap
- start:节点的起始地位
- end:节点的终止地位
- raws:存储节点的附加符号,分号、空格、正文等,在 stringify 过程中会拼接这些附加符号
装置体验
npm i postcss postcss-modules-extract-imports postcss-modules-local-by-default postcss-modules-scope postcss-selector-parser
这些插件的性能咱们都能够本人一一去体验,咱们先将这些次要的插件串联起来试一试成果,再来自行实现一个Postcss-modules-scope
插件
(async () => { const css = await getCode('./css/default.css') const pipeline = postcss([ postcssModulesLocalByDefault(), postcssModulesExtractImports(), postcssModulesScope() ]) const res = pipeline.process(css) console.log('【output】', res.css)})()
把这几个外围插件集成进来,咱们会发现,咱们的css中的款式不必再写:local
也能生成惟一hash名称了,并且也可能导入其它文件的款式了。这次要是依附postcss-modules-local-by-default
、postcss-modules-extract-imports
两个插件。
/* default.css */.qd_box { border: 1px solid #ccc; composes: a_btn from 'a.css'}.qd_header { display: flex; justify-content: center; align-items: center; width: 100%; composes: qd_box;}.qd_box { background: coral;}
编写插件
当初咱们就本人来实现一下相似postcss-modules-scope
的插件吧,其实原理很简略,就是遍历AST,为选择器生成一个惟一的名字,并将其与选择器的名称保护在exports
外面。
次要API
说到遍历AST,与Babel类似Post CSS也同样提供了很多API用于操作AST:
- walk: 遍历所有节点信息
- walkAtRules: 遍历所有
atrule
类型节点 - walkRules: 遍历所有
rule
类型节点 - walkComments: 遍历所有
comment
类型节点 - walkDecls: 遍历所有
decl
类型节点
(更多内容可在postcss文档上查看)
有了这些API咱们解决AST就十分不便了
插件格局
编写PostCSS插件与Babel相似,咱们只须要依照它的标准进行解决AST就行,至于它的编译以及指标代码生成咱们都不须要关怀。
const plugin = (options = {}) => { return { postcssPlugin: 'plugin name', Once(root) { // 每个文件都会调用一次,相似Babel的visitor } }}plugin.postcss = truemodule.exports = plugin
外围代码
const selectorParser = require("postcss-selector-parser");// 随机生成一个选择器名称const createScopedName = (name) => { const randomStr = Math.random().toString(16).slice(2); return `_${randomStr}__${name}`;}const plugin = (options = {}) => { return { postcssPlugin: 'css-module-plugin', Once(root, helpers) { const exports = {}; // 导出 scopedName function exportScopedName(name) { // css名称与其对应的作用域名城的映射 const scopedName = createScopedName(name); exports[name] = exports[name] || []; if (exports[name].indexOf(scopedName) < 0) { exports[name].push(scopedName); } return scopedName; } // 本地节点,也就是须要作用域隔离的节点:local() function localizeNode(node) { switch (node.type) { case "selector": node.nodes = node.map(localizeNode); return node; case "class": return selectorParser.className({ value: exportScopedName( node.value, node.raws && node.raws.value ? node.raws.value : null ), }); case "id": { return selectorParser.id({ value: exportScopedName( node.value, node.raws && node.raws.value ? node.raws.value : null ), }); } } } // 遍历节点 function traverseNode(node) { // console.log('【node】', node) if(options.module) { const selector = localizeNode(node.first, node.spaces); node.replaceWith(selector); return node } switch (node.type) { case "root": case "selector": { node.each(traverseNode); break; } // 选择器 case "id": case "class": exports[node.value] = [node.value]; break; // 伪元素 case "pseudo": if (node.value === ":local") { const selector = localizeNode(node.first, node.spaces); node.replaceWith(selector); return; }else if(node.value === ":global") { } } return node; } // 遍历所有rule类型节点 root.walkRules((rule) => { const parsedSelector = selectorParser().astSync(rule); rule.selector = traverseNode(parsedSelector.clone()).toString(); // 遍历所有decl类型节点 解决 composes rule.walkDecls(/composes|compose-with/i, (decl) => { const localNames = parsedSelector.nodes.map((node) => { return node.nodes[0].first.first.value; }) const classes = decl.value.split(/\s+/); classes.forEach((className) => { const global = /^global\(([^)]+)\)$/.exec(className); // console.log(exports, className, '-----') if (global) { localNames.forEach((exportedName) => { exports[exportedName].push(global[1]); }); } else if (Object.prototype.hasOwnProperty.call(exports, className)) { localNames.forEach((exportedName) => { exports[className].forEach((item) => { exports[exportedName].push(item); }); }); } else { console.log('error') } }); decl.remove(); }); }); // 解决 @keyframes root.walkAtRules(/keyframes$/i, (atRule) => { const localMatch = /^:local\((.*)\)$/.exec(atRule.params); if (localMatch) { atRule.params = exportScopedName(localMatch[1]); } }); // 生成 :export rule const exportedNames = Object.keys(exports); if (exportedNames.length > 0) { const exportRule = helpers.rule({ selector: ":export" }); exportedNames.forEach((exportedName) => exportRule.append({ prop: exportedName, value: exports[exportedName].join(" "), raws: { before: "\n " }, }) ); root.append(exportRule); } }, }}plugin.postcss = truemodule.exports = plugin
应用
(async () => { const css = await getCode('./css/index.css') const pipeline = postcss([ postcssModulesLocalByDefault(), postcssModulesExtractImports(), require('./plugins/css-module-plugin')() ]) const res = pipeline.process(css) console.log('【output】', res.css)})()
原文首发地址点这里,欢送大家关注公众号 「前端南玖」,如果你想进前端交换群一起学习,请点这里
我是南玖,咱们下期见!!!