
1. 一次样式覆盖事故引发的思考先从一个真实的夜晚说起。前两年我接手一个中后台管理系统代码里同时存在三四个版本的按钮样式.btn躺在公共样式里.el-button是第三方组件库的.nice-btn是某位离职同事自己加的还有一个.button-primary是业务页面里临时塞的。结果就是视觉走查阶段设计提了一堆问题QA 提了一堆 bug我半夜在 devtools 里一层层翻样式来源看哪条规则的优先级更高最后发现是页面同时引了两份样式文件某个 class 名被后者覆盖了。那一刻我真正意识到样式冲突根本上是命名缺乏边界意识造成的必然结果而不是 CSS 的层叠机制有问题。从那时起我开始认真研究 CSS 命名方法论从 OOCSS 到 SMACSS再到 BEM、SUITCSS以及后来的 CSS Modules、CSS-in-JS、原子化 CSS。这篇文章想把这条演进线串起来讲清楚BEM 为什么能成为事实标准它在工程化过程中暴露了什么问题SUITCSS 又是怎么针对这些问题做改良的以及最终在真实项目里如何落地。1.1 事故的根因不是优先级是命名空间失控那次事故的触发点表面上是选择器优先级真正的问题是 class 名没有携带归属信息。.button-primary这个名字你根本看不出它是属于哪个模块、哪个组件、哪个页面于是任何地方都可以引用它、覆盖它。只要两个文件里同时出现同名的 class后加载的那个就赢了——这跟层叠优先级无关纯粹是命名设计的问题。很多人说用 scoped 不就好了但 scoped无论 Vue 的>div classuser-card img classuser-card__avatar src... / div classuser-card__name张三/div button classuser-card__btn user-card__btn--primary关注/button /div对应的 CSS.user-card { border: 1px solid #eee; border-radius: 8px; } .user-card__avatar { width: 48px; height: 48px; border-radius: 50%; } .user-card__name { font-size: 16px; font-weight: 600; } .user-card__btn { padding: 4px 12px; } .user-card__btn--primary { background: #1677ff; color: #fff; }2.1 Block、Element、Modifier 三件套的运行逻辑块是最顶层的抽象代表一个独立组件或功能单元比如user-card、search-box。元素是块内部的组成部分必须依赖块存在写法是块名加双下划线加元素名。修饰符表示状态、主题、尺寸等变化写法是块或元素名加双连字符加修饰名。这套命名的关键点在于它强行要求你回答这个 class 属于谁。看到search-box__input你不会把它当普通 input 去全局复用你会意识到它是搜索框内部的输入框。这就是工程边界的具象化。BEM 同时强调选择器扁平化——理想状态下CSS 里只有单类选择器/* 推荐单类选择器优先级完全可控 */ .search-box {} .search-box__input {} .search-box__input--focused {} /* 不推荐嵌套选择器破坏 BEM 的扁平化意图 */ .search-box .input {}扁平化让优先级计算变成一个可预测的简单问题所有 class 的特异性相同后面的规则覆盖前面的规则仅此而已。这比过去标签选择器加层级嵌套的写法不知道清爽多少。2.2 BEM 是工程化协作的底座BEM 出现之前最常见的写法是.sidebar ul li a这种选择器串。特异性层层叠加想改一个链接颜色可能要加两三个前缀来压优先级最终演化出满屏!important。BEM 把优先级问题、命名冲突问题、结构语义问题打包成一套规则让团队成员在写样式时共享同一套思维模型。两个新人同时开发两个不同组件只要遵守 BEM它们之间的样式几乎不可能互相干扰——因为组件边界在命名上就已经划开了。这也是它能在不依赖构建工具、不依赖特定框架的前提下仅靠命名约定就降低协作成本的原因。对一个从零开始的组件库项目BEM 到今天依然是相当好用的底座。问题是当项目规模继续膨胀BEM 的先天短板会慢慢变成明显的痛点。3. BEM 用久了那些教科书没提的痛点这里不是要吐槽BEM 不好看这种审美层面的事而是从工程可维护性角度讲真实的不适感。我在多个项目里都撞上过同样的墙。3.1 深层嵌套下命名开始失控BEM 原教旨主义只支持一层元素后缀但真实页面结构往往不止两层。比如一个卡片里有一个评论区评论区里有一个操作栏操作栏里有个点赞按钮。你得在三件事里选第一种把所有层级压平写成card__comment-like-btn名字又长又拗口。第二种往下填两层写成card__comment__actions__like这已经脱离了双下划线的原始语义属于自创的扩展 BEM。第三种把评论区独立成另一个块comment-box再互相嵌套——这要求你具备很强的何时拆块的分寸感。真实项目里三种写法往往同时存在代码库就会变成 BEM 方言混杂区。我接手项目时见过card__header__title__text这种四层命名也见过card-header-title-text这种彻底放弃 BEM 的写法。规范一旦不能在嵌套场景下给出清晰指导它就会以肉眼可见的速度瓦解。3.2 全局命名空间天然共享撞名只能事后补救BEM 的块名是全局共享的这个设计在多年前没问题但在组件化时代开始让人别扭。只要项目里有两张卡片设计稿都叫blog-card而细节样式不同冲突马上就来。你只能改其中一个块名比如main-blog-card、mini-blog-card。但改名是发现冲突后的被动补救不是主动预防。组件化框架普及之后一个.vue文件或.tsx文件天然就是一个边界BEM 的全局约束反而显得冗余。更普遍的痛点是当同一结构在不同上下文里要有不同表现BEM 命名表达不出来你得靠一堆状态类去兜底class 就越写越长。3.3 修饰符语法太单一状态与主题混为一谈--active、--selected、--disabled这类状态修饰符和--primary、--large这类主题尺寸修饰符在 BEM 里是同一套语法。但它们的生命周期完全不同状态会随交互变化主题相对固定、伴随设计品牌。混在一起之后一旦需要主题色变化加状态变化叠加命名会膨胀成.btn--primary--active--large而且耦合顺序稍微一变就让人分不清谁修饰谁。SUITCSS 以及后来的多个方案都在这一点上做了细化状态归状态工具类归工具类组件变体归组件变体。4. SUITCSS 对 BEM 的改良结构化命名与心智分层SUITCSS 是 2015 年前后由 Nicole Sullivan 等人推动的一套方法论核心思路是保留 BEM 的 Block-Element-Modifier 分层逻辑但重新设计命名语法让它更适应大规模组件化开发同时把状态类、工具类这些 BEM 没说清的东西单独拎出来定义。4.1 驼峰类名、单连字符和命名空间SUITCSS 把块名改成 PascalCase元素用单连字符连接修饰符继续用双连字符。举个例子div classProfileCard img classProfileCard-avatar src... / div classProfileCard-name张三/div button classProfileCard-btn ProfileCard-btn--primary关注/button /div对应的 CSS.ProfileCard { border: 1px solid #eee; } .ProfileCard-avatar { width: 48px; height: 48px; } .ProfileCard-btn { padding: 4px 12px; } .ProfileCard-btn--primary { background: #1677ff; color: #fff; }这个改动看起来只是大小写加符号的差别实际上解决了不少问题。驼峰让块名成为一个整体标识视觉上不太会被人随意截断拼写。单连字符明确表达后代层级双连字符表达状态变体从语法上就能区分元素和修饰符。更关键的是SUITCSS 支持可选命名空间前缀比如.app-ProfileCard等于在全局命名空间上又加了一层显式的归属声明——这是 BEM 完全没考虑的。4.2 工具类与组件类的分层SUITCSS 的一个设计亮点是把通用工具类单独划出来用u-前缀标记div classu-textCenter button classProfileCard-btn ProfileCard-btn--primary u-mt8 关注 /button /div工具类负责高频复用的单一样式比如文本对齐、字号、间距、显示隐藏组件类负责有结构语义的样式。这个分层最大的价值你不用为了一个margin-top: 8px去新建一个组件子元素也不用从别处复制粘贴同样的间距样式。它和如今流行原子化 CSS 的思路是一脉相承的——先承认存在大量与组件无关的样式原子再用工具类去承载。区别在于 SUITCSS 把工具类视为组件类的补充而不是替代品组件的语义边界依然存在只是把边界里的一部分琐碎样式外包给了工具类。4.3 状态类与修饰符解耦SUITCSS 明确把状态单独处理用is-前缀定义状态类.ProfileCard { opacity: 1; } .ProfileCard.is-hidden { display: none; } .ProfileCard--large { width: 400px; }.is-hidden与组件名连用状态被绑定在组件结构上又不污染原有的修饰符体系。在 BEM 实践中也常有人这么做但 SUITCSS 把规则写进了官方文档让状态是状态修饰是修饰成为团队默认共识而不是靠某个人的自觉。5. 实际选型BEM 和 SUITCSS 到底该用哪个经常有团队在 BEM 和 SUITCSS 之间反复横跳。我的态度是先看团队阶段和既有代码再看具体场景。两者不是对错问题是适配问题。5.1 核心差异速查表维度BEMSUITCSS块名写法全小写连字符连接PascalCase 驼峰元素分隔双下划线__单连字符-修饰符双连字符--双连字符--状态类无独立约定is-前缀工具类无专门约定u-前缀命名空间无内置方案可加.app-前缀组件哲学以页面/区块为中心以组件/模块为中心使用体验上BEM 在小团队、老项目里上手更快因为全小写对大小写零要求后端同事都能写。SUITCSS 更适合组件驱动、多人并行开发的前端工程因为驼峰块名在 JSX、模板、CSS 之间的对应关系更直观——写ProfileCard组件样式文件里就找.ProfileCard几乎不需要翻译。5.2 从 BEM 迁到 SUITCSS 的实操路径真要做迁移别想着一次性全局替换成本极高且收益有限。我用的方式分三步。第一步既有代码继续用 BEM新代码统一 SUITCSS并在组件库文档里写清楚两套规范的边界。第二步把公共工具类抽出来统一u-前缀完成工具类层面的融合。第三步对改动频繁的高频组件顺手做一次重命名重构用 IDE 的重构工具替换 class 名并同步更新 CSS 文件。迁移过程中最常踩的坑是全局替换.user-card__avatar成.UserCard-avatar结果忘了 CSS 文件里还留着旧类名。如果真要做建议先写脚本生成一份新旧类名映射表再逐个文件处理最后用 stylelint 的selector-class-pattern规则兜底——类名不符合 SUITCSS 模式CI 直接报错从根上堵住漏网之鱼。6. 工程化落地把命名规范锁进自动化流程规范如果只靠人肉记忆必然会在某个赶版本的黑夜崩塌。工程化实践的最后一道闸门一定是自动化工具。6.1 CSS Modules 与命名规范的关系在 React 和 Vite 生态里CSS Modules 基本是默认方案。它会把.ProfileCard-avatar编译成带哈希的.ProfileCard-avatar_x2as_1保证全局唯一。它解决的是编译期冲突但源码层的命名语义依然要靠规范约束。CSS Modules 真正的价值在于你可以放心写短名字、语义名不用再做命名空间前缀因为编译器已经帮你隔离好了。实践中有个细节不要用:global随意逃逸局部作用域。每次逃逸都是向全局注入一个类相当于亲手拆掉 CSS Modules 的隔离能力。真要用第三方库样式尽量集中在一个公共文件里处理而不是散落在业务代码各处。6.2 Vue Scoped 场景下的双保险Vue 的 scoped 是另一套隔离机制编译时给元素添加>/^(is|has|u)-[a-z][a-zA-Z0-9]*$|^[A-Z][a-zA-Z0-9]*(-[a-z][a-zA-Z0-9]*)*(--[a-z][a-zA-Z0-9]*)?$/规则看起复杂但效果立竿见影——命名不合规的代码在 lint 阶段就被拦下根本进不了评审。配合selector-max-class、selector-max-id、no-descending-specificity等规则能把优先级失控的概率压到最低。另一个很实用的技巧给 IDE 配 Live Template。输入sb自动展开成 BEM 注释模板输入su展开成 SUITCSS 组件模板。人在赶工期时很容易随手乱写但工具模板能保证肌肉记忆始终是规范的。7. 再往前走一点现代 CSS 工程里命名规范的位置聊到这儿不妨把视野拉高。现在的 CSS 工程已经进入组件即模块、构建工具即隔离层的阶段命名规范的作用正在慢慢变化。原子化 CSS 的流行让一部分团队不再写语义化类名而是直接堆工具类。这是对 BEM/SUITCSS 的一种有效补充甚至在某些场景下可以替代。我的看法是原子化 CSS 解决的是琐碎样式爆炸的问题但它不解决组件边界问题——你依然需要知道哪些工具类组合属于哪个组件。所以不少团队的最终形态是混合使用原子类负责布局和间距组件类负责主题和状态。设计系统层面命名规范也在逐步被 design token 替代。颜色、字号、间距变成了--color-primary、--spacing-md这样的语义变量类名退为次要角色。但 token 本身的命名同样需要分层思想基础 token、组件 token、状态 token本质上还是 BEM 启发的思路。做了这么多年前端我的体感是没有一套规范是永恒的上帝方案。重要的是团队能否就名字必须表达边界这件事达成共识。BEM 简单、直接、普适性极强适合快速启动和中小团队SUITCSS 在结构表达和组件化心智上更优适合规模较大、多人并行的前端工程而无论选哪套最后都必须靠自动化工具兜底。最后分享一个我这些年一直保留的小习惯代码评审时看到不规范的 class 名我通常不直接改而是先问作者——你写这个类名时心里的边界是什么很多次对方答不上来只是顺手复制了上一行。那一刻我才意识到命名规范最大的作用不是让 CSS 变得好看而是逼着每个写样式的人认真思考我写下的到底是什么它的边界又在哪里。