什么是CSS模块?官方文档的介绍是这样的:ACSSModules是一个CSS文件,默认情况下,所有类名和动画名都在本地作用域。默认情况下,所有类名和动画名称都有自己的范围CSS文件。CSSModules不是CSS的官方标准,也不是浏览器的特性,而是使用一些构建工具,比如webpack,来限制CSS类名和选择器(类似于命名空间)的范围。本文介绍CSSModulesCSSModules的简单使用,以及CSSModules的实现原理(在CSS-loader中实现)CSSModules的简单使用项目搭建与配置新建项目,Demonpxcreate-react-applearn-css-modules-reactcdlearn-css-本文中modules-react#显示webpack的配置。yarneject看到config/webpack.config.js。默认情况下,只有.module.css支持React脚手架构建的项目的模块化。如果自己构建,可以支持.css文件后缀等//添加对CSSModules的支持(https://github.com/css-modules/css-modules)//使用扩展名.module.css{test:cssModuleRegex,use:getStyleLoaders({importLoaders:1,sourceMap:isEnvProduction?shouldUseSourceMap:isEnvDevelopment,modules:{getLocalIdent:getCSSModuleLocalIdent,},}),}其中getStyleLoaders函数,可以看css-loaderconstgetStyleLoaders的配置=(cssOptions,preProcessor)=>{constloaders=[//...{loader:require.resolve('css-loader'),选项:cssOptions,},//...];//...返回负载rs;};我们就以这个环境为例,演示localscope之前的样式首先,我们将App.css修改为App.module.css,然后导入css,并设置(这里是一个小知识点,其实CSSModules的命名建议是驼峰命名,主要是这种情况,可以使用对象style.className来访问如果如下,则需要styles['***-***'])importstylesfrom'./App.module.css';//...会根据特定的规则生成对应的类名。这个命名规则可以通过css-loader来配置,类似如下配置:=[path][name]---[local]---[hash:base64:5]"},]}globalscope接下来我们发现在cssmodules中定义的类名必须设置为HTML中类似设置变量的方法(如上例所示),那我可以像其他CSS文件一样直接使用类名(也就是普通的设置方法),而不是编译后的hash字符串吗?使用:global语法,可以声明一个全局规则:global(.App-link){color:#61dafb;}这样就可以直接在HTML中使用和普通CSS一样了,不过感觉是开发用的CSSModules而已是作者留下的一个后门。像我们这样的css直接放在普通的.css文件中比较好。我明白这就是为什么React以不同的方式对待.css和.module.css的不同后缀。Class的组合在CSSModules中,一个选择器可以继承另一个选择器的规则,称为“组合”。比如我们定义一个font-red,然后在.App-header中使用composes:font-red;继承.font-red{color:red;}.App-header{composes:font-red;/*...*/}不仅在同一个文件中输入其他模块,还从其他文件继承CSS规则定义了一个another.module.css.font-blue{color:blue;}inApp.module.css.App-header{/*...*/composes:font-bluefrom'./another.module.css';/*...*/}使用变量我们也可以使用变量,定义一个colors.module.css@valueblue:#0c77f8;@valuecolorsinApp.module.css:"./colors.module.css";@valuebluefromcolors;.App-header{/*...*/color:blue;}使用总结Overall,CSSModules易于使用且上手非常快。接下来看看Webpack中的CSS-loader是如何实现CSSModules的CSSModules实现原理从CSSLoader开始,看lib/processCss.jsvarpipeline=postcss([...modulesValues,modulesScope({//生成具体的根据规则命名generateScopedName:function(exportName){returngetLocalIdent(options.loaderContext,localIdentName,exportName,{regExp:localIdentRegExp,hashPrefix:query.hashPrefix||"",context:context});}}),parserPlugin(解析器选项)]);主要看modulesValues和modulesScope方法,其实这两个方法来自另外两个包varmodulesScope=require("postcss-modules-scope");varmodulesValues=require("postcss-modules-values");postcss-modules-scope这个包主要实现了CSSModules的样式隔离(ScopeLocal)和继承(Extend)。它的代码比较简单,基本上一个文件就完成了,源码可以看这里,这里会使用postcss来处理AST相关的,我们大致了解一下它的思路,默认的命名规则其实如果你不设置任何规则,你会根据下面的命名方法//生成Scopedname(不通过导入时的默认规则)processor.generateScopedName=function(exportedName,path){varsanitisedPath=path.replace(/\.[^\.\/\\]+$/,'').replace(/[\W_]+/g,'_').replace(/^_|_$/g,'');返回'??_'+sanitisedPath+'__'+exportedName;};这种写法在很多源码中都有使用可以看到,也可以使用varprocessor=_postcss2['default'].plugin('postcss-modules-scope',function(options){//...returnfunction(css){//如果有输入,使用传入的命名规则//否则,使用默认定义的processor.generateScopedNamevargenerateScopedName=options&&options.generateScopedName||processor.generateScopedName;}//...})前置知识——postcss遍历样式方法cssast主要有3个父类型AtRule:@xxx类型,比如@screen,因为下面会提到变量的使用@valueComment:注解规则:有几种常见的css规则更重要的subtype:decl:指的是每个具体的cssrulerule:as一个selector上不同类型的css规则集用于不同的遍历walk:遍历所有节点信息,无论是atRule的父类型,rule,comment,还是rule的子类型,declwalkAtRules:遍历所有atRulewalkComments:遍历实现注释walkDeclswalkRules范围样式的//查找任何:localclasses//查找所有包含的类:localcss.walkRules(function(rule){varselector=_cssSelectorTokenizer2['default'].parse(rule.selector);//获取selectorvarnewSelector=traverseNode(selector);rule.selector=_cssSelectorTokenizer2['default'].stringify(newSelector);//遍历每条规则,如果匹配则将类名等转换为作用域名rule.walkDecls(function(decl){vartokens=decl.value.split(/(,|'[^']*'|"[^"]*")/);tokens=tokens.map(function(token,idx){如果(idx===0||tokens[idx-1]===','){varlocalMatch=/^(\s*):local\s*\((.+?)\)/.exec(token);if(localMatch){//获取作用域名称returnlocalMatch[1]+exportScopedName(localMatch[2])+token.substr(localMatch[0].length);}else{返回令牌;}}else{返回令牌;}});decl.value=tokens.join('');});});css.walkRules遍历所有节点信息,无论是atRule的父类型,rule,comment,还是rule的子类型,decl,Getselector//递归遍历节点找到目标节点functiontraverseNode(node){switch(node.type){case'nested-pseudo-class':if(node.name==='local'){if(node.nodes.length!==1){thrownewError('意外的逗号(",")在:本地块');}返回localizeNode(node.nodes[0]);}/*通过*/case'selectors':case'selector':varnewNode=Object.create(node);newNode.nodes=node.nodes.map(traverseNode);返回新节点;}returnnode;}walkDecls遍历每条规则并生成对应的ScopedName//生成一个ScopedNamefunctionexportScopedName(name){varscopedName=generateScopedName(name,css.source.input.from,css.source.input.css);导出[名称]=导出[名称]||[];如果(exports[name].indexOf(scopedName)<0){exports[name].push(scopedName);}returnscopedName;}关于实现composes的组合语法有些类似,就不赘述了。postcss-modules-values库的主要功能是在模块文件之间传递任意值,主要实现CSSModules中变量的使用。它的实现也只有一个文件。勾选这里查看所有@value语句,并将它们视为局部变量或导入,最后将它们保存在定义对象中/*查看所有@value语句并将它们视为局部变量或导入*///查看所有css.walkAtRules('value',atRule=>{//类似下面//@valueprimary,secondaryfromcolorsif(matchImports.exec(atRule.params)){addImport(atRule)}else{//处理定义该文件类似于以下内容//@valueprimary:#BF4040;//@valuesecondary:#1F4F7F;if(atRule.params.indexOf('@value')!==-1){result.warn('Invalidvaluedefinition:'+atRule.params)}addDefinition(atRule)}})如果是import,调用addImport方法constaddImport=atRule=>{//如果有import语法letmatches=matchImports.exec(atRule.params)if(matches){let[/*match*/,aliases,path]=matches//我们可以使用常量作为路径名if(definitions[path])path=definitions[path]letimports=aliases.replace(/^\(\s*([\s\S]+)\s*\)$/,'$1').split(/\s*,\s*/).map(alias=>{lettokens=matchImport.exec(alias)if(tokens){let[/*match*/,theirName,myName=theirName]=tokensletimportedName=createImportedName(myName)definitions[myName]=importedNamereturn{theirName,importedName}}else{thrownewError(`@importstatement"${alias}"isinvalid!`)}})//最后会根据import.remove()的语法}}否则直接addDefinition。我大致的理解是两种思路,就是找到对应的变量,然后替换//adddefinitionconstaddDefinition=atRule=>{letmatcheswhile(matches=matchValueDefinition.exec(atRule.params)){let[/*match*/,key,value]=matches//添加到definitions中,知道values可以互相引用definitions[key]=replaceAll(definitions,value)atRule.remove()}}CSSModules总结不是官方的CSS的标准,也不是浏览器的特性,而是利用一些构建工具,比如webpack,对CSS类名进行分类一种用选择器(类似于命名空间)限制作用域的方式通过CSSModules,我们可以实现CSS局部作用域、Class组合等功能。最后我们知道CSSLoader其实是通过两个库来实现的。其中,postcss-modules-scope——实现CSSModules风格的隔离(ScopeLocal)和继承(Extend)和postcss-modules-values——实现模块文件之间的任意值传递。参考开发postcss插件CSSModulesCSSModules使用教程