我踩爆了 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 不好,它的确能解决一堆命名冲突、样式污染的问题。

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

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

相关推荐
雪碧聊技术几秒前
深入解析Vue中v-model的双向绑定实现原理
前端·javascript·vue.js·v-model
快起来别睡了2 分钟前
手写 Ajax 与 Promise:从底层原理到实际应用
前端
打不着的大喇叭1 小时前
uniapp的光标跟随和打字机效果
前端·javascript·uni-app
无我Code1 小时前
2025----前端个人年中总结
前端·年终总结·创业
程序猿阿伟1 小时前
《前端路由重构:解锁多语言交互的底层逻辑》
前端·重构
Sun_light1 小时前
6个你必须掌握的「React Hooks」实用技巧✨
前端·javascript·react.js
爱学习的茄子1 小时前
深度解析JavaScript中的call方法实现:从原理到手写实现的完整指南
前端·javascript·面试
莫空00001 小时前
Vue组件通信方式详解
前端·面试
呆呆的心1 小时前
揭秘 CSS 伪元素:不用加标签也能玩转出花的界面技巧 ✨
前端·css·html
susnm1 小时前
Dioxus 与数据库协作
前端·rust