BEM核心是类名自带归属、角色、状态三重语义并固化进构建流程,block名须对应物理组件目录,禁止泛化命名与后代选择器,JS中禁用字符串拼接类名,CI必须阻断非BEM类名落地。

大型CSS项目用BEM组织结构,核心不是“怎么分文件夹”,而是让每个类名自带归属、角色、状态三重语义,并通过工具链把这种语义固化进构建流程。手写 user-card__title--highlighted 看似合规,但若它出现在 common.css 里、被三个组件共用、又被 JS 动态拼接,那结构再整齐也白搭。
block 名必须对应物理文件或组件边界
一个 product-list block,就该有对应的 /components/product-list/ 目录,里面只放 ProductList.vue 和 ProductList.module.css(或 ProductList.scss)。禁止把所有 block 样式塞进 common.css 或 layout.scss —— 后者本质是命名空间污染的温床。
常见错误现象:
-
.section-2、.wrapper这类纯布局类名不能当 block,它们没功能含义,也无法独立复用 -
.card是泛化命名,必须升级为具体业务 block:user-card、product-card、ad-card - block 名必须小写+字母数字:
UserCard或card-header会触发stylelint-selector-bem-pattern报错
SCSS 文件里只允许单层嵌套 &__ 和 &--
正确写法编译后输出扁平类名;错误写法会悄悄生成后代选择器,破坏 BEM 封装性。
立即学习“前端免费学习笔记(深入)”;
允许:
.product-card {
&__price { color: #e74c3c; }
&--featured { background: #f8f9fa; }
}禁止:
.product-card {
&__content {
&__icon { /* 编译出 .product-card__content .product-card__content__icon */ }
}
}遇到“元素里的元素”,比如 .table-cell__status-badge,说明 status-badge 其实已具备独立语义和复用价值,应升格为 block:.status-badge + .status-badge--in-table。
JS 中禁止字符串拼接 BEM 类名
写 className={`product-card__body ${isCollapsed ? 'product-card__body--collapsed' : ''}`} 是最高危破规点——它绕过所有工具链校验,且大小写、空格、连字符错一位,CI 构建时 Linux 环境直接失效。
实操建议:
- 统一用封装函数,如
cn('product-card', 'body', { 'product-card__body--collapsed': isCollapsed }) - 函数内部必须校验参数:传入
''或'product-card__body__icon'应直接抛错,不静默容忍 - 搭配
clsx或classnames做布尔映射,但前提是所有 key 都是静态字符串,禁用动态生成修饰符名(如`product-card__body--${status}`)
CI 流程中必须阻断非 BEM 类名落地
光靠人眼 Review 拦不住手滑。ESLint 和 Stylelint 只是第一道防线,真正起效的是 CI 里跑强制校验。
关键配置项:
-
stylelint-selector-bem-pattern规则设为{"componentName": "[a-z][a-zA-Z0-9]+", "styleType": "bem"},否则默认配置允许.btn或三层嵌套.card__title__link - CI 脚本加
npx stylelint "**/*.{css,scss}" --fix,失败即中断构建 - VS Code 安装
stylelint-plus插件,保存时标红.header .logo这类后代选择器
最易被忽略的一点:SSR 场景下,服务端渲染的类名必须和客户端完全一致。若用了 useId() 或随机 hash 生成类名,hydration mismatch 会触发整块 DOM 重绘——BEM 名必须静态、可预测、不依赖运行时。


















