bem 在 css modules 中是必须对齐的映射契约,文件名、类名、jsx 访问键三者须严格一致:文件名 pascalcase 且与组件名相同,类名以 block__element 形式声明,jsx 中用 styles['block__element'] 访问。

styles.button__icon 返回 undefined,不是 CSS 写错了,而是文件名、类名、JSX 三者没对齐。BEM 在 CSS Modules 里不是“可选风格”,而是必须对齐的映射契约。
Button.module.css 文件名必须和 Block 名完全一致
CSS Modules 用文件名生成哈希前缀,Button.module.css → Button_button__xxx;如果文件叫 button.module.css 或 MyButton.module.css,styles.button__icon 就永远是 undefined。
- 文件名必须 PascalCase,且与 React 组件名严格一致(如
Card.tsx↔Card.module.css) - 所有类名必须以
.card开头,禁止混用.product-card或.ui-card - Element 必须用双下划线:
.card__header✅,.card-header❌(后者不参与 BEM 映射) - 验证方式:打开编译后 CSS,看类名是否以
Card_开头;再console.log(styles)确认键是否存在
JSX 中访问 styles 必须用字符串键,不能解构或硬编码
styles.button__icon 是语法错误(点号访问非法字符),styles['button__icon'] 才是正确路径 —— 而且这个字符串必须和 CSS 文件中声明的类名逐字匹配,包括大小写和连字符。
- 禁止:
${styles.button} ${styles['button--primary']}—— 后者为undefined时会拼出"button undefined" - 禁止:
styles.button--primary(语法报错)或styles['Button__icon'](大小写不一致) - 推荐调试配置:
localIdentName=[name]__[local]___[hash:base64:5],编译后类名带原始名,一眼可查来源
动态拼接类名必须用 classnames + BEM 工厂函数
手写 {`button ${isActive ? 'button--active' : ''}`} 容易漏空格、拼错 modifier、传入 null 导致多余空白,而且绕过 CSS Modules 的哈希校验,样式静默失效。
- 定义工厂:
const bem = (block) => ({ e: (el) => `${block}__${el}`, m: (mod) => `${block}--${mod}` }) - 绑定 Block:
const buttonBem = bem('button') - 配合
classnames使用:className={cn(styles.button, buttonBem.m('primary'), { [styles[buttonBem.e('icon')]]: hasIcon })} - 禁止混用:
cn('button', { 'button--disabled': disabled })—— 字符串绕过校验,TypeScript 也无法约束
postcss-bem-linter 不是锦上添花,是防止结构滑坡的底线
没有它,团队很快会写出 .button__icon--large--dark 或在 Button.module.css 里偷偷引用 .modal__close,BEM 的 Block 边界就形同虚设。
- 它能拦截:
.button .icon(空格选择器)、.button-icon(缺双下划线)、.button__content--loading(Element 下挂 Modifier) - 必须集成进构建流程,CI 阶段失败即阻断,不能只靠人工 Code Review
- 搭配 ESLint 插件
eslint-plugin-css-modules,校验 JSX 中 className 是否匹配当前文件声明的 BEM 模式
BEM 的价值不在 DevTools 里看到 button__icon,而在于文件名、CSS 类名、JSX 访问键三者之间那条不可绕过的映射链——断一环,整个结构就静默崩塌。最常被跳过的其实是文件命名一致性,而不是函数怎么写。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











