我不是第一次用 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>
结果是什么?
- Tailwind 样式是全局的,优先级高
- 你写的
.customBtn
不好加权重,除非你写:global(.customBtn)
最终你会在代码里频繁看到:
scss
.customBtn {
@apply flex items-center text-white !important;
}
你就会问自己一句:
我要是都用 Tailwind 了,我为啥还写 Module?
那我最后怎么办的?
我最后还是妥协了。
我做了个"样式三分法":
- 全局样式 (reset、layout、主题):用普通
.scss
或 Tailwind - 通用组件样式 :用
CSS Module
- 动态行为类名 :用 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 不好,它的确能解决一堆命名冲突、样式污染的问题。
别把它当"傻瓜式模块化解决方案",它其实是你代码整洁的放大镜。你写得不清不楚,它就暴露得特别明显。