news 2026/10/3 7:07:07

BEM命名方法实战指南:从入门到落地,打造可维护的CSS类名体系

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
BEM命名方法实战指南:从入门到落地,打造可维护的CSS类名体系

说实话,刚写CSS那几年,我最头疼的不是Flex布局调不对,也不是浏览器兼容,而是打开一个项目看到一摞这样的class:

<div class="content_box contentBox2 f-l clearfix none p-10">

每个英文单词拆开都能看懂,合在一起完全不知道这个元素的职责是什么。更崩溃的是,想改一个按钮样式,搜“btn”能搜出几百条结果,根本分不清哪个是哪个。后面接触了BEM命名方法,才发现自己一直缺的不是CSS技巧,而是一套给类名“定规矩”的方法论。这篇文章我就把自己从BEM入门到实际落地踩过的坑、总结出的经验,完整梳理一遍。

BEM是Block(块)、Element(元素)、Modifier(修饰符)三个英文单词的缩写。简单说,它是一套让CSS类名自带“结构说明书”的命名规范,核心语法长这样:

.block__element--modifier

这个方法论最早由俄罗斯的Yandex团队提出,用于解决大型项目样式难以维护的问题。现在你能看到的大多数主流组件库,底层类名都脱胎于这套思路。适合刚入门前端想建立工程化习惯的同学看,也适合已经在业务项目里被命名问题折磨、想给团队定规范的人参考。

1. BEM命名方法的核心理念与语法规则——为什么类名需要“说明书”

1.1 一句话理解BEM:给类名画一张结构图

BEM的核心思想是把页面拆成三层角色:块是独立、可复用的组件单元,元素是块的组成部分,修饰符控制状态和外观变化。三者组合成一个类名,就能在一行代码里看到完整的信息。

我常用一个生活化的类比来理解:写快递地址。块是“城市”,元素是“街道和门牌号”,修饰符是“备注:放快递柜”。如果不写清楚,快递小哥找不到门;类名如果不写清楚,三个月后的你自己也找不到那段样式在哪。

用一个具体例子来说:

<button class="button button--primary">提交</button>

button是块,button--primary是这个块的一个修饰符变体。看到这个类名,你就知道:这是个按钮、是主色调样式,不需要再额外翻CSS注释。

再看一个带元素的结构:

<div class="card"> <h3 class="card__title">标题</h3> <p class="card__desc">描述内容</p> </div>

card__title里的双下划线表示:title 这个元素“属于” card 这个块,没有独立存在意义。整个卡片的结构在HTML层面就已经可视化地表达出来了。

1.2 为什么偏偏用双下划线和双中划线

第一次看到.block__element--modifier这种写法,很多人第一反应是“太长了”或者“好丑”。但Yandex团队选这两个符号是有讲究的。

单下划线_在类名里一般是“单词连接符”,比如content_box,它表示“这是两个词组成的一个名字”。如果BEM再继续用单下划线去区分块和元素,就会非常混淆:

/* 到底是“内容盒子”还是“内容这个块的盒子”? */ .content_box

双下划线__是一个足够醒目的分隔标记。同理,单词之间的连接符用中划线-,比如my-button,而修饰符用双中划线--和块元素区分开。这样在看到类名的一瞬间,就能准确判断哪部分是块名、哪部分是元素名、哪部分是修饰符,三条信息不会互相污染。

从工程实践角度看,这种符号选择还有一个现实好处:搜索非常方便。在代码库里搜button--primary,精确命中目标;搜card__title,不会再混进别的card_title。我用Chrome DevTools调试的时候深有体会,BEM命名的元素,Elements面板里一扫就能定位结构,省去大量翻DOM的时间。

1.3 命名即文档:调试和维护里的隐形收益

类名写清楚了,能省下什么?省下“文档同步”这件事。

传统命名方式下,一个组件改版,HTML结构调整了,但CSS里还是老类名,两边的对应关系很容易断掉。BEM把结构关系织进了类名本身,HTML和CSS之间的对应是“强绑定”的。比如看到HTML里card__footer这个类,CSS那边就必须有这个类名的独立样式块;看到card--featured,就知道这是一个变体样式。哪边漏了,一眼就能发现。

这点在接手别人的项目时尤其明显。我一个同事曾经接手过一个外包项目,原开发用.box1 .box2 .contentright这种命名,他花了两周才理清样式脉络。后来我们用BEM重构了一版,结构清晰到产品经理都能对着HTML猜出页面布局。这套命名方法,本质上是在为团队降低沟通成本。

2. Block、Element、Modifier的划分边界与设计要诀

2.1 Block块:独立可复用的“零件”

块是页面里能独立存在的最小复用单元。判断标准很简单:这个组件脱离当前页面环境,放到另一个页面里还能正常工作吗?如果能,它就应该是一个块。

实际项目里常见块有很多:按钮、导航栏、轮播图、分页器、搜索框、表单、弹窗内容区。块的名字要体现它的职责,而不是它的外观。red-button不如primary-button,因为前者描述的是“当前长什么样”,后者描述的是“它承担什么角色”。外观会变,角色一般不会。

命名块的时候有一个常见误区:把位置信息写进块名。left-sidebar-card就是反面教材——今天它可能确实在左边栏,明天UI改版挪到右边,类名就要跟着改一遍,自找麻烦。正确的做法是叫profile-card,它是一张“用户资料卡”,放在哪由布局层决定,不由组件自己决定。

2.2 Element元素:属于块的“内部件”

元素是块的组成部分,脱离块它没有任何意义。卡片里的标题、图片、正文、按钮,导航里的logo、菜单项、链接,这些都是典型的元素。

BEM对元素的写法是扁平化的:块名加双下划线加元素名。这里有一个非常重要的实操细节:不管元素在DOM里嵌套多深,类名里都不需要体现“层中层”的关系。

举个反例和正例。页面结构是这样:

<div class="card"> <div class="card__header"> <h3 class="card__header__title">标题</h3> </div> </div>

card__header__title看似把层级写清楚了,实际却是个陷阱。一旦视觉改版,标题不再放在header里,类名就全乱了。正确的做法是:

<h3 class="card__title">标题</h3>

HTML里的层级关系由DOM结构表达,类名只表达“title 属于 card”这一层归属关系。职责归职责,嵌套归嵌套,两件事别混在一起。这个扁平化原则,是BEM在实际项目中减少返工的关键。

2.3 Modifier修饰符:状态和变体的“开关”

修饰符用来表达两类信息:一类是外观或行为变体,比如大按钮、主按钮、禁用态;另一类是特定状态,比如 active、disabled、hover。

语法上,修饰符可以挂在块上,也可以挂在元素上:

/* 块的修饰符 */ .button--primary /* 元素的修饰符 */ .card__title--highlight

实操中,修饰符最常见的写法是“叠加类”,而不是“替换类”。什么意思?看下面的对比。

<!-- 错误:替换基础类,会丢失块的基本样式 --> <button class="button--primary">提交</button> <!-- 正确:基础类加修饰符类,叠加生效 --> <button class="button button--primary">提交</button>

对应的CSS是这样:

.button { padding: 8px 16px; border: none; border-radius: 4px; } .button--primary { background: #1677ff; color: #fff; }

基础类负责公共结构,修饰符类只负责差异项。这样写的好处是,新增一个变体只需要加一个.button--danger、.button--success,完全不用动基础类。这是BEM和面向对象思路很接近的地方:公共部分抽出来,差异部分靠组合。

还有一个判断技巧:修饰符是布尔型还是键值型。布尔型直接写--disabled,表示“有没有这个状态”;键值型写成--theme: dark或.card--theme-dark,表示“这个状态的值有多种选项”。前者适合开关类状态,后者适合多主题这类枚举场景。

2.4 粒度判断经验:怎么决定一个元素是块还是元素

这是BEM最容易纠结的地方,我要重点说说我的判断维度。

我自己的经验是看复用范围。一个部分如果只服务于当前组件内部,那就是元素;如果它可能被多个不相关的场景复用,那就应该提成独立的块。

举个实际例子。一个文章卡片里有“点赞按钮”,我开始时把它写成了card__like-btn,后来发现用户动态列表、评论区都要用这个点赞按钮,只是容器不同。那这个按钮就应该独立成块.like-btn,卡片、列表、评论三个场景各自引用它。

另一个例子是“壳”。一个模态框组件,它可能有标题、内容、脚部,这些都是modal__title、modal__content、modal__footer元素。但如果模态框的内容本身是一张复杂的用户表单,这个表单就应该独立成块.user-form,而不是modal__form。因为表单可能被用在非模态框的场景里。

我有一个简单口诀:组件内部的小部件,先按元素写;发现第二个地方要用,立刻提成块。不用一开始就追求完美,重构的成本并不高。

3. 完整实操:用BEM重写一个前端页面组件

3.1 案例目标和分析步骤

纸上谈兵没意思,我拿一个真实业务组件走一遍完整流程。假设我们要做一个“文章卡片”,包含封面图、标题、摘要、标签列表、底部操作区(阅读量和点赞按钮),还要支持一个“精选”高亮变体。

拿到设计稿以后,我的分析步骤一般是固定的三步。

第一步,找块。文章卡片作为一个整体可以被列表页、推荐位、搜索结果页复用,所以它本身是一个块,命名为.article-card。

第二步,找元素。卡片内部有封面article-card__cover、标题article-card__title、摘要article-card__summary、标签容器article-card__tags、底部操作区article-card__footer。

第三步,找修饰符。精选卡片有特殊高亮边框,用article-card--featured。标签有不同类型,比如技术、生活,用article-card__tag--tech这类元素修饰符来区分颜色。

这里有个容易犯的错误:直接把底部操作区里的点赞按钮叫article-card__like-btn。还是我前面说的那个原则,点赞按钮如果只在卡片里出现,可以这么写;如果其他地方也要用,独立成.like-btn块。我在这个例子里假设它只在卡片内用,就保留为元素。

3.2 HTML结构该怎么写

清晰类名对应的HTML长这样:

<article class="article-card article-card--featured"> <img class="article-card__cover" src="cover.jpg" alt="cover" /> <div class="article-card__body"> <h2 class="article-card__title">BEM命名方法完全指南</h2> <p class="article-card__summary">一套让CSS类名结构化的命名方法论</p> <div class="article-card__tags"> <span class="article-card__tag article-card__tag--tech">前端</span> <span class="article-card__tag article-card__tag--life">生活</span> </div> <div class="article-card__footer"> <span class="article-card__meta">阅读 2.1k</span> <button class="article-card__like-btn">点赞</button> </div> </div> </article>

对比一下传统写法的HTML会是什么样的:

<article class="card featured"> <img class="cover" src="cover.jpg" alt="cover" /> <div class="body"> <h2 class="title">...</h2> <p class="summary">...</p> <div class="tags"> <span class="tag blue">前端</span> <span class="tag green">生活</span> </div> </div> </article>

第二版看起来短,问题在于:.cover在全局里极小概率重名吗?不,大概率重名。.tag.blue的语义完全靠人脑记忆,CSS和HTML的对应关系也是松散的。用BEM版本,即使不打开CSS文件,每个类名都在自述它是什么、属于谁、处于什么状态。

3.3 CSS该怎么写:结构样式和变体分离

对应HTML,CSS的写法遵循一个原则:选择器简单化,样式职责单一化。我给出完整示例:

/* 块的基础样式 */ .article-card { border: 1px solid #e5e6eb; border-radius: 8px; overflow: hidden; background: #fff; transition: box-shadow 0.2s; } /* 元素的独立样式 */ .article-card__cover { width: 100%; height: 200px; object-fit: cover; } .article-card__title { font-size: 18px; font-weight: 600; margin-bottom: 8px; } .article-card__summary { font-size: 14px; color: #666; line-height: 1.6; } .article-card__tags { margin: 12px 0; } .article-card__tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; background: #f2f3f5; } /* 修饰符:差异化样式 */ .article-card--featured { border-color: #1677ff; box-shadow: 0 4px 12px rgba(22, 119, 255, 0.15); } .article-card__tag--tech { background: #e6f4ff; color: #1677ff; } .article-card__tag--life { background: #f6ffed; color: #52c41a; }

注意几个细节。修饰符类选择器都放在结构样式后面,优先级相同的情况下,后者覆盖前者;基础类结构不能随便删属性,因为修饰符只依赖覆盖。还有就是CSS的引入方式上,我建议一个组件跑一个独立文件,用外链方式按需加载,不要在全局样式表里堆上千行组件样式。这样组件不用的页面不会加载多余CSS,维护边界也清楚。

组件级CSS文件里的类名,天然被限定在组件内部引用,不会和全局公共样式互相污染。这就是BEM和CSS工程化结合的基本玩法。

3.4 嵌套和层级:BEM是不是禁止写后代选择器

很多人误以为BEM不能写后代选择器,必须保证所有选择器都是单类名。这是天大的误解。BEM追求的是类名语义不依赖嵌套,但CSS选择器里偶尔的后代选择器完全可以使用。

什么情况适合用后代选择器?在一个块的作用域内,临时调整所有子元素的样式,用:

.article-card:hover .article-card__title { color: #1677ff; }

这比给title单独再写一个--hover修饰符清晰得多。但要注意控制嵌套深度,建议最多不超过两层。一旦CSS选择器嵌套超过四层,可读性和性能都会下降,通常意味着设计本身就过于复杂了。

从HTML结构上讲,BEM真正推荐的是“类名扁平化”,而不是“DOM扁平化”。DOM该嵌套就嵌套,类名始终保持 block 和 block__element 两层语义就够。

4. BEM与前端生态的配合:预处理器、CSS Modules与组件库

4.1 BEM加SCSS:用嵌套语法减少重复前缀

原生CSS写BEM有个痛点,article-card__title、article-card__footer里面的article-card反复写,代码看着累。引入SCSS之后,可以用&符号把重复前缀抹掉:

.article-card { border: 1px solid #e5e6eb; &__title { font-size: 18px; } &--featured { border-color: #1677ff; } &:hover &__title { color: #1677ff; } }

编译出来的结果和手写长类名一模一样,但源码的维护体验好很多。这里有一个我踩过的坑:不要为了图省事,把修饰符和元素写成“嵌套加深”的方式,比如:

.article-card { &__footer { &__like-btn { // 这个会被编译成 .article-card__footer__like-btn } } }

前面说过,元素扁平化是原则,编译出的长串命名会让结构信息失真。SCSS只是帮你少写字,不是帮你重新发明命名结构。

对于顶级块的边界,还有一招@at-root可以把某些样式“踢”出作用域,不过一般业务项目用不上,知道有这回事就行。

4.2 在React和Vue组件化项目中怎么用BEM

现代前端开发几乎都是组件化开发了,BEM和组件化天生合拍。一个组件对应一个块,组件内部的所有元素都挂在块名下,组件的props或state转换成修饰符类名。

在React里,我通常结合classnames这个库来写:

import classNames from 'classnames'; function Button({ type = 'default', size = 'md', disabled = false }) { const className = classNames( 'button', `button--${type}`, `button--${size}`, disabled && 'button--disabled' ); return <button className={className} disabled={disabled}>{children}</button>; }

Vue的话可以直接用数组和对象语法,逻辑是一样的。

这里我建议优先用BEM原生类名加CSS Modules的局部作用域组合。为什么不用纯CSS Modules?因为CSS Modules默认生成的混淆类名,会丢失BEM带来的“类名即结构文档”的价值,调试的时候看不到语义。我的习惯是:保留BEM语义类名,再用:local限制作用域,鱼和熊掌兼得。

4.3 和原子化CSS、Tailwind的关系:不是替代关系

这几年原子化CSS热度很高,尤其是Tailwind,把很多样式拆成flex、p-4、text-center这样的小工具类。热词里提到的“原子性css”就是这个方向。我经常被问BEM和Tailwind哪个好,我的看法是:它们根本不是同一层的东西。

BEM解决的是“类名的语义边界”——这个类代表什么组件、什么元素、什么状态。Tailwind解决的是“样式属性的快速组合”——间距、布局、字号这类一次性的视觉调整。

实际项目里,我的组合策略是:布局和间距这类通用样式用工具类,比如排列卡片的外层容器;组件内部的结构样式和状态变体用BEM类。比如:

<div class="flex gap-4"> <article class="article-card article-card--featured"> <h2 class="article-card__title">...</h2> </article> </div>

这样既不会让BEM类膨胀到几十个,也不会因为纯工具类导致完全不知道DOM表达什么业务含义。BEM在大型业务系统、设计系统组件库里依然值得用,因为那些场景需要稳定的可维护的命名契约。

5. 常见问题与避坑指南——实战中的经验总结

5.1 类名太长、太啰嗦怎么办

BEM最被吐槽的就是命名长,缩进一多,HTML里全是长类名。我的处理方式有四种。

一种是用SCSS嵌套减少源码里的重复,刚才说过。第二种是块名的单词精简,article-card不写成article-information-card-container,能一眼看懂就是合适的长度。第三种是修饰符上涨时定义简写,但团队要约定好,比如--sm、--lg、--primary作为标准枚举。第四种是,关键边界在于:相同语义的修饰符尽量复用,不要一个组件一套新词。

有一点要说清楚,类名长不太影响性能。类名长度和CSS选择器匹配性能的关系,在现代浏览器里基本可以忽略不计,业务项目真正的瓶颈不在这。别为了省几个字符牺牲可读性。

5.2 BEM和工具类、公共类并存会冲突吗

并存本身没冲突,就怕没有边界。我见过一个项目,全局工具类.mt-20、.clearfix满天飞,组件类名和工具类交织在一起。

我的解决思路是这样:工具类只用于布局和不可复用的临时调整,组件内的元素样式全部由BEM类负责。禁止在组件内部给同一个元素同时挂BEM类和一个可能影响相同属性的工具类,比如class="card__title text-lg"这种组合,一旦样式顺序不稳定,就是给排查问题挖坑。

真出现冲突时,优先级规则要统一:工具类放前,组件类放后,或者反过来,但整个项目必须一个标准。最省心的做法是写一条校验规则到ESLint里,限制工具类和BEM类不能同时出现在同一元素的class列表里。

5.3 团队规范怎么落地,而不是写在文档里吃灰

BEM规范光有一篇Markdown文档是不够的,落地要配合工具和流程。

我团队里的实际组合是:约定规范文档加Stylelint插件校验类名格式,再加Code Review把关边界划分。Stylelint可以配置selector-class-pattern,强制类名匹配BEM的正则,不通过直接报错,从工具层面卡住格式。Code Review阶段主要看语义划分:某个部分是块还是元素、修饰符写得合理不合理。

还有个小技巧:新成员入职的前两周,要求他们写组件前先画一个“类名清单”(就是先列出要用的所有类名),评审通过再写代码。这个习惯能让人强迫自己思考组件结构,而不是边写边乱起名。

5.4 从面试角度看BEM:常见面试题怎么答

既然很多前端岗位面试会问命名规范,我顺手把这个问题梳理一遍,也算给正在准备面试的朋友一个参考。

面试官问BEM,往往不只看你会不会写,还看你对“为什么需要规范”有没有体感。回答框架可以是:

  • 为什么需要:大型项目多人协作,类名冲突和维护成本会指数级上升,需要一套语义化、结构化的命名约定。
  • BEM解决什么:把页面拆成块、元素、修饰符三层,让类名自带归属和状态信息,可读性高、复用性好、调试成本低。
  • 具体语法和例子:现场写出按钮的.button.button--primary结构。
  • 短板:类名偏长、粒度划分有主观性,需要团队共同维护约定。
  • 演进:结合SCSS减少重复,结合CSS Modules做作用域隔离,或者对比CSS-in-JS方案看适用场景。

回答的时候如果能带上真实项目里“命名混乱到重构”的案例,会比背诵定义打动人得多。面试官不是要你背标准答案,是看你对工程问题的真实思考。

最后再分享一点个人习惯。我在写任何组件之前,会先花一两分钟把设计稿里的结构在纸上拆成“块、元素、修饰符”三列清单,写清楚再开代码。这个习惯帮我在项目后期省下了大量改类名的时间。BEM不是银弹,但它是我用过的CSS命名方案里,最适合团队协作、最经得起时间考验的一套。如果你还没试过,挑个实际组件项目练一次手,感受过前后对比,你就知道这件事的长期价值在哪里了。

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

STM32F217ZG+DRV8818PWPR步进电机控制方案与工程实践

STM32F217ZG 配 DRV8818PWPR 这套组合&#xff0c;是我在定位平台和机器人项目里用得比较多的一套步进电机控制方案。一个负责出脑子&#xff0c;一个负责出大力&#xff1a;STM32F217ZG 作为主控生成 STEP/DIR 脉冲并跑加减速逻辑&#xff0c;DRV8818PWPR 作为专用步进驱动芯片…

作者头像 李华
网站建设 2026/10/3 7:06:08

基于DRV8818与MKV46的双极步进电机驱动控制方案详解

1. 项目背景与核心方案拆解1.1 双极步进电机在工业与机器人场景中到底难在哪双极步进电机和单极电机最大的区别在于绕组结构&#xff1a;双极电机每组绕组只有两根线&#xff0c;驱动时必须由H桥电路换向&#xff0c;让电流可以正反两个方向流过绕组。这意味着驱动器至少要两个…

作者头像 李华
网站建设 2026/10/3 7:05:31

STM32F722VE联合DRV8818PWPR:双极步进电机驱动与运动控制实战

干了几年运动控制&#xff0c;双极步进电机这条线我一直很喜欢用 DRV8818PWPR 搭配 STM32F722VE 来推。前者是 TI 的老牌双极步进驱动芯片&#xff0c;HTSSOP-16 封装&#xff0c;带 PWM 电流斩波、细分和完整保护&#xff1b;后者是 Cortex-M7 内核、主频 216MHz 的 MCU&#…

作者头像 李华
网站建设 2026/10/3 7:04:46

DRV8818+PIC18F46K40双极步进电机控制方案全解析

去年给一台小型桌面机器人换运动控制系统时&#xff0c;我选了DRV8818PWPR加PIC18F46K40的组合来控制双极步进电机。当时的场景很典型&#xff1a;电机是额定电流 1A、步距角 1.8 的 42 步进&#xff0c;供电 24V&#xff0c;要求能走梯形加减速、支持微步细分、体积和成本还不…

作者头像 李华
网站建设 2026/10/3 7:03:25

基于DRV8818与TM4C1299的双极步进电机工业控制方案详解

这几年做工业运动控制和机器人相关项目&#xff0c;打交道最多的执行机构就是双极步进电机。很多人一上来就选伺服&#xff0c;但真到量产、成本敏感、对精度要求又不是伺服级的那种设备里&#xff0c;步进电机配一颗靠谱的驱动器&#xff0c;反而是最稳的答案。今天把我实际跑…

作者头像 李华