css modules 的哈希不破坏bem语义,因原始类名(如button__text)完整保留在源码中作为key用于js访问和调试,语义由源码承载,隔离由哈希实现;关键在于css-loader需显式配置modules: { mode: 'local' }及localsconvention: 'camelcaseonly'等参数。

css-loader 的 modules 配置必须显式启用,否则 BEM 类名不隔离
Webpack 默认不开启 CSS Modules,modules: true 不是可选项,而是前提。没配这一项,.button__text 就是全局类名,BEM 命名再规范也挡不住样式污染。
常见错误现象:开发时看着正常,上线后按钮样式突然被另一个子应用的 .button 覆盖——根本原因是 css-loader 没开 modules,哈希映射压根没发生。
- Webpack 5+ 必须写
modules: { enabled: true }(旧版用modules: true) - 别依赖 Vite 那套默认行为,Webpack 不会自动识别
.module.css后缀 - 如果项目混用全局 CSS 和模块化 CSS,记得给非模块文件加
/* webpackMode: "global" */注释,否则会被误处理
localsConvention 必须设为 'camelCaseOnly',否则 BEM 双下划线结构丢失
localsConvention: 'camelCaseOnly' 是保命配置。默认值会把 button__text 转成 buttonText,BEM 的语义层级直接坍塌——你再也搜不到 __text,校验工具也报错。
使用场景:团队用 ESLint + stylelint-selector-bem-pattern 检查 BEM 规范,一旦 button__text 在 JS 中变成 buttonText,规则就形同虚设。
- 必须显式写
localsConvention: 'camelCaseOnly',不能省略 - 禁用
camelCase或asIs:前者丢掉双下划线,后者让button__text在 JS 中变成button__text(合法但不可读) - Vite 默认就是
camelCaseOnly,Webpack 不会继承这个行为
localIdentName 要保留 [name]__[local],不能只用哈希
调试时得知道 button__text_abc123 对应哪个文件哪一行。localIdentName: '[hash:base64]' 看似简洁,实则断掉溯源链——你只能看到一串随机字符,没法反查源码。
性能影响几乎为零:加 [name]__[local] 多几个字符,gzip 后体积差异可忽略;但开发体验天差地别。
- 推荐写法:
localIdentName: '[name]__[local]--[hash:5]' - 禁止删掉
[name]或[local]:否则button__text在生成结果里消失,只剩哈希 - 别用
[path]:路径太长,构建慢,且 Windows/macOS 路径分隔符不一致,CI 容易出错
BEM 类名本身不能动态拼接,CSS Modules 映射只认静态键
JS 里写 styles[`button__${state}`] 或模板字符串拼类名,css-loader 无法识别——它只解析字面量键,比如 styles.button__text。动态键会导致运行时 undefined,最终 class 属性为空。
常见错误现象:按钮在 disabled 状态下文字颜色没变,检查 DOM 发现 class 没渲染出来,根源就是用了动态属性访问。
- 修饰符必须提前定义好:
styles['button__text--disabled'],而不是styles[`button__text--${state}`] - 状态组合用多个 class:
className={`${styles.button__text} ${styles['button__text--disabled']}` - 需要大量变体时,用对象映射预先声明:
const modifiers = { disabled: styles['button__text--disabled'] }
BEM 的三段式(block__element--modifier)不是装饰,是 css-loader 映射时的解析契约。少一个下划线、多一个层级、动态拼字符串,都会让映射断裂——问题不在构建失败,而在运行时无声失效。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











