CSS Modules实战指南从入门到避坑我踩过的那些雷

UI琪航 前端 阅读 2,147
赞 9 收藏
二维码
手机扫码查看
反馈

React项目里CSS Modules配置踩坑记

今天在重构一个老项目的样式系统,准备把全局CSS改成CSS Modules,结果被各种配置搞得很头疼。本来以为就是改个文件后缀名的事儿,没想到折腾了一下午。

CSS Modules实战指南从入门到避坑我踩过的那些雷

首先是创建了一个新的React项目测试配置。之前都是直接用Create React App默认的CSS Modules支持,但这次是在一个Webpack自定义配置的项目里,就得自己配了。网上搜了一堆教程,大部分都是说在webpack.config.js里加loader,但每个人的配置都不一样,复制粘贴后总是报错。

核心配置就这几行

折腾了半天发现,其实关键配置没几行。这里是最终生效的webpack配置:

module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: {
                localIdentName: '[name]__[local]___[hash:base64:5]'
              }
            }
          }
        ]
      },
      {
        test: /.s[ac]ss$/i,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              modules: {
                localIdentName: '[name]__[local]___[hash:base64:5]'
              }
            }
          },
          'sass-loader'
        ]
      }
    ]
  }
}

这里的 localIdentName 配置很重要,它决定了生成的类名格式。默认的是那种乱七八糟的hash,调试起来很痛苦。我用了 [name]__[local]___[hash:base64:5] 这种格式,至少能看到是哪个文件的哪个类。

动态类名处理是个坑

配置好了之后,又遇到了另一个问题。原来的一些组件用了动态类名拼接,比如这样:

// 原来的写法
const className = btn ${type ? 'btn-' + type : ''} ${disabled ? 'disabled' : ''};

CSS Modules里这样写就挂了,因为btn这个类名会被转换成类似 Button__btn__abcde 这样的字符串。后来改成这样:

import styles from './Button.module.css';

const Button = ({ type, disabled }) => {
  const baseClass = styles.btn;
  const typeClass = type ? styles['btn-' + type] : '';
  const disabledClass = disabled ? styles.disabled : '';
  
  return (
    <button className={${baseClass} ${typeClass} ${disabledClass}}>
      {/* ... */}
    </button>
  );
};

这里我踩了个坑,就是CSS类名里如果有连字符,需要用方括号来访问,不能用点号。比如 btn-primary 要写成 styles['btn-primary'],而不是 styles.btn-primary

全局样式怎么处理

有些第三方库的样式需要全局生效,或者一些reset样式。CSS Modules默认会对所有CSS文件启用模块化,这就麻烦了。查了官方文档发现有个 :global 伪类:

/* Button.module.css */
.localClass {
  color: red;
}

:global(.global-class) {
  background: blue;
}

/* 也可以整个文件都是全局的 */
:global {
  .another-global-class {
    margin: 0;
  }
}

不过我后来发现了更好的方案,就是给文件名加上特殊后缀。如果文件名是 *.global.css 或者 *.module.css,可以配置不同规则:

{
  test: /.global.css$/,
  use: [
    'style-loader',
    'css-loader' // 不启用modules
  ]
},
{
  test: /^((?!.global).)*.css$/,
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        modules: {
          localIdentName: '[name]__[local]___[hash:base64:5]'
        }
      }
    }
  ]
}

这样命名规范一点,global.css 就是全局样式,Component.module.css 就是模块化样式。

配合Tailwind使用的注意事项

项目里还用了Tailwind,这里也有个小问题。CSS Modules和Tailwind的utility classes混用时要注意,不能直接在模板里写原生class名字:

// 错误写法
<div className="flex items-center justify-center bg-blue-500">
  {/* ... */}
</div>

// 正确写法 - 如果想用Tailwind utility,还是直接用
<div className={styles.container + " flex items-center justify-center bg-blue-500"}>
  {/* ... */}
</div>

其实这样挺混乱的,所以我一般的做法是:局部样式用CSS Modules,通用的utility classes还是用Tailwind,不冲突。只有一些组件特有的复杂样式才写到module文件里。

热更新和开发体验

配置完成后发现HMR(热模块替换)有问题,修改CSS模块文件后页面会闪一下而不是局部更新。查了资料发现需要确保style-loader也支持HMR,还好我用的版本默认支持,这个问题算是自动解决了。

调试的时候还有个问题,就是生成的类名太长了,Chrome DevTools里看起来很乱。不过习惯了也就好了,毕竟这是为了防止样式污染的代价。

构建优化考虑

生产环境打包时,这些hash后的类名会不会让CSS文件变大?理论上会稍微大一点点,但实际项目测试下来影响微乎其微。而且CSS压缩后,这些冗余信息会被很好地处理掉。

唯一需要注意的是,如果你有很多CSS文件,每个都会生成一份映射关系,可能会影响构建速度。不过现在机器性能都挺好的,这种影响基本可以忽略。

最后的总结

配置CSS Modules确实比直接用全局CSS复杂一些,特别是要处理动态类名的时候。但是避免样式冲突的好处还是很明显的,特别是团队协作的大型项目。

我现在的最佳实践是:组件内部样式用CSS Modules,公共utility类用Tailwind,第三方库样式单独引入。这样既能享受模块化的好处,也不会把配置搞得太复杂。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论