ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

BEM命名方法实战指南:从入门到落地,打造可维护的CSS类名体系 说实话刚写CSS那几年我最头疼的不是Flex布局调不对也不是浏览器兼容而是打开一个项目看到一摞这样的classdiv classcontent_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 classbutton button--primary提交/buttonbutton是块button--primary是这个块的一个修饰符变体。看到这个类名你就知道这是个按钮、是主色调样式不需要再额外翻CSS注释。再看一个带元素的结构div classcard h3 classcard__title标题/h3 p classcard__desc描述内容/p /divcard__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 classcard div classcard__header h3 classcard__header__title标题/h3 /div /divcard__header__title看似把层级写清楚了实际却是个陷阱。一旦视觉改版标题不再放在header里类名就全乱了。正确的做法是h3 classcard__title标题/h3HTML里的层级关系由DOM结构表达类名只表达“title 属于 card”这一层归属关系。职责归职责嵌套归嵌套两件事别混在一起。这个扁平化原则是BEM在实际项目中减少返工的关键。2.3 Modifier修饰符状态和变体的“开关”修饰符用来表达两类信息一类是外观或行为变体比如大按钮、主按钮、禁用态另一类是特定状态比如 active、disabled、hover。语法上修饰符可以挂在块上也可以挂在元素上/* 块的修饰符 */ .button--primary /* 元素的修饰符 */ .card__title--highlight实操中修饰符最常见的写法是“叠加类”而不是“替换类”。什么意思看下面的对比。!-- 错误替换基础类会丢失块的基本样式 -- button classbutton--primary提交/button !-- 正确基础类加修饰符类叠加生效 -- button classbutton 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 classarticle-card article-card--featured img classarticle-card__cover srccover.jpg altcover / div classarticle-card__body h2 classarticle-card__titleBEM命名方法完全指南/h2 p classarticle-card__summary一套让CSS类名结构化的命名方法论/p div classarticle-card__tags span classarticle-card__tag article-card__tag--tech前端/span span classarticle-card__tag article-card__tag--life生活/span /div div classarticle-card__footer span classarticle-card__meta阅读 2.1k/span button classarticle-card__like-btn点赞/button /div /div /article对比一下传统写法的HTML会是什么样的article classcard featured img classcover srccover.jpg altcover / div classbody h2 classtitle.../h2 p classsummary.../p div classtags span classtag blue前端/span span classtag green生活/span /div /div /article第二版看起来短问题在于.cover在全局里极小概率重名吗不大概率重名。.tag.blue的语义完全靠人脑记忆CSS和HTML的对应关系也是松散的。用BEM版本即使不打开CSS文件每个类名都在自述它是什么、属于谁、处于什么状态。3.3 CSS该怎么写结构样式和变体分离对应HTMLCSS的写法遵循一个原则选择器简单化样式职责单一化。我给出完整示例/* 块的基础样式 */ .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 classflex gap-4 article classarticle-card article-card--featured h2 classarticle-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类和一个可能影响相同属性的工具类比如classcard__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命名方案里最适合团队协作、最经得起时间考验的一套。如果你还没试过挑个实际组件项目练一次手感受过前后对比你就知道这件事的长期价值在哪里了。
返回列表