我踩爆了 CSS Module 的所有坑,别再被骗了

我不是第一次用 CSS Module,但这次是真的被坑麻了。

写这篇不是为了教你怎么用,而是想提醒一下------别轻信"模块化样式管理更高级"这种话,现实比你想象得复杂多了。


起因:领导说"咱们不要用 styled-components 了,改回 CSS Module 吧,更轻"

我说行啊,确实,现在构建性能很重要,CSS-in-JS 太吃性能,react-native-style 写着也太烦了。

于是我把原来的 styled 全部替换成了 .module.scss,然后开始写:

jsx 复制代码
import styles from './index.module.scss';

<div className={styles.container}>Hello</div>

写起来还行,但没两天我就开始疯狂打 console.log(styles),调样式名调得像在 debug 变量。

因为我发现很多类名"根本没生效"。


坑一:你写的类名压根没进 styles 对象里

这个最坑。你 .module.scss 里定义了这个:

scss 复制代码
.button {
  color: red;
}

你以为 styles.button 一定有?不一定。

你只要哪个类名不被用上,某些构建工具(特别是用了 tree shaking 的,比如 Vite + PostCSS + purgeCSS)会直接删掉它。

控制台一 log:

js 复制代码
console.log(styles); // {}

你还以为是不是 import 路径写错了,其实是你 class 根本被构建优化干掉了。


坑二:你不能写动态 className(或者你得写得像谜语)

你想写个 toggle 状态,最正常写法是:

jsx 复制代码
<div className={active ? 'btnActive' : 'btn'} />

不好意思,这样写等于白写,因为 CSS Module 根本识别不了你动态拼接的字符串。

只能这么写:

jsx 复制代码
<div className={active ? styles.btnActive : styles.btn} />

甚至有时候你用了 classnames 库,还得用花式写法:

jsx 复制代码
classnames({
  [styles.btn]: true,
  [styles.btnActive]: active
})

你看代码就像在写谜语,搞得跟写魔法公式一样


坑三:全局样式没法用,但你不得不用

CSS Module 最大的卖点是"模块化隔离作用域",说白了每个类名都编译成:

css 复制代码
.button__x1j2z

但有些场景你真得用全局样式,比如:

  • 和第三方组件对接(antd、element-plus)
  • 页面背景统一样式 .dark-mode
  • 通用布局:.flex, .row, .mt20

你会发现自己不断加:

scss 复制代码
:global {
  .flex {
    display: flex;
  }
}

或者你开始分文件:

scss 复制代码
// index.module.scss
// common.scss(非模块化)

结果就变成了"部分模块化 + 部分全局 + 一堆命名重复的类名",代码风格反而更混乱。


坑四:你写了两个模块,样式名居然冲突了(?)

你以为 CSS Module 会自动帮你做唯一命名哈?是的,但不是所有构建工具都配置好了 hash。

比如我当时 vite + css modules,默认生成的 class 是:

css 复制代码
.btn__abc

我在两个文件都写了 .btn,结果打包后都叫 .btn__abc,最后样式覆盖了......

一查是因为 vite.config.ts 里没开:

ts 复制代码
css: {
  modules: {
    generateScopedName: '[name]__[local]___[hash:base64:5]',
  }
}

加了才 OK。问题是,大多数人根本不会意识到这个细节。 就像你以为穿了保险套结果发现是假的------以为安全,结果暴露了。


坑五:子组件样式干不过父组件"莫名其妙的类"

这个是真踩到爆。

我写了一个按钮组件,样式如下:

scss 复制代码
// Button.module.scss
.button {
  background: green;
}

然后放在一个弹窗里:

jsx 复制代码
<div className="modal">
  <Button />
</div>

弹窗的 CSS 居然是这样:

css 复制代码
.modal button {
  background: red;
}

就算你 button 是 .button__x8fa2,也照样被 button 选中覆盖了。你写的 CSS Module 其实只是类名隔离,不是 CSS 权重隔离

这种时候你只能加 !important,然后你就走上了一条不归路。


坑六:你以为 className 是 string,其实是对象 key(debug 崩溃)

看这个经典报错:

js 复制代码
TypeError: Cannot read properties of undefined (reading 'container')

你一看:

jsx 复制代码
<div className={styles.container}>

你说不对啊,我不是写了 container 吗?

结果一查发现你把 .container 写成了:

scss 复制代码
.contianer {
  padding: 20px;
}

打错了,styles 里压根没这个 key。

TS 也没提示,因为 .scss 导入后默认是 any。

最尴尬的是你还用 VSCode 自动补全,结果打完没爆错,但样式没生效,然后你开始疯狂怀疑人生。


坑七:和 Tailwind 一起用就像打架

现在团队都开始上 Tailwind,但又说:"基础样式我们统一用 Tailwind,组件内部用 CSS Module 写细节。"

这想法听起来没错,但实际写起来像这样:

jsx 复制代码
<div className={`flex justify-center ${styles.customBtn}`}>Click</div>

结果是什么?

  1. Tailwind 样式是全局的,优先级高
  2. 你写的 .customBtn 不好加权重,除非你写 :global(.customBtn)

最终你会在代码里频繁看到:

scss 复制代码
.customBtn {
  @apply flex items-center text-white !important;
}

你就会问自己一句:

我要是都用 Tailwind 了,我为啥还写 Module?


那我最后怎么办的?

我最后还是妥协了。

我做了个"样式三分法":

  1. 全局样式 (reset、layout、主题):用普通 .scss 或 Tailwind
  2. 通用组件样式 :用 CSS Module
  3. 动态行为类名 :用 classnames 或者手写逻辑加 styles[xxx]

甚至我给组件封装了个小 hook:

ts 复制代码
function useStyle(styles, keys: string[]) {
  return keys.map(k => styles[k]).join(' ');
}

这样写起来能稍微清爽点:

jsx 复制代码
<div className={useStyle(styles, ['btn', active && 'active'])} />

👉CSS Module 是把双刃剑,你要真用得顺,得知道它到底在哪坑你

我不是说 CSS Module 不好,它的确能解决一堆命名冲突、样式污染的问题。

别把它当"傻瓜式模块化解决方案",它其实是你代码整洁的放大镜。你写得不清不楚,它就暴露得特别明显。

📌 你可以继续看我的系列文章

相关推荐
Carlos_sam1 小时前
OpenLayers:封装一个自定义罗盘控件
前端·javascript
前端南玖1 小时前
深入Vue3响应式:手写实现reactive与ref
前端·javascript·vue.js
wordbaby1 小时前
React Router 双重加载器机制:服务端 loader 与客户端 clientLoader 完整解析
前端·react.js
itslife2 小时前
Fiber 架构
前端·react.js
3Katrina2 小时前
妈妈再也不用担心我的课设了---Vibe Coding帮你实现期末课设!
前端·后端·设计
hubber2 小时前
一次 SPA 架构下的性能优化实践
前端
可乐只喝可乐2 小时前
从0到1构建一个Agent智能体
前端·typescript·agent
Muxxi2 小时前
shopify模板开发
前端
Yueyanc2 小时前
LobeHub桌面应用的IPC通信方案解析
前端·javascript
我是若尘3 小时前
利用资源提示关键词优化网页加载速度
前端