CSS Module的背景
众所周知,Javascript的发展已经出现了许多模块化规范,如AMD、CMD、 Common JS、ESModule等,这些模块化规范可以隔离我们的JS。但是CSS并没有那么幸运,但是到目前为止还没有模块化规范,因为CSS是 根据选择器对元素进行全局匹配,因此,如果您在页面的两个不同位置定义相同的类名,则先定义的样式将被后定义所覆盖。因此,CSS的命名冲突一直困扰着前端人员。
前端开发者无法接受这种情况,因此CSS社区也诞生了各种CSS模块化解决方案(这不是标准),例如:
- 命名方法: 命名规则人为约定
- scoped: vue中常见的隔离方法
- CSS Module: 每个文件都是独立的模块
- CSS-in-JS: 这在reacttt中很常见、 JSX中
现在来看CSS Module
它是目前最流行的解决方案之一,可以与CSS预处理器一起用于各种框架。
假如这篇文章对你有帮助,❤️关注+点赞❤️鼓励作者,文章微信官方账号首发,关注 前端南玖
第一次获得最新文章~
局部作用域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()
。
组合是指一个选择器可以继承另一个选择器的规则。
继承当前文件的内容: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的导入导出问题,对应上述两种新的伪类。
:importInteroperable CSS (ICSS) 是标准 CSS 的超集。
语句:import
允许从其他 CSS 文件导入变量。它执行以下操作:
- 获取和处理依赖项
- 根据导入的令牌分析依赖项的导出,并匹配它们
localAlias
- 搜索和替换当前文件中的某些地方(如下所述)
localAlias
依赖项的exportedValue
.
一个:export
块定义了将导出给消费者的符号。可以认为,它在功能上等同于以下几点 JS:
module.exports = {"exportedKey": "exportedValue"}
语法有以下限制:export
:
- 它必须在顶层,但可以在文件中的任何位置。
- 若一个文件中有多个,则将键与值组合并导出。
- 如果
exportedKey
重复一个特定项目,最后一个(按源顺序)优先。 - An
exportedValue
可以包含对 CSS 任何声明值有效的字符(包括空格)。 exportedValue
不需要引用an ,它已被视为文字字符串。
输出可读性需要以下内容,但不是强制性的:
- 应该只有一个
:export
块 - 它应该位于文件的顶部,但在任何地方
:import
块之后
对CSS有了大致的了解 在Module语法之后,我们可以看到它的内部实现和它的核心原理 —— 作用域隔离。
一般来说,我们在开发中使用它并不那么麻烦。例如,我们可以在vue项目中开箱即用。最重要的插件是css-loader
,我们可以从这里开始探究竟。
你可以在这里思考,css-loader
处理主要依靠哪些库?
我们必须知道,CSS Module
事实上,这些新语法不是CSS 内置语法,必须编译
那么CSS编译我们首先想到的是哪个库呢?
postcs对吗?对于CSS来说,就像babel对于javascriptts一样
可以安装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字符内容,得到一个完整的节点树。接下来,对节点树进行一系列转换操作(基于节点树API的插件)。最后,CSS节点树拼接器将转换后的节点树重新组成CSS字符。source可以在此期间生成 map显示转换前后的字符对应关系。
CSS还需要在编译过程中生成AST,就像Babel处理JS一样。
ASTPostCSSAST主要有以下四种:
- 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
里面。
说到遍历AST,Post与Babel类似 CSS还为操作AST提供了许多API:
- 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");// 随机生成选择器名称constt 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)})()
欢迎关注微信官方账号。 「前端南玖」,如果你想一起进入前端交流群学习,请点击这里
我是南九,下次见!!!
