我们知道,Javascript发展到现在出现了众多模块化规范,比如AMD、CMD、 Common JS、ESModule等,这些模块化规范能够让我们的JS实现作用域隔离。
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 : 8 px ;
color : #fff ;
}
:local (.qd_btn ):nth (1 ) {
color : pink ;
}
:local (.qd_title ) {
font - size : 20 px ;
}
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 : 8 px ;
color : #fff ;
}
:local (.qd_title ) {
font - size : 20 px ;
composes : qd_btn ;
}
继承其它文件 Composes 还可以继承外部文件中的样式
复制 /* a.css */
:local (.a_btn ) {
border : 1 px solid salmon ;
}
复制 /** default.css **/
.qd_box {
border : 1 px 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重复某个特定项,则最后一个(按源顺序)优先。 AnexportedValue可以包含对 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主要有以下这四种:
复制 #main {
border : 1 px solid black ;
}
复制 @ media screen and (min - width : 480 px ) {
body {
background - color : lightgreen ;
}
}
复制 border : 1 px solid black ;
与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 )
})()
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 把这几个核心插件集成进来,我们会发现,我们的css中的样式不用再写:local也能生成唯一hash名称了,并且也能够导入其它文件的样式了。这主要是依靠postcss-modules-local-by-default、postcss-modules-extract-imports两个插件。
复制 /* default.css */
.qd_box {
border : 1 px 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 ;
}
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15.
编写插件 现在我们就自己来实现一下类似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 = true
module .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 = true
module .exports = plugin
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86. 87. 88. 89. 90. 91. 92. 93. 94. 95. 96. 97. 98. 99. 100. 101. 102. 103. 104. 105. 106. 107. 108. 109. 110. 111. 112. 113. 114. 115. 116. 117. 118. 119. 120. 121. 122. 123. 124. 125. 126. 127. 128. 129. 130. 131. 132. 133. 134. 135. 136. 137. 138. 使用 复制 (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 )
})()