news 2026/10/2 18:18:20

BEM命名法深度实践:从组件拆分到工程化落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
BEM命名法深度实践:从组件拆分到工程化落地

很多人最开始接触 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 里是class="btn btn--primary",CSS 里是把公共样式放到.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 class="member-card"> <div class="member-card__header"> <img class="member-card__avatar" alt="avatar" /> <h3 class="member-card__name">李雷</h3> <span class="member-card__status">在线</span> </div> <p class="member-card__desc">前端工程师,负责会员增长方向。</p> <div class="member-card__actions"> <button class="btn 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 里写class="tabs__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 不是万能的。做组件库的底层样式的时候,原子化 CSS(utility-first,比如 Tailwind)在效率上确实有优势:改一个内边距间距不用新起一个类,不用去设计命名,纯粹就是组合工具类。加上现代 CSS 里层叠层(@layer)的出现,也可以更优雅地处理不同来源样式的优先级问题。

BEM 在两类项目里会显得笨重:一类是纯展示型页面,消费者不需要知道结构层级,只要元素换肤调整即可;另一类是高度原子化的设计系统,里面大量样式都是p-4、flex、text-sm这种通用原子类,BEM 反而会跟它们叠加成冗余类名。

我自己的项目选型原则是这样的:

  • 组件库内部、多端复用、长期演进:选 BEM,因为 API 语义稳定,调用方通过 Modifier 来控制变化
  • 营销页、活动页、快速原型:直接 utility-first,因为页面一次性,性能优先
  • 大型业务系统:两者混合,组件层用 BEM,布局层和细节微调用 utility

6.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 只是把这种心智模型外化到了类名上而已。方法论都是可以迭代的,但问题意识和分层习惯一旦养成,后面用什么工具都不会太差。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 18:18:11

openclaw技能添加实战:从环境部署到Obsidian笔记

最近不少朋友在折腾本地部署的智能体网关&#xff0c;问到最多的问题就是“openclaw怎么添加技能”。网上教程五花八门&#xff0c;但大多数只讲了装完环境怎么跑起来&#xff0c;真正到“让代理学会一个新技能、能在对话里被调起来”这一步&#xff0c;很多人卡住了。我前前后…

作者头像 李华
网站建设 2026/10/2 18:16:44

微小型双足鸭形机器人:强化学习从仿真到实机部署全解析

微型双足机器人这两年最热闹的赛道&#xff0c;其实是往“小”里卷。大尺寸人形机器人有波士顿动力和几家头部厂商在前面顶着&#xff0c;硬件成本和运动控制门槛都极高&#xff1b;反倒是微小型双足鸭形机器人这种形态&#xff0c;整机质量可以压在几百克以内&#xff0c;关节…

作者头像 李华
网站建设 2026/10/2 18:15:44

口岸高峰那篇毕业论文,到底该让哪个 AI 帮你?[特殊字符]

边防管理专业的同学大概都懂一种“夹在中间”的写作状态&#xff1a;题目既要像法学&#xff0c;又要像公安学&#xff1b;既要讲法律法规&#xff0c;又要懂口岸运行、边检执法、边境治理和部门协同。 比如这篇毕业论文就很典型&#xff1a; 《陆路口岸高峰期大客流应急协同治…

作者头像 李华
网站建设 2026/10/2 18:14:43

Tello TT无人机+YOLOv5:目标识别追踪与单目测距实战

简介&#xff1a;这份资源面向K12阶段学生与AI入门教师&#xff0c;提供基于YOLOv5与大疆教育无人机Tello TT的目标识别、检测、追踪与测距完整方案&#xff0c;将深度学习理论落地到真实飞行场景&#xff0c;解决从模型训练到无人机部署的全流程问题。压缩包共1667个文件&…

作者头像 李华
网站建设 2026/10/2 18:10:15

从梦狐残神到任务暂缓,ABAP 如何让业务安静等待,并在合适的时候恢复

我们正在讨论的「梦狐残神」,有一个很适合拿来理解企业软件的细节。目标仍然存在,战斗仍然继续,但这个目标暂时不能正常行动。程序开发里也经常需要这样的处理,订单不能立刻释放,接口消息不能立刻重试,后台任务不能立刻再次调用故障中的外部服务。业务没有被删除,也没有…

作者头像 李华
网站建设 2026/10/2 18:09:50

VB+SQL Server连锁超市进销存系统:从数据库设计到小票打印完整实现

简介&#xff1a;这份课程设计报告面向高校信息管理与信息系统、计算机相关专业学生&#xff0c;围绕连锁超市进销存管理信息系统的分析与设计展开&#xff0c;可作为课程设计、毕业设计选题的完整参考方案。报告从设计背景与可行性分析入手&#xff0c;依次覆盖系统功能设计、…

作者头像 李华