最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 【工程化】深入浅出 CSS Modules

    正文概述 掘金(Gopal)   2021-04-19   400

    CSS Modules 是什么?

    官方文档的介绍如下:

    所有的类名和动画名称默认都有各自的作用域的 CSS 文件。CSS Modules 并不是 CSS 官方的标准,也不是浏览器的特性,而是使用一些构建工具,比如 webpack,对 CSS 类名和选择器限定作用域的一种方式(类似命名空间)

    本文来介绍一下 CSS Modules 的简单使用,以及 CSS Modules 的实现原理(CSS-loader 中的实现)

    CSS Modules 的简单使用

    项目搭建以及配置

    新建一个项目,本文的 Demo

    npx create-react-app learn-css-modules-react
    cd 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;
    };
    

    我们就基于这个环境当做示例进行演示

    局部作用域

    之前的样式

    【工程化】深入浅出 CSS Modules

    首先,我们将 App.css 修改成 App.module.css,然后导入 css,并设置(这里有个小知识点,实际上 CSS Modules 推荐的命名是驼峰式,主要是这样的话,使用对象 style.className 就可以访问。如果是如下,就需要 styles['***-***'])

    import styles from './App.module.css';
    
    // ...
    <header className={styles['App-header']}></header>
    

    就会根据特定的规则生成相应的类名

    【工程化】深入浅出 CSS Modules

    这个命名规则是可以通过 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 Modules

    输入其他的模块

    不仅仅可以同一个文件中的,还可以继承其他文件中的 CSS 规则

    定义一个 another.module.css

    .font-blue {
      color: blue;
    }
    

    在 App.module.css 中

    .App-header {
      /* ... */
      composes: font-blue from './another.module.css';
      /* ... */
    }
    

    【工程化】深入浅出 CSS Modules

    使用变量

    我们还可以使用变量,定义一个 colors.module.css

    @value blue: #0c77f8;
    

    在 App.module.css 中

    @value colors: "./colors.module.css";
    @value blue from colors;
    
    .App-header {
      /* ... */
      color: blue;
    }
    

    【工程化】深入浅出 CSS Modules

    使用小结

    总体而言,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)
    ]);
    

    主要看 modulesValuesmodulesScope 方法,实际上这两个方法又是来自其他两个包

    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 的父类型,还是 ruledecl 的子类型
    • walkAtRules:遍历所有的 atRule
    • walkComments:遍历注释
    • walkDecls
    • walkRules

    作用域样式的实现

    // Find any :local classes
    // 找到所有的含有 :local 的 classes
    css.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 的父类型,还是 ruledecl 的子类型,获取 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 Name
    function 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 用法教程

    起源地下载网 » 【工程化】深入浅出 CSS Modules

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元