
很多人最开始接触 BEM 命名法都是冲着“解决 CSS 命名冲突”去的用了两年之后又会开始犹豫为什么项目里照样乱为什么照样有人写出.card__header__title__text这样的类名我的经验是BEM 被严重低估了也严重被误读。它看起来只是三行命名规则但真正的难点在命名之前——你要先搞清楚一个组件里的哪些东西是块哪些是元素哪些是状态然后才轮到下划线怎么写。这篇文章想做的就是用庖丁解牛的方式把 BEM 这头牛从骨架到筋膜完整拆一遍把我在一线项目里踩过的坑、总结的判断标准、以及和现代工程化工具配合的姿势都摊开来讲。1. 解其骨命名混乱到底坏了多少事1.1 一个发展中的项目是怎么被命名拖垮的先说一个我 2019 年接手的真实项目一个运营后台有三十多个页面CSS 全部手写命名风格大概有三种。早期的人用短横线.user-card中期的人用驼峰.userCard,后期的人用 BEM 但不彻底.user-card_item都有。这个项目真正崩塌的节点不是功能难写而是所有人不敢改样式了。举个例子想要给某个按钮加一个disabled灰色背景你得先全局搜索.btn结果出来六百多处里面既有按钮又有标签页控件还有一些表格行。没有人敢确定改这一处不伤其他页面于是团队养成了一个很诡异的习惯给新页面复制一份类似的按钮类名再改一改。类名越来越多样式之间互相覆盖important出现频率越来越高最后只能靠“谁最后加载谁生效”来决定样式连靠前的人都说不准。这不是管理问题是命名体系的问题。一套结构化命名方法的缺失会让样式表的维护成本随着代码量指数级上升。BEM 的价值恰恰在这里它不直接帮你写样式它帮你把“这个类属于哪个组件、描述的是结构还是外观状态”这件事在任何人都能看懂的前提下在代码里标记出来。1.2 “什字路口”式的命名方案你可能会觉得只要团队规定一种命名风格不就行了可惜事情没那么简单。kebab-case、camelCase、PascalCase之间的区别只是表皮真正的问题是语义层面一个人写.card-title,另一个人写.cardTitle,两个人其实表达的是同一个东西却因为风格不同导致搜索的时候搜不全改样式的时候判断不了全局影响。BEM 最强的地方正是把这个问题解构了Block块、Element元素、Modifier修饰符三个角色对应三种命名模式任何类名只要看一眼就能判断它属于哪一层。这就好比在你家书架上每本书都贴了分类标签找书和放书都变得有规律可循。命名体系不是表面形式它是代码可读性的骨架骨架正了后面的肉和筋才有地方挂。2. 解其肉Block Element Modifier 三层命名只有三件事2.1 Block块独立、自足、可复用的组件单元Block 是 BEM 的基石它代表一个“独立存在的 UI 组件”。独立指的是不依赖父容器也能成立。比如一个.search-form放在头部是搜索框放到侧边栏还是搜索框放到移动端底部也一样。这也意味着 Block 在设计时就要尽量做到上下文无关不写依赖父级选择器的样式。一个 Block 可以是简单的按钮.btn也可以是复合组件比如.member-card成员卡片内部可以包含头像、姓名、描述、状态标签但对外暴露的入口永远只有一个 Block 名。这里有一个关键判断标准如果一个组件的复用仅限单一页面它可能只是一个元素如果它被放置在不同页面、不同容器里都能独立工作那才配叫 Block。2.2 Element元素必须隶属于 Block 的组成部分Element 是 Block 内部的一部分不能脱离 Block 单独存在。比如.member-card__avatar头像、.member-card__name姓名脱离了 member-card 这个整体它们没有任何独立意义。命名语法就是block__element双下划线连接。这里容易犯的第一个错误是嵌套过深.card__header__title__text这种。BEM 官方并不支持元素里再套元素因为一旦允许无限嵌套类名会变得又长又脆弱。如果你发现一个 Element 结构太复杂比如.member-card__header里要拆很多东西正确的做法是无中生有把.member-card__header升级为一个新的 Block比如.card-header或者把它里面需要细分的部分抽成另一个独立的 Block 放在原 Block 内部。2.3 Modifier修饰符同一种结构的差异化Modifier 描述一个块或元素的外观、状态、行为的变化。语法是block--modifier或者block__element--modifier用双横线连接。比如按钮有.btn--primary、.btn--large、.btn--disabled标签页有.tabs__item--active。Modifier 的核心价值在于它和原始类名耦合出现HTML 里是classbtn btn--primaryCSS 里是把公共样式放到.btn差异样式放到.btn--primary。这种做法避免了你去动态拼接类名或者是复制粘贴一大段基础样式。Modifier 对应的是可枚举的状态集合primary、large、active、disabled而不是一个可以无限取值的变量这一点后面我还会展开讲。2.4 基础语法速查命名模式语法示例语义Block.block.search-form独立组件Element.block__element.search-form__input组件内部的组成部分Modifier块级.block--modifier.search-form--compact组件的变体或状态Modifier元素级.block__element--modifier.search-form__input--error元素的具体状态这套语法不是随便定的。双横线--和双下划线__之所以比单横线更“重”是因为单横线经常出现在单词拼接里.btn-small会被误判成“这个名字里 right 部分可能是个 block 而 left 部分是形容词”造成语义歧义。双分隔符就是为了在类名里划出清晰的边界让机器和人都能一眼区分角色。3. 解其筋从组件模型出发的实操命名流程3.1 第一步先切块构建组件地图这一步很多人跳过了于是命名全凭感觉。我的标准做法是拿到视觉稿后先用正方形画出页面里的独立组件每个框就是一个候选 Block。框与框之间不重叠、不嵌套。举例一个成员管理页面可以拆出筛选栏.filter-bar、列表容器.member-list、成员卡片.member-card、分页器.pagination、空态提示.empty-state。这时候有个很容易犯的错误把视觉上连续的部分硬拆成多个 Block。比如一个卡片内部有头部区域、内容区、底部操作区这些区域是卡片的一部分不是独立组件所以用 Element 表达而不是 Block。切块有个简单的判断口诀“拎出来还能说的清是什么”才配当 Block。member-card拎出来你知道它是什么member-card__header拎出来如果不带上卡片这个上下文它就什么都不算所以它是 Element。3.2 第二步辨元素只给有结构语义的节点命名Block 内部不是所有 DOM 节点都需要类名。样式的目标有三类层级布局如 flex 容器、视觉呈现如背景色以及状态控制如条件渲染。如果一个标签只是为了包一层、没有实际样式职责那它就没有存在的必要更不需要 BEM 命名。比如 card 内部只有一个div包裹头像和文本这个div既没布局职责也没视觉职责完全可以去掉类名数量自然降下来了。真正需要元素类名的节点通常是这几种承载文本内容的标签标题、描述、注释承载多媒体内容的标签图片、图标、视频功能控件输入框、按钮、链接布局容器卡片头部、主体、底部操作区对应到示例上article classmember-card div classmember-card__header img classmember-card__avatar altavatar / h3 classmember-card__name李雷/h3 span classmember-card__status在线/span /div p classmember-card__desc前端工程师负责会员增长方向。/p div classmember-card__actions button classbtn btn--primary发消息/button /div /article注意这里.btn是另一个 Block 直接被放进member-card__actions内部。Block 与 Block 之间的嵌套是完全允许的。member-card__actions是一个布局容器元素它内部挂载独立按钮 Block这种逻辑非常清晰。3.3 第三步提修饰把“变化”从“基础”里解放出来写完结构类名之后再检查一下每个 Block 和 Element 身上有哪些视觉或者行为上的差异。差异点从两个视角提取一是同一页面里相同组件出现多次时各自不同的部分二是同一组件在不同断点或者不同用户状态下需要切换样式的部分。比如成员卡片在同一列表里有“在线”和“离线”两种状态背景色和边框不同这就可以抽象成.member-card--offline或.member-card--online。但这里有一个更细的问题在线和离线是业务语义直接做类名会让 Block 和业务耦合太重。常见的做法是做成通用状态修饰符.member-card--active和.member-card--inactive状态值交给人来映射。如果差异点是多个值的比如按钮有 primary、secondary、ghost 三种用 Modifier 就非常合适.btn { padding: 8px 16px; border-radius: 4px; } .btn--primary { background: #07c; color: #fff; } .btn--secondary { background: #eee; color: #333; } .btn--ghost { background: transparent; border: 1px solid #07c; color: #07c; }我见过不少团队把这种差异做成了.btn-primary、.btn-secondary各自成套基础样式复制三份结果后期改圆角改三处这恰恰是 BEM 想消灭的低效。4. 解其髓修饰符的控制边界与嵌套分寸4.1 状态修饰符和变体修饰符的界限Modifier 可以再分两类状态式 Modifier 与变体式 Modifier。状态式描述的是瞬时状态比如.btn--loading、.tabs__item--active、.member-card--selected它们通常由用户交互动态添加或移除。变体式描述的是相对固定的外观分类比如.btn--primary、.alert--success一般在渲染时就确定不随交互变化。这两者的工程意义不太一样。状态式 Modifier 经常配合 JavaScript 来切换所以我倾向于在 JS 里只用状态类来控制逻辑不让内联样式参与显示切换。变体式 Modifier 则更像组件的 API——调用方只要说“我需要一个 danger 类型的按钮”组件就能输出.btn--danger。如果混为一谈组件长期迭代后状态类会膨胀到不可维护。4.2 不要在 __elems 里无限嵌套BEM 的官方范例中没有block__elem1__elem2。理论上每个 Element 都直属于 Block。实际操作时你会发现有些结构确实需要三层以上我的建议是遇到这种情况先停下来问自己“这个 Element 是否已经复合到可以被抽象成一个独立 Block”举个例子.member-card__header__title如果觉得别扭说明.member-card__header本身已经承担了足够复杂的布局职责可以把它抽成.card-headerBlock标题变成.card-header__title。这样看起来是重构但本质上你是在把组件边界画得更清楚。当然也存在一些特例比如.page-header__title__icon通常是某个深层节点无法再拆的时候被迫写出来的。真到了那一步我宁愿用一个语义更直接的类名.page-header__title-icon单横线表示复合词也不愿意堆出三层下划线。规则是死的人是活的但“尽量两层”的原则能保证类名长度和可读性。4.3 修饰符组合的顺序与可读性一个元素同时有多个修饰符的时候比如一个既 active 又 large 的标签HTML 里写classtabs__item tabs__item--active tabs__item--large。一个常见争议是到底用tabs__item--active-large这种复合修饰符还是拆成多个。我强烈建议拆开。复合修饰符会污染类名空间每多一种组合就等于新造了一个类名而拆开只是任意两组修饰符的排列组合两个类都能匹配到对应的基础样式。组合顺序也不必纠结BEM 没有规定顺序我自己的习惯是状态类在前尺寸类在中风格类在后保证团队统一就可以。4.4 Element 的省略技巧实际写的时候有些元素是不需要出现在类名里的。比如.member-card__header只是一个 flex 布局容器它的样式就一行display:flex; align-items:center; gap:8px;这种纯布局容器也可以不命名直接把 flex 写在.member-card上然后用.member-card__avatar来控制自身间距省掉一个层级。我的原则是如果某个 Element 只承载布局、没有独立的视觉语义而且它底下只有一个子元素那就去掉这层容器如果它有多个子元素需要做横向或纵向排布而它的布局属性会直接影响这些子元素的排列那保留.member-card__header是合理的。5. 解其器SCSS、Vue/React 工程化与 BEM 的协作方式5.1 SCSS 的 符号写 BEM 有三层坑很多人在 SCSS 里用嵌套写 BEM语法本身没错但会踩几个坑。第一个坑是过度嵌套导致选择器特权度升高。比如写成.member-card { .member-card__name { font-size: 16px; } }实际编译出来是.member-card .member-card__name这是一个后代选择器优先级变成了 0,2,0而直接写.member-card__name是 0,1,0。这会导致以后想覆盖样式的时候必须同样用后代选择器层级越来越深。正确的做法是用at-root把嵌套写法的类名拉回根级.member-card { at-root { __name { font-size: 16px; } --active { border-color: #07c; } } }这样编译出来.member-card__name和.member-card--active都是平级类名选择器权重不会积累。第二个坑是编辑器里的自动缩进容易让人忽略从属关系类名层次混乱。第三个坑是在修饰符内部继续嵌套元素选择器比如.member-card--active .member-card__name这个跟上面的问题一样应直接写成.member-card--active .member-card__name的逻辑尽量用.member-card__name--active或直接给元素加 Modifier 来表达。5.2 CSS Modules 和 scoped 到底还需不需要 BEM先说结论需要但侧重点不同。CSS Modules 解决的是“类名冲突”的问题它会把局部类名编译成带哈希的全局唯一类名。但 CSS Modules 不解决“语义是否统一”的问题同一个组件在两个团队手里可能会叫不同的名字。BEM 解决的是语义的统一与层级关系的清晰表达这两者并不冲突实践中可以叠加用 CSS Modules 做作用域隔离用 BEM 做组件内结构的语义编码。Vue 的scoped属性会在选择器末尾加>// .stylelintrc.json { rules: { selector-class-pattern: ^[a-z][a-z0-9]*(-[a-z0-9])*(__[a-z0-9](-[a-z0-9])*)?(--[a-z0-9](-[a-z0-9])*)?$ } }这个正则的含义是Block 名可以带短横线member-card可选一个__元素可选一个--修饰符元素和修饰符内部也可以带短横线。基本覆盖 BEM 的完整形态。CSS 预编译的类名比如 Vue 里的:class动态字符串只要最终渲染出来的类名符合规范一样会被规则约束。团队的 onboarding 成本一下就降下来了不需要有人天天提 PR 审查要求。6. 解其域BEM 的适用边界与同行方案的选择逻辑6.1 哪些场景 BEM 确实不够好BEM 不是万能的。做组件库的底层样式的时候原子化 CSSutility-first比如 Tailwind在效率上确实有优势改一个内边距间距不用新起一个类不用去设计命名纯粹就是组合工具类。加上现代 CSS 里层叠层(layer)的出现也可以更优雅地处理不同来源样式的优先级问题。BEM 在两类项目里会显得笨重一类是纯展示型页面消费者不需要知道结构层级只要元素换肤调整即可另一类是高度原子化的设计系统里面大量样式都是p-4、flex、text-sm这种通用原子类BEM 反而会跟它们叠加成冗余类名。我自己的项目选型原则是这样的组件库内部、多端复用、长期演进选 BEM因为 API 语义稳定调用方通过 Modifier 来控制变化营销页、活动页、快速原型直接 utility-first因为页面一次性性能优先大型业务系统两者混合组件层用 BEM布局层和细节微调用 utility6.2 同行方案对照方案核心机制优势短板和 BEM 关系BEM命名约定表达结构和状态语义清晰、无构建依赖、可读性强类名较长、需要团队纪律基准方案CSS Modules编译期生成本地类名自动隔离、不必想全局唯一名调试困难哈希类名、层叠语义弱可叠加CSS-in-JS组件内直接写样式对象局部隔离、动态样式天然友好运行时开销、SSR 样式抽取复杂概念冲突Tailwind / 原子化HTML 里组合工具类写起来快、类名几乎不用自己想可读性依赖工具类的熟练度可混用6.3 BEM 在大型团队里最容易失效的组织原因BEM 失效通常不是因为语法而是因为组织没有建立组件分层。当团队里任何人都能新造一个 Block、随意添加 Modifier而组件清单没有评审时BEM 会退化成“只是长一点的随意命名”。所以光有命名规范远远不够还要配套组件代码规范、Storybook 维护规范和评审流程。规范的价值只有依托流程才能被维持。落到我自己的团队我们有一起“组件命名会议”新页面拆模之后命名清单先过一轮评审重点就是判断新起的类名到底配不配 Block现有 Block 能不能复用Modifier 是否应该收敛。这一步消耗的时间不多但避免了百分之八十的后续返工。写在最后我对 BEM 的真实态度如果你问我BEM 是不是最好的命名方案我的回答是在我参与过的三四十人规模的前端团队里它是最能让不同水平的人写出相似代码的方案。你可以不认同它在具体项目里的表现但它的核心价值——强制你把 UI 解构成清晰的层级模型再把这些层级映射到命名上——这个思路永远不过时。我见过太多团队从 BEM 迁移到 CSS Modules 之后问题依旧本质原因是他们没有带走 BEM 的思考方式。真正能救项目的不是某一个规范而是一套稳定的、全员共享的组件拆分心智模型。BEM 只是把这种心智模型外化到了类名上而已。方法论都是可以迭代的但问题意识和分层习惯一旦养成后面用什么工具都不会太差。