·  阅读

CSS Modules 本身比较简单,还要写它的原因是我发现网上很多关于 CSS Modules 的教程或实践文章都已经过时,会存在某些使用方式现今已不可行的情况。可前往 css-modules-demo 查看本文中所有示例完整代码。

CSS Modules 比较简单,先介绍一下 CSS Modules 常用特性。 在此之前先做一个约定,以 .module.css 结尾的 css 文件表示 CSS Modules,其他 .css 文件仅视为普通的 css 模块,这也是很多项目中的约定。

局部/全局作用域

由于 CSS 的规则都是全局生效的,所以如果希望一个类名( .class )需要实现“局部作用域”的效果就必须保证类名是唯一的。

  • 使用类选择器语法( .className )或 :local 伪类将类声明为局部作用域类,类名将被编译成一个唯一的哈希类名(哈希字符串,如 _1wtd1y1DR22bj8P0JYV7nH )。值得注意的是,同一个局部类名被使用多次,编译后的类名都是相同的。
  • 使用 :global 伪类将声明为一个全局类名,编译后类名不会改变。
  • /* 局部作用域 */
    .title {
      color: red;
    /* 等效于:
    :local(.title) {
      color: red;
    /* 全局作用域 */
    :global(.title) {
      color: green;
    
    import React from 'react';
    import styles from "./scope.module.css";
    const Demo = () => {
      return (
          <h3 className={styles.title}>局部作用域</h3>
          <h3 className="title">全局作用域</h3>
        </div>
    export default Demo;
    

    自定义哈希类名

    默认算法生成的哈希类名虽然唯一但没有可读性(如 title 编译为 _3zyde4l1yATCOkgn-DBWEL),所以我们往往希望自定义哈希类名,有两种方式:

  • 设置 css-loaderoptions.modules.localIdentName 选项,值接受一个字符串模板。比如:
  • module.exports = {
      module: {
        rules: [
            test: /\.module\.css$/,
            use: [
              require.resolve('style-loader'),
                loader: require.resolve('css-loader'),
                options: {
                  // 开启 CSS Modules
                  modules: {
                    compileType: 'module',
                    localIdentName: "[path][name]__[local]--[hash:base64:5]",
    
  • 通过设置 css-loaderoptions.modules.getLocalIdent 选项,值传递一个自定义函数。
  • 这里使用 create-react-app 中的设置作为示例👇,getCSSModuleLocalIdent 函数是为了创建格式为 [filename]_[classname]__[hash] 唯一类名。比如 .content.module.css 中的 title 类名将编译为 content_title__2eyf6

    const loaderUtils = require('loader-utils');
    const path = require('path');
    function getCSSModuleLocalIdent (context, localIdentName, localName, options) {
      // 使用文件名或文件夹名
      const fileNameOrFolder = context.resourcePath.match(/index\.module\.(css|scss|sass)$/) ? '[folder]' : '[name]';
      // 根据文件位置和类名创建哈希
      const hash = loaderUtils.getHashDigest(
        path.posix.relative(context.rootContext, context.resourcePath) + localName,
        'md5',
        'base64',
      // 使用 loaderUtils 查找文件或文件夹名称
      const className = loaderUtils.interpolateName(
        context,
        fileNameOrFolder + '_' + localName + '__' + hash,
        options
      // 删除类名中的 `.module`,并替换所有 "." with "_"。
      return className.replace('.module_', '_').replace(/\./g, '_');
    module.exports = {
      module: {
        rules: [
            test: /\.module\.css$/,
            use: [
              require.resolve('style-loader'),
                loader: require.resolve('css-loader'),
                options: {
                  modules: {
                    compileType: 'module',
                    getLocalIdent: getCSSModuleLocalIdent
    

    CSS Modules 中使用 composes 实现样式复用。 composes 不仅可组合本模块类名,还可以组合其他 CSS Module 中导入的类名,均仅限在局部样式(:local)中使用:

    /* btn.module.css */
    .btn {
      border: 1px solid #ccc;
    /* 组合本模块中的类名👇 */
    .btnPrimary {
      composes: btn;
      background: #1890ff;
    /* 组合其他 CSS Module 中导入的类名👇 */
    .btnCircle {
      font-size: 14px;
      composes: circle from './shape.module.css';
      /* 要从多个模块导入,则使用多个 composes */
      composes: bgColor color from "./color.module.css";
      composes: btn;
    
    // Composes.js
    import React from 'react';
    import styles from "./btn.module.css";
    const Demo = () => {
      return (
          <button className={styles.btn}>Button</button>
          <button className={styles.btnPrimary}>Primary Button</button>
          <button className={styles.btnCircle}><Icon type="search"/></button>
        </div>
    export default Demo;
    

    className={styles.btnPrimary} 编译后的类名为 class="btn_btnPrimary__1RVFt btn_btn__1qiyw"

    :global 中使用 composes 将报错:

    /* Error: composition is only allowed when selector is single :local class name not in ".button", ".button" is weird */
    :global(.button) {
      composes: btn;
      color: red;
    

    variables.module.css

    @value primary: #BF4040;
    @value secondary: #1F4F7F;
    

    text.module.css

    @value fontSize: 16px;
    /* 从其他模块文件中导入👇 */
    @value primary as color-primary, secondary from "./variables.module.css";
    /* 等效于👇 */
    @value variables: "./variables.module.css";
    @value primary as color-primary, secondary from variables;
    /* 值作为选择器名称 */
    @value selectorValue: secondary-color;
    .selectorValue {
      color: secondary;
    .textPrimary {
      font-size: fontSize;
      color: color-primary;
    .textSecondary {
      font-size: fontSize;
      color: secondary;
    
    import React from 'react';
    import styles from "./text.module.css";
    const Demo = () => {
      return (
          变量:<br/>
          <p className={styles.textPrimary}>这是一段话...</p>
          <p className={styles.textSecondary}>这是一段话...</p>
          <p className={styles['secondary-color']}>这是一段话...</p>
        </div>
    export default Demo;
    

    导入、导出变量

    此特性是为了将变量从 CSS 传递给 JS。CSS Module 通过 Interoperable CSS (ICSS) 实现此特性,ICSS 作为 CSS Modules 的低级文件格式规范,只是在标准 CSS 中额外增加了两个的伪选择器 :import:export。也因此 ICSS 下不能使用上面👆介绍的 CSS Modules 特性。

    实际项目中,通常约定扩展名为 .module.css 为的文件需解析为 CSS Modules,而常规的 .css 文件解析为 ICSS。css-loader 中通过 options.modules.compileType 选项设置 CSS Modules 解析程度。

    test: /\.css$/, // 匹配css 模块 exclude: /\.module\.css$/, // 排除 `.module.css` 扩展文件 use: [ require.resolve('style-loader'), loader: require.resolve('css-loader'), options: { modules: { // compileType:控制编译程度 compileType: 'icss', // 仅开启 :import 和 :export test: /\.module\.css$/, // 匹配 CSS Modules use: [ require.resolve('style-loader'), loader: require.resolve('css-loader'), options: { modules: { compileType: 'module', // CSS Modules 所有特性

    如果设置为 false 值会提升性能,因为避免了 CSS Modules 特性的解析。

    :export:import

    /* 导入变量 */
    :import("path/to/dep.css") {
      localAlias: keyFromDep;
      /* ... */
    /* 导出变量 */
    :export {
      exportedKey: exportedValue;
    	/* ... */
    

    :export 相当于 cjs 中的 module.exports

    module.exports = {
    	exportedKey: exportedValue;
    

    推荐约定,但不强制:

  • :export:只有一个 :export块,位于文件的顶部,但在任何 :import 块之后。
  • :import:每个依赖项应该有一个导入;所有导入都应位于文件顶部;本地别名应以双下划线(__)为前缀。
  • 具体示例:

    ./dep.css

    :export {
      theme-color: #1890ff;
      header-height: 60px;
      header-name: abc-header;
      color-secondary: #666;
      screen-min: 768px;
      screen-max: 1200px;
    

    icss.css

    :import("./dep.css") {
      __themeColor: theme-color;
      __headerHeight: header-height;
      __headerName: header-name;
      __secondary: color-secondary;
      __screenMin: screen-min;
      __screenMax: screen-max;
    /* 导入的变量可用于任何选择器、任何值和媒体查询参数中 */
    /* 任何选择器 */
    .__headerName .logo {
      color: red;
      line-height: __headerHeight;
    /* 任何值 */
    .border-theme {
      border: 1px solid __themeColor;
    /* 媒体查询参数 */
    @media (min-width: __screenMin) and (max-width: __screenMax) {
      .__headerName {
        box-shadow: 0 4px 4px __secondary;
    /* 导出供 js 模块使用 */
    :export {
      headerHeight: __headerHeight;
      headerName: __headerName;
    

    👆 因为上面使用了导出的变量(__headerName__headerHeight),所以 :export 块需放在样式规则下方,否则会导致样式规则失效。

    import React from 'react';
    import styles from "./icss.css";
    const { headerHeight, headerName } = styles;
    const Demo = () => {
      return (
        <div className={`${headerName} border-theme`} style={{ height: headerHeight }}>
          <span className="logo">logo</span>
        </div>
    export default Demo;
    

    实际项目开发中都使用 webpack,使用 css-loader 解析使用 css-loader 解析 CSS Modules。本文中使用 webpack5 做示例演示,去这里可查看完整代码,package.jsonwebpack.config.js 配置如下👇,使用 npm run dev 命令启动本地服务。

    webpack 配置

    package.json

    "name": "css-modules-demo", "scripts": { "dev": "webpack serve --hot", "build": "webpack" "devDependencies": { "@babel/core": "^7.14.6", "@babel/preset-react": "^7.14.5", "babel-loader": "^8.2.2", "css-loader": "^5.2.6", "html-webpack-plugin": "^5.3.2", "loader-utils": "^2.0.0", "postcss-loader": "^6.1.1", "style-loader": "^3.0.0", "webpack": "^5.44.0", "webpack-cli": "^4.7.2", "webpack-dev-server": "^3.11.2" "dependencies": { "react": "^17.0.2", "react-dom": "^17.0.2" // ...

    项目中通常做如下约定:

  • .module.css 结尾的扩展文件表示 CSS Modules(解析所有 CSS Modules 特性)。
  • 其他 .css 文件仅视为常规的 css 模块(仅解析 ICSS 特性, 相比标准 CSS 规范仅额外支持 :import:export)。
  • import React from 'react';
    import styles from './Button.module.css'; // 导入 CSS Modules 样式文件
    import './another-stylesheet.css'; // 导入常规样式
    const Button = () => {
      // 作为 js 对象引用
      return <button className={styles.error}>Error Button</button>;
    

    这样约定的目的一方面是为了规范代码书写,另一方面避免所有样式文件 webpack 都要做 CSS Modules 解析,造成性能浪费。

    通过为所有未匹配到 *.module.scss 命名约定文件设置 compileType 选项,只允许使用可交互的 CSS 特性(即 ICSS 特性, 如 :import:export),而不使用其他的 CSS Module 特性。此处仅供参考,因为在 v4 之前,css-loader 默认将 ICSS 特性应用于所有文件。

    因此做如下 webpack 配置,其中设置 css-loadermodules 选项已启用 CSS Modules,modules.compileType 控制编译程度,有 moduleicss 可选。 css-loader 更多配置详解,请查看这里

    // webpack.config.js
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const path = require('path');
    const cssModuleRegex = /\.module\.css$/;
    module.exports = {
      mode: 'development',
      devServer: {
        contentBase: './dist',
        hot: true,
      module: {
        rules: [
            oneOf: [
                test: /\.(js|mjs|jsx|ts|tsx)$/,
                loader: require.resolve('babel-loader'),
                options: {
                  presets: ['@babel/preset-react']
              // 匹配普通 css 模块
                test: /\.css$/,
                exclude: /\.module\.css$/, // 排除以 `.module.css` 扩展名的文件
                use: [
                  require.resolve('style-loader'),
                    loader: require.resolve('css-loader'),
                    options: {
                      modules: {
                        // 控制编译程度
                        compileType: 'icss', // 仅开启 :import 和 :export
              // 匹配 CSS Modules
                test: /\.module\.css$/,
                use: [
                  require.resolve('style-loader'),
                    loader: require.resolve('css-loader'),
                    options: {
                      modules: {
                        compileType: 'module', // CSS Modules 所有特性
                        getLocalIdent: getCSSModuleLocalIdent, // 自定义哈希类名,上面👆介绍过
      plugins: [
        new HtmlWebpackPlugin({
          inject: true,
          template: 'index.html',
    

    sass/scss

    项目中通常都会使用像 less、sass 这些预处理器,这里就以 sass 为例。 同样约定仅扩展名为 .module.scss.module.sass 解析 CSS Modules 特性,其他 .scss.sass 扩展文件视为普通的 sass 模块,只做支持 ICSS 特性(:import:export)的解析。

    修改 webpack.config.js,增加两个匹配组,配置上主要区别在于 modules.compileType 选项:

    // 匹配常规的 sass 模块,仅支持 ICSS
      test: /\.(scss|sass)$/,
      exclude: /\.module\.(scss|sass)$/, // 排除 .module.scss 或 .module.sass 扩展文件
      use: [
        require.resolve('style-loader'),
          loader: require.resolve('css-loader'),
          options: {
            importLoaders: 3, // 3 => postcss-loader, resolve-url-loader, sass-loader
            modules: {
              compileType: 'icss',
          // 帮助 sass-loader 找到对应的 url 资源
          loader: require.resolve('resolve-url-loader'),
          options: {
            root: resolveApp('src'),
          loader: require.resolve('sass-loader'),
          options: {
            sourceMap: true, // 这里不可少
    // 支持 CSS Modules, 仅匹配 .module.scss 或 .module.sass 扩展文件
      test: /\.module\.(scss|sass)$/,
      use: [
        require.resolve('style-loader'),
          loader: require.resolve('css-loader'),
          options: {
            importLoaders: 3,
            modules: {
              compileType: 'module',
              getLocalIdent: getCSSModuleLocalIdent,
          loader: require.resolve('resolve-url-loader'),
          options: {
            root: resolveApp('src'),
          loader: require.resolve('sass-loader'),
          options: {
            sourceMap: true,
    // 下面示例中将使用 file-loader 处理图片
      loader: require.resolve('file-loader'),
      exclude: [/\.(js|jsx)$/, /\.html$/, /\.json$/],
      options: {
        name: 'static/media/[name].[hash:8].[ext]',
    
  • importLoaders:表示配置在 css-loader 之前的 loader,有几个可以去处理 @import 资源(如 @import 'a.css';)。上面的配置中 3 表示 @import 进来的资源可以经过 postcss-loaderresolve-url-loadersass-loader 处理。
  • resolve-url-loader:帮助 sass-loader 找到对应的 url 资源。Saas 没有提供 url 重写的功能,此 loader 设置于 loader 链中的 sass-loader 之前就可以重写 url。
  • 分别创建了一个 header.module.sassheader.sass 文件,内容均如下:

    $shadow-color: #ccc
    $header-height: 60px
    $theme: #1890ff
    .className
      border: 1px solid $theme
      padding: 20px
      box-shadow: 0 4px 4px $shadow-color
        background-color: #fff
        display: inline-block
    // 导出变量
    :export
      height: $header-height
    
    import React from 'react';
    import styles1 from "./header.module.sass";
    import styles2 from "./header.sass"; // ICSS
    import logo from './logo.png';
    console.log('styles1', styles1); // {height: "60px", className: "header_className__rUUph"}
    console.log('styles2', styles2); // {height: "60px"}
    const Logo = ({ height }) => (
      <img style={{ height, width: height }} src={logo} />
    const Demo = () => {
      return (
        <div className={styles1.className}>
          <Logo height={styles1.height} />
        </div>
    export default Demo;
    

    classnames

    匹配 classnames 使用更加方便:

    import classNames from 'classnames';
    import styles from './dialog.module.css';
    const Dialog = ({ disabled }) => {
      const cx = classNames({
        [styles.confirm]: !disabled,
        [styles.disabledConfirm]: disabled
      return (
        <div className={styles.root}>
          <a className={cx}>Confirm</a>
        </div>
    
  • css-loader#modules
  • CSS Modules
  • Exporting values variables
  • CSS Modules ICSS
  • Create React App
  • 分类:
    前端
    • 1154
  •