我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

CSS类名规范实战:深入理解BEM命名法及其落地要点

CSS类名规范实战:深入理解BEM命名法及其落地要点 接手别人代码的都知道最让人血压飙升的往往不是业务逻辑而是 CSS 文件里那一排放飞自我的 class 命名。.left、.blue、.txt-1、.content-l——写的时候怎么顺手怎么来三个月后再回头看连自己都猜不透这个类名到底属于哪个模块。这个问题几乎每个前端团队都会遇上而 BEM 命名法就是为解决它而生的一套可落地的 CSS 类名规范。它把类名分成 Block、Element、Modifier 三种角色用一套统一语法把结构、状态、职责直接写进名字里让人看类名就能还原组件结构。这篇内容会从原理、判定方法到真实组件改造一步步拆解 BEM 命名法顺带聊聊我在实际项目中踩过的坑和最终沉淀的用法。无论你是刚入行的前端新手还是正在为团队隐患排查而头疼的负责人这篇文章都能给你一套可以直接抄作业的方案。1. BEM 到底在解决什么问题1.1 没有命名规范时代码有多痛苦先还原一个很常见的场景。产品提了个需求要在首页卡片上加一个重磅推荐的角标。开发同学飞快地在 CSS 里写下.recommend { background: #f00; color: #fff; }之后他随手把类名挂在卡片右下角的span上。看起来一切正常直到一个月后另一个同事要做一张数据看板推荐位发现页面里已经有了.recommend但在不同业务上下文中这个类名需要完全不同的样式。于是新一轮猜疑链开始到底是改全局还是新建一个名字新建的话叫.recommend-2还是.recommend-data这类问题在 CSS 里几乎每天都会发生原因只有一个——CSS 的类名天然是全局共享的而命名规范没有被认真定义。更隐蔽的问题是没有规范时命名本身带有极大的随意性缩写、拼音、中文直译混在一处甚至同一个词在不同人手里有完全不同的含义。.title可能是标题字号也可能是某个模块的标题容器.icon可能是小图标也可能是包含图标的按钮。等到需要修改样式只能靠全局搜索类名 揣摩上下文来猜改起来如履薄冰。CSS 不像 JavaScript 有变量作用域可以兜底类名就是唯一的 boundary没有边界意识样式覆盖和冲突只是时间问题。1.2 BEM 的三件套Block、Element、ModifierBEM 的完整称呼是 Block Element Modifier由 Yandex 团队提出最开始服务于大型站点的样式组织。它给出的核心思路非常朴素把每个独立的 UI 组件看作一个 Block块把 Block 内部的组成片段叫作 Element元素把 Block 或 Element 的外观、状态、行为变体叫作 Modifier修饰符。用这三件套命名后一个最简单的例子长这样div classcard card--featured h2 classcard__title卡片标题/h2 p classcard__desc内容描述/p /div这里的.card是 Block.card__title、.card__desc是 Element.card--featured是 Block 的 Modifier。看类名就能得到完整信息card是一个独立组件__title明确指出它是卡片内部的一部分以卡片为作用域--featured则说明这张卡片处于推荐这个特殊状态。三个角色分工不同语法也不同保证了看到名字就知道结构。光看概念可能觉得不过如此但真正常用的方法开起来简单用起来却到处都是玄机什么时候该拆 Element什么时候该上 BlockModifier 能不能和 Element 组合嵌套组件怎么不越界这些才是 BEM庖丁解牛的刀锋所在。接下来我把判定逻辑一块块拆开讲。2. BEM 命名规则逐步拆解2.1 三种角色的命名写法先明确 BEM 的基本语法。在同名字符串的前提下Blockheader、menu、search-form。多词拼写一律用单个连字符hyphen连接如search-form、user-profile。Elementblock__elem。Block 名 双下划线 元素名如card__title、menu__item、search-form__input。Modifierblock--mod或block__elem--mod。Block 或 Element 名 双连字符 修饰符名如card--featured、menu__item--active。Element 里的组件名同样遵循单词间用连字符规则比如card__close-btnModifier 内部如果一个状态由多个词组成也可以用连字符分隔如card__btn--bg-primary整个名字读完后语义是自解释的。需要注意BEM 不是一刀切的规范它没有要求所有地方都必须沿用同一种写法前端社区后来衍生出了各种变体有人坚持经典的双下划线、双连字符也有人把 Block 和 Element 的分隔符简化为单个下划线还有的团队干脆只用单下划线区分层级。经典写法在代码审查时最好辨认因为它把 Element 和 Modifier 在视觉上拉开了足够的差异几乎不用思考就能准确拆分角色。2.2 为什么偏偏是两个下划线和两个连字符一个常见问题是为什么 Block 和 Element 之间要用__而不是单下划线或者空格这就要回到 CSS 类名选择器的解析逻辑上说。单连字符一般被用作单词内部连接符比如search-form、user-profile它让两个词保持为一个整体查询词。如果再用一个连字符表示层级像search-form-title容易让阅读者产生歧义这究竟是search-form-title整体概念还是 search-form 的 title 元素多打一个字符恰恰是在类名里建立了分层的视觉符号让扫描速度更快。同样的道理Modifier 用双连字符--目的也是把它和 Block 内的单词连写区分开。card--featured一眼能看出这是 card 的 featured 状态如果写成card-featured看起来反而更像一个独立的 Block 名。双分隔符给了规则可解析性写样式的人能通过类名所属角色决定选择器的嵌套深度工具也能依赖这些符号做快速的归类统计。还有一层重要原因是可检索性。类名一旦统一采用block__elem--mod结构全局搜索就变得非常可靠。要查卡片标题的样式直接搜索card__title要查菜单激活态搜索menu__item--active。相比过去menu active这种模糊搜索这套命名让调试效率提升了不少。2.3 判定原理何时是 Element何时是 Modifier何时该独立成 Block很多新手栽的最后一道坎不是写法记不住而是到底怎么归类。我给团队的判断标准有三条按顺序问自己第一这个类名描述的是一种状态、场景、外观变体还是结构的一部分如果是状态归 Modifier如果是结构的一部分归 Element。比如选中、禁用、展开、推荐都是修饰状态而标题描述按钮图标则是结构组成。第二这个片段脱离父级 Block 之后还有没有独立存在的意义比如卡片里的按钮如果只服务这张卡片不打算被别处复用那就写成.card__btn如果这个按钮在页面里到处出现本身是一个完整组件那就应该拆成独立 Block.btn再在卡片里组合使用。BEM 明确支持 Block 里嵌套 Block这不违反规则。第三DOM 是否对类名有强绑定如果这个元素的位置固定只在某个 Block 结构中出现一次直接用 Element如果它在不同 Block 里都有可能被复用优先考虑 Block。这条标准能有效防止类名无限膨胀。3. 实操一个真实组件的 BEM 改造全过程3.1 案例 HTML一个常见卡片组件的结构先拿最常见的文章卡片当靶子。未用 BEM 前代码可能是这样的div classcard-wrap div classtop img srccover.jpg alt classcover / span classtag热门/span /div div classinfo h2 classarticle-title这里说标题/h2 p classdesc这里是描述/p /div div classbottom a href# classread-more/a /div /div问题一眼就能看出来.top、.info、.bottom都是极高碰撞率的通用词.cover、.tag也是。一旦页面里有别的模块用了同样的名字全局样式很可能会互相干扰。按 BEM 重写后div classcard card--featured div classcard__header img srccover.jpg alt classcard__cover / span classcard__tag热门/span /div div classcard__body h2 classcard__title这里说标题/h2 p classcard__desc这里是描述/p /div div classcard__footer a href# classcard__link card__link--primary/a /div /div这轮改造里我把原来的card-wrap直接命名为cardcard-wrap这种包裹层本身就是一种多余的语义Block 自己就代表最外层的组件容器。top、info、bottom因为是内部结构区域改成card__header、card__body、card__footer结构语义瞬间清晰。cover、tag、title、desc统一挂上card__前缀。在卡片语境里是按钮元素但为了让状态变化清晰我把它拆成.card__link和.card__link--primary前者是结构后者是外观修饰。3.2 CSS 落地没有预处理器的写法也可以干净不引入任何工具纯 CSS 也能把 BEM 写得很规整。最直接的方案是类名即选择器每个类名都是一个独立选择器通过前缀关系控制作用域。.card { border: 1px solid #e5e5e5; border-radius: 8px; overflow: hidden; } .card__header { width: 100%; height: 200px; position: relative; } .card__cover { width: 100%; height: 100%; object-fit: cover; } .card__tag { position: absolute; top: 12px; left: 12px; background: #f05b56; color: #fff; font-size: 12px; padding: 4px 8px; border-radius: 4px; } .card__body { padding: 16px; } .card__title { font-size: 18px; line-height: 1.4; margin: 0 0 8px; } .card__desc { font-size: 14px; color: #666; line-height: 1.6; } .card__footer { padding: 16px; border-top: 1px solid #f0f0f0; display: flex; justify-content: flex-end; } .card__link { display: inline-block; padding: 6px 16px; border-radius: 4px; text-decoration: none; border: 1px solid #ccc; color: #333; font-size: 14px; } .card__link--primary { background: #1a73e8; border-color: #1a73e8; color: #fff; }这种写法的好处是每个选择器都独立不存在嵌套带来的权重踩踏。.card__link--primary和.card__link是两条独立规则谁写在后面谁生效维护时完全可控。坏处也明显一个稍大的组件CSS 行数会涨得比较快。但靠组件本身的作用域换来的可维护性远远高于省下来的那几行代码。3.3 用 SCSS 优化书写体验嵌套与 的取舍如果项目里用了 SCSS大多数人会写成嵌套结构.card { border: 1px solid #e5e5e5; border-radius: 8px; overflow: hidden; __header { width: 100%; height: 200px; position: relative; } __cover { width: 100%; height: 100%; object-fit: cover; } __tag { position: absolute; top: 12px; left: 12px; } --featured { border-color: #f05b56; box-shadow: 0 4px 16px rgba(240, 91, 86, .2); } __link { --primary { background: #1a73e8; border-color: #1a73e8; color: #fff; } } }用拼接后缀最终编译输出的类名依然是card__header、card--featured这些标准 BEM 名但源码结构更紧凑组件的所有类都收拢到一个块下逻辑一眼到底。这里有一个必须重点提醒的坑SCSS 里的嵌套本身会新增后代选择器。也就是说__link--primary虽然编译后类名是.card__link--primary但如果写成.card { .card__header { // 编译后是 .card .card__header } }这会引入额外的后代组合符直接抬高了选择器优先级。不少团队因此踩过坑明明只是想把组件样式收拢结果生成了.card .card__header这种带着祖先嵌套的选择器和组件的全局作用域理念完全冲突。所以用 SCSS 写 BEM 的原则是嵌套只用于生成类名不用于制造后代组合。可以借助at-root强制把生成的规则放到根级也可以直接规范团队__、--是允许的嵌套形式空格嵌套一律禁止。前者能用规则约束后者是纯口头约定真到了执行层面还得靠风格检查工具。4. BEM 高频争议与避坑指南4.1 元素嵌套过深block__elem__sub-elem是个坑新手最容易出现的偏差是看到 DOM 层级就跟着写一个卡片里有头部头部里有图标于是写出了.card__header__icon。这种三层夹心写法不是 BEM 的推荐用法。BEM 的 Element 只解决直接归属于 Block 的某一组成部分它不要求对应完整的 DOM 树深度。到了.card__header__icon这种深度通常的正确做法是再包一层独立 Block或者直接允许更深的层级用block__sub-elem简化。我给出的经验法则是一个 Block 内部的 Element 尽量保持一层。如果发现元素结构超过三层第一步不是继续往下补__xxx而是反问这个深层的东西是不是一个可以复用的独立组件比如卡片里的头像组件如果头像在用户列表、评论区、文章头部都会出现就应该独立成avatarBlock卡片内部用avatar avatar--sm组合而不是card__header__user-avatar一路挂到天荒地老。如果确定只是卡片私有的、不会再被别处复用的片段也可以直接用一个稍长的 Element 名比如.card__user-avatar语义上把它当作 card 的直接组成部分没必要在类名里把所有 DOM 层级都复刻出来。4.2 Modifier 的排列组合爆炸BEM 很爽的一点是状态可控但随之而来的问题是如果每个维度都做成修饰符类的数量会呈指数级增长。以按钮为例光尺寸就有btn--small、btn--large颜色有btn--primary、btn--danger形状有btn--round、btn--square状态有btn--disabled、btn--loading。都写在类名里HTML 会变成一长串button classbtn btn--small btn--primary btn--round这种写法 M 满天飞改起来极容易漏改。我的建议是把修饰符分成语义状态和外观变体两层来处理。语义状态比如选中、禁用、展开、折叠这些与业务逻辑绑定建议用 Modifier 如实写如menu__item--active外观变体比如颜色、尺寸、圆角尽量用 CSS 自定义属性、设计变量或工具类去做而不是每一种组合都生成一份类名。举个实际例子如果主题支持三种品牌色直接把颜色定义在 CSS 变量里再让组件消费变量。把哪个组件、哪个状态交给 BEM 管把长什么样、多大、什么颜色交给 token 层两条线分开后Modifier 组合爆炸的问题基本就消失了。这是我在两个中后台项目里踩了无数次坑后总结出的可行方案。4.3 BEM 与全局工具类如何共存现在原子化 CSS 在社区很流行经常会有同学问用了工具类框架还要不要用 BEM我的看法是二者不是替代关系而是分工关系。BEM 负责组件级封闭作用域工具类负责基础布局、间距、排版单值。一个卡片可以写成div classcard p-4 border rounded-lg img classcard__cover w-full h-48 object-cover ... / /div这里的card、card__cover依然承担组件边界的职责p-4、border、rounded-lg负责单维度的设计属性。这种组合的好处是日常微调不需要去 CSS 文件里翻 Block 的样式直接在 HTML 里改工具类组件特定、复杂的状态逻辑则仍由 BEM 类名固定。坏处是 HTML 标签上的类名变多可读性下降团队需要约定好哪些属性用工具类、哪些属性必须写进 BEM。我个人倾向布局类、间距类、基础排版类用工具类颜色主题、状态过渡、组件骨架结构用 BEM。这个约定能省掉大量样式重复。4.4 常见问题速查表情况错误示范推荐写法理由Block 名中多个单词写成.card-title.card-title单词内连字符允许Block 整体概念保持Element 与 Block 分隔.cardTitle或.card_titlecard__title双下划线统一标识元素归属Modifier 与 Element 分隔card__title-activecard__title--active双连字符与单词连写区分开Element 深层嵌套card__header__iconcard__icon或独立 Blockicon避免类名无限加深多个 Block 组合.btn-primary cardcard card--primary 内部btn组合语义放在 Modifier 或嵌套 Block状态类与业务类混用.card .hide .no-bordercard--hidden/card--no-border状态语义收敛到一个命名空间内这张表基本覆盖了日常 code review 里我点名最多的写法。照着这个方向改样式文件的可读性会有质的提升。5. 从学会到落地风格检查、团队约定与扩展方案5.1 用 stylelint 把命名规范变成可执行规则口头的我们要用 BEM到真正落地之间隔着一道人工审查的鸿沟。团队大起来以后不能指望每个同学都自觉背规范处理方式是把规则写进 stylelint。stylelint 是 CSS 的 lint 工具能校验类名是否符合自定正则。我常用的一段 stylelint 规则大致长这样npm i stylelint stylelint-config-standard -D然后在.stylelintrc.json里添加自定义规则{ extends: stylelint-config-standard, plugins: [stylelint-selector-bem-pattern], rules: { plugin/selector-bem-pattern: { componentName: [a-z][a-zA-Z0-9]*, componentSelectors: ^\\.component__[element]--[modifier]$|^\\.component__[element]$|^\\.component--[modifier]$|^\\.component$ } } }实际使用时组件名、元素名、修饰符名的正则写法可以按团队喜好调整也可以用现成的stylelint-selector-bem-pattern插件来强制。有了这条规则任何写错形状的类名提交前就会在 IDE 里被标红不让错误进入代码库。配上 pre-commit 的 lint-staged 钩子把校验放在提交前效率最高。5.2 从经典 BEM 到变体两下划线的可替代方案经典 BEM 有两套常见变体一是block__element--modifier全量写法二是把 Element 分隔符改成单个下划线的简化 BEM。简化版类名更短输入更快但牺牲了Element 与 Modifier 一眼可区分的优势。我的建议是如果你在做一个多端共用、组件非常多的中大型项目保守选经典 BEM可读性最强如果你在写一个小工具或私人项目想少打两个下划线完全可以用简化版。关键是团队内部达成一致并在 stylelint 里同步固定规则。从工具链角度看这两套变体都能被 stylelint 校验不存在兼容性问题。至于类名长度经典 BEM 确实偏长但现代前端项目大多走构建工具会统一做 postcss 处理最终线上的类名可以被精简。只要源码保持 BEM 结构线上体积的问题基本不用担忧。真正值得优化的不是类名字符长度而是选择器的复杂度和 CSS 总字节数。5.3 团队落地建议把命名规范当成设计系统的一部分最后聊一点组织层面的方法。BEM 落地的最大阻力不是学不会而是团队里没有形成组件边界的共同语言。如果每个人对一个 Block 是什么的定义都不一样BEM 写出来也会五花八门。我给团队推过一套比较实用的流程先挑两个高频组件比如 Button、Card用 BEM 全量重构作为团队的命名标杆再把组件文档化在 code review 时对照文档检查新类名是否符合规则最后把涉及命名冲突、类名过深、状态词滥用的问题汇总成《BEM 命名手册》新同学进来以后先读手册再接手代码。实际操作中还有一个很有效的小动作把 Block 名和组件物理目录名对齐。组件库的文件夹叫card那它的类名前缀就是card文件夹叫search-form类名前缀就是search-form。代码里看到类名能直接映射到文件系统这对跨文件追踪样式非常有帮助。这也是 BEM 长期以来在团队协作里最容易被低估的价值。6. 最后想说的几句实在话BEM 不是银弹它也有自己的问题类名偏长、嵌套结构写起来啰嗦、刚开始团队内部磨合成本高。但在我维护过的项目里那些真正让人崩溃的样式问题几乎都不是 BEM 本身造成的而是没有规则、或者规则执行不严造成的。BEM 的价值在于它给 CSS 提供了一个稳定可预期的边界让陌生代码变得可以读让改样式变成一件能搜得到、找得着、改得动的事情。如果你现在正被项目里的样式混乱折磨第一步可以不用大刀阔斧全局重构找个高频组件试着用 BEM 重写一遍感受一下类名即结构的体验。等团队成员都尝到甜头再把规范逐步铺开。命名这件事没有绝对的唯一解但每条规则都认真执行下去代码库会自己给你反馈。最后分享一个小技巧在 BEM 类名旁边再配一份组件类名清单把每个 Block 下允许出现的 Element、Modifier 列成表格。看起来多了一步工作实际省下的时间不小——新人改样式不需要猜测该用哪个 class直接查清单就能定位。我自己每接手一个新项目第一件事就是看它的 class 命名是否成体系因为从命名风格里基本能判断出这个项目的前端工程化水平。如果你想给团队的样式库建立秩序从 BEM 开始是最稳妥的一刀。
返回列表