BEM在React中必须绑定CSS Modules、构建校验和封装函数,否则失控;文件名与Block名须严格一致且大小写敏感;禁止手写字符串,须用bem工厂函数+classnames生成类名;Modifier须受控原子化;必须启用postcss-bem-linter强制校验。

不能只靠手写类名或拼字符串,BEM在React里必须和CSS Modules、构建校验、封装函数三者绑定,否则很快失控。
文件名与Block名必须严格一致
CSS Modules的styles对象键名完全由文件名决定:文件叫Card.module.css,里面所有类就必须以.card开头;写成.Card或.ui-card,styles.card__header就返回undefined,且控制台不报错。
- 验证方法:运行
npm run build后打开生成的Card.module.css,看类名前缀是不是Card_开头 - 开发期建议配置
localIdentName=[name]__[local]___[hash:base64:5],调试时一眼看出Card__header___abc12来自哪个文件 - 大小写敏感:文件是
button.module.css,就不能在JS里用styles.Button__icon——哪怕CSS里写了.Button__icon也不行
禁止手写BEM字符串,必须用bem工厂函数 + classnames
写className={`card ${isHovered ? 'card--hovered' : ''}`}有三个硬伤:漏空格、拼错连字符(比如card-hovered)、isHovered为null时留下多余空格。更关键的是,它绕过CSS Modules哈希映射,样式静默失效。
- 正确做法:在组件顶部定义
const cardBem = bem('card'),其中bem是通用工厂函数:const bem = (block) => ({ e: (el) => `${block}__${el}`, m: (mod) => `${block}--${mod}` }); - 所有类名必须经
cardBem.e('footer')或cardBem.m('loading')生成,再喂给classnames:className={cn(styles.card, cardBem.m('loading'), { [styles[cardBem.e('footer')]]: hasFooter })} - 禁止解构
styles后直接拼:${styles.card} ${styles['card--loading']}——后者不存在时会拼出undefined
Modifier值必须受控且原子化
BEM的--modifier不是视觉描述词,而是可枚举的状态标识。写cn(styles.button, `button--${theme}`)很危险——theme若来自props且未校验,可能注入button--hacker这种非法类。
立即学习“前端免费学习笔记(深入)”;
- 修饰符只能来自白名单:比如
type: 'primary' | 'secondary' | 'danger',不能是任意字符串 - 禁止嵌套修饰符:
button__icon--large--bold是反模式;应拆成button__icon button__icon--large button__icon--bold,或合并为button__icon--large-bold(仅当设计系统明确定义该组合) - 修饰符永远作用于具体UI单元:card里只有header要禁用,就加
card__header--disabled,而不是card--disabled再靠CSS降权控制子元素
必须启用postcss-bem-linter强制校验
它不是可选插件,而是防止BEM结构滑坡的底线工具。没有它,button.module.css里混进.modal__close、.card__footer这类跨Block类名,没人能及时发现。
- 配置后,
button.module.css里出现.card__header会直接报错,打断构建 - 配合ESLint插件
eslint-plugin-css-modules,校验JSX中className是否只用了当前文件声明的Block前缀 - 所有Element名只能是名词:
__icon、__content合法;__show、__toggle违规——那是状态逻辑,该交给JS控制类名切换
最易被忽略的一点:BEM本身不解决作用域问题,真正起作用的是“约束力”。比如Button.module.css里写了.button__icon,但你在Card.tsx里import了它并用了styles['button__icon'],这就破坏了Block边界——这种跨组件引用必须被lint拦住,不能靠自觉。


















