1. 命名规范这件事,为什么值得单独用一整期来聊
先讲个真实的经历。前两年我接手过一个H5营销活动项目,页面不算复杂,四个活动页一个抽奖转盘,但打开那个项目的CSS文件时,我整个人是懵的。一个类名叫 .box 的样式出现了十二次,.content 出现了九次,.red 这种用颜色命名的类名也有七八处。更离谱的是,有些地方把 style 直接写在 HTML 标签上,有些地方用 !important 硬怼。第一个需求改下来,我光是排查样式互相覆盖就花了半天时间,改完一个按钮的颜色,另一个页面的列表背景跟着变了。
这个经历不是个例。CSS选择器的命名看起来是最不需要动脑筋的活儿,随手写个 .a、.b、.div1 也能跑,但它恰恰是前端项目里最容易积累技术债的地方。业务代码可以靠组件化、模块化来约束,JavaScript 有 eslint 和各种 lint 工具兜底,唯独 CSS 的命名,在当前的大多数工程体系里几乎没有强约束。等到项目上线三个月,需求迭代了七八轮,新来的同事在样式文件里滚三屏都找不到自己想要的类名时,你才会意识到:命名规范不是锦上添花,而是保证项目能持续迭代的基本前提。
这一期笔记就来把 CSS 选择器命名这件事讲透。我会从命名规范背后的设计思路讲起,对比主流的命名方案,再给出一个在 H5 项目里真正能落地的实践路线。无论你是刚入行的新人,还是被项目里混乱的样式折磨过的老手,这篇文章都值得看完,至少能帮你少踩几个坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 主流命名方案怎么选:BEM、OOCSS、SMACSS还是混合制
2.1 三种常见命名体系的设计逻辑
CSS 命名规范发展到现在,比较有影响力的有三套体系:OOCSS、SMACSS 和 BEM。很多新人一听到这些英文缩写就头大,其实它们解决的核心问题是一致的:让类名能表达“这个元素是什么、在哪儿、状态如何”,而不是让类名成为一个随机的标签。
OOCSS,也就是面向对象的 CSS,核心思想是把视觉样式拆成可复用的对象。举个例子,一个按钮可能有蓝色、红色、大号、小号几种变体,按照 OOCSS 的思路,可以拆成 .btn(基础样式)和 .btn-blue、.btn-large(修饰样式)这样两层结构。这套方案的优点是复用性极强,缺点是类名会比较碎,一个元素上可能要挂四五个类名,视觉上有点吓人。
SMACSS 的核心理念是给样式分层,把 CSS 规则分成基础、布局、模块、状态、主题五类,每一类有对应的命名前缀。比如布局类用 .layout- 开头,状态类用 .is- 开头。这套方案在大型项目中很实用,因为它给了团队一个清晰的分类框架,写样式时先想“这个规则属于哪一层”,再去动手。
BEM 是这三套里传播最广的,全称是 Block Element Modifier,即块、元素、修饰符。它的核心思想是:页面由独立的块(Block)组成,块内部可以有元素(Element),块或元素可以有修饰符(Modifier)来表示不同状态或变体。典型的写法是 .block__element--modifier,比如 .card__title--highlight。这套方案把选择器的命名空间管理做到了极致,一个类名就能完整表达它在结构中的位置和状态。
2.2 为什么H5项目里我偏向使用BEM的变体
在实际的 H5 项目里,我最终选的是 BEM 的简化变体。原因很直接:H5 项目通常是组件驱动的,一个页面由若干个区块拼起来,BEM 的“块”和组件的边界天然吻合。你写一个 Vue 组件,组件名是 Card,那这个组件根节点的类名就对应 BEM 里的块,组件内部的标题、描述、按钮就是元素,它们的类名自然是 .card__title、.card__desc、.card__btn。这种对应关系是另外两套规范很难提供的。
但我不完全照搬 BEM,而是做了一些简化。原版 BEM 要求每个元素都写完整的 .block__element 前缀,这在嵌套很深的组件里会导致类名非常长。比如卡片里有个列表项,列表项里又有个链接,按标准 BEM 得写 .card__list-item-link,一串下来又长又丑。我在实际项目里允许两级以内的缩写,如上图这种嵌套结构可以写成 .card__item 加 .card__link,同时在注释里标明层级关系。这需要团队约定,但换来的阅读体验是值得的。
2.3 选择命名方案的判断标准
命名方案没有绝对的好坏,关键是和你的项目形态匹配。我根据自己的实践经验,整理了三个判断标准:
第一个标准是团队规模。两三个人的小项目,只要大家口头约定好类名别太随意就行,复杂的规范反而是负担;但如果是十人以上的前端团队,或者项目要长期维护、多人协作,就必须有书面化的命名规范。
第二个标准是技术栈。如果你用原生 CSS 写多页面站点,SMACSS 的分层思路会很舒服;如果你用 Vue、React 这种组件化框架,BEM 的块元素模型几乎是天然契合的。
第三个标准是样式复用的频率。如果你的项目里大量存在按钮、表单、弹窗这种复用组件,OOCSS 的组合式类名能显著减少重复代码;如果项目是偏内容展示的,每个页面结构差异大,BEM 的独立命名空间反而更清爽。
注意,这三个标准不互斥,很多成熟团队是把它们混着用的。我自己现在的方案是“BEM 为主体,状态类借鉴 SMACSS 的
.is-前缀,通用原子类借鉴 OOCSS 的思想”,这样既保证了组件边界的清晰,又兼顾了状态管理和高频样式的复用。
3. 核心实操:一套可以直接落地的命名规范
3.1 块、元素、修饰符的划分规则
纸上谈兵没有意义,直接来看我在项目里实际执行的规则。
块的划分是最关键的决策。我给的边界定义是:能独立复用的完整视觉单元就是一个块。一个按钮是一个块,一个卡片是一个块,一个导航栏是一个块,但一个卡片内部的标题文字不是块,它是元素。块的生命周期和组件对齐——在 Vue 里,一个 .vue 文件对应一个块;在 React 里,一个组件对应一个块。这保证了 CSS 命名和组件结构之间的映射关系,开发时看到类名就能知道它属于哪个组件。
元素的划分标准是:只属于这个块、离开块就没有独立意义的内部节点。卡片的标题、图片、描述文字,都是 .card 这个块的元素。有一个容易混淆的地方:子组件里的结构算不算父组件的元素?我的约定是不算。子组件是独立的块,即使它在视觉上位于父组件内部,它依然拥有自己的命名空间。比如卡片里放了一个按钮组件,按钮的类名应该是 .btn 而不是 .card__btn。
修饰符的使用是 BEM 里最容易被玩坏的部分。我的规则是:修饰符只用来表达视觉状态的变化,比如尺寸大小、颜色主题、是否禁用,不允许用修饰符去覆盖结构样式。结构差异应该拆成不同的元素或者新的块,而不是同一元素加了个修饰符就完全变形。举例来说,卡片有两种布局,一种是横排图文,一种是竖排图文,这属于结构差异,应该拆成 .card--horizontal 和 .card--vertical 两个修饰符,但对应的内部元素命名可能需要重新设计,而不是硬套同一套元素类名。
3.2 修饰符怎么用才能避免权重灾难
修饰符的适用伴随着一个隐秘的陷阱——权重问题。.card__title--highlight 这个类名的权重是 0,1,0,和 .card__title 一样,所以当两者同时出现在一个元素上时,谁在 CSS 文件里靠后谁就生效。这种同权重的覆盖规则其实很优雅,不会产生权重叠加的问题。
但如果有人图省事,把修饰符写成 .card .highlight 或者 .card__title.highlight,权重就变成 0,2,0,此时想要恢复默认样式就得再加一个更高权重的选择器,久而久之就是一场灾难。我在团队里定了一条死规矩:修饰符必须独立作为一个类名挂在元素上,禁止使用后代选择器或交集选择器来构造修饰符。这条规则执行到位之后,样式覆盖的问题减少了九成以上。
另一个和修饰符相关的常见问题是“修饰符中嵌套修饰符”。比如按钮的基本样式是 .btn,激活状态是 .btn--active,但如果是主按钮的激活状态,有人会写 .btn--primary--active。这种写法在 BEM 规范里是不推荐的。正确做法是把激活状态视为一种通用状态,配合 SMACSS 的状态类写法,用 .btn--primary.is-active 来表达。状态类独立于块和修饰符,只通过 JS 动态切换,这样即便有一天主按钮的激活样式变了,也只需要改状态类的规则,不用去翻修饰符那层。
3.3 状态类和业务逻辑类怎么与JS解耦
状态类和业务逻辑类的问题在很多项目里是被忽视的。最常见的情况是:写 React 或 Vue 组件的时候,业务逻辑需要一个 isOpen 类来切换显隐,于是直接在模板里写了 <div class="isOpen">,然后在 CSS 里给 isOpen 定了样式。过两周需求变了,另一个组件也需要一个 isOpen 状态,两个组件都叫 isOpen,但样式需求不一样,于是 CSS 里出现了 .isOpen 的两条定义,互相覆盖。
我在实践中的经验是:凡是只用于 JS 操作、不需要独立视觉样式的类,统一用 js- 前缀,并且不在 CSS 文件里给它们写任何样式规则。比如轮播图切换需要查找 .js-slider-item 来管理索引,这个类在 CSS 里完全不存在,它只是给 JS 用的“钩子”。这样一来,CSS 类名和 JS 类名的职责就彻底分开了:CSS 类名负责视觉呈现,js- 类名负责行为绑定,互不干扰。
对于确实需要视觉反馈的状态,比如选中、激活、完成,使用 .is-active、.is-disabled 这类语义化状态类,配合块的修饰符使用。这类类名的样式规则在 CSS 里必须紧跟在对应的块规则之后,保持代码阅读的连贯性。我在团队规范里甚至限定了一条:状态类的样式只允许用类选择器来写,不允许写 .card.is-active 这种后代组合,直接把权重固定在 0,1,0,避免出现权重打架。
3.4 组件化框架下的选择器命名实战
Vue 和 React 的流行给选择器命名带来了一个微妙的变量:scoped 样式。很多新手的认知是“用了 scoped 就可以随意命名了,反正编译时会加上哈希”。这个理解是有误区的。
scoped 样式确实给选择器加了属性选择器,让样式只作用于当前组件,但它并没有解决命名可读性的问题。你在模板里写了一个类名叫 .content,编译后虽然被限定在当前组件内,但代码里到处都是含义模糊的 .content,换个人来维护根本不知道每个 .content 是干嘛的。而且 scoped 样式在遇到子组件根节点穿透、第三方组件样式覆盖这些场景时,反而会因为属性选择器的存在引入新的坑。
所以在组件化框架里,我依然坚持 BEM 命名。举个例子,我在 Vue 里写一个商品卡片组件,模板长这样:
html复制<template>
<div class="prod-card">
<img class="prod-card__thumb" :src="product.cover" alt="商品封面" />
<div class="prod-card__info">
<h3 class="prod-card__name">{{ product.name }}</h3>
<p class="prod-card__desc">{{ product.desc }}</p>
<span class="prod-card__price">{{ product.price }}元</span>
</div>
</div>
</template>
这种情况下,即使不用 scoped,类名的命名空间也足够防止样式泄露到别的组件。我的判断标准是:如果你每写一个组件都要依赖 scoped 才能保证样式不互相污染,说明类名本身的唯一性是不够的;反过来,如果你的类名都遵循了 BEM 规范,scoped 只是一个万无一失的保险,而不是唯一的依靠。
4. H5项目特有的命名陷阱与优化技巧
4.1 移动端适配方案对命名的隐性约束
H5 项目普遍会用到 rem、vw/vh 或者 viewport 缩放来适配不同尺寸的屏幕。这些适配方案的背后有个容易被忽略的点:动态计算的根字体大小或者视口单位会影响某些布局的写法,进而影响命名。
用 rem 适配的时候,很多人会把根元素的字体大小和页面里组件的尺寸混在一起,结果出现这样的代码:组件根节点上的 class 是 .page-wrapper--small,里面用 rem 定义宽度,但另一个完全不相干的组件也用了同样的 rem 基准,于是一个组件的尺寸调整连带影响了全局。这个问题的本质不是 rem 的错,而是类名没有把“这是布局容器”这个意图表达清楚。
我的经验是,在命名层面单独划分一类布局类,用 .layout- 前缀做标识。比如 .layout-main、.layout-sidebar、.layout-grid,这些类只负责页面的整体骨架,不掺杂任何组件级的视觉细节。尺寸单位的选择虽然和命名不直接相关,但这种分类让团队的认知更清晰:看到 layout- 开头就知道是全局布局,不敢随便动;看到 prod-card 就知道是组件内部的事情,改起来心里有底。
另外,H5 页面的点击区域普遍要求不小于 44x44 像素,这个约束也会影响命名。一个按钮的尺寸需要由外部容器控制,因为不同页面里同样的按钮大小可能不一样。我用修饰符来处理这种场景,默认按钮是 .btn,占据大半屏的按钮写 .btn--block,小尺寸的写 .btn--sm。尺寸的变化通过修饰符来表达,而不是每次都在外层包一个新容器类。
4.2 多端复用场景下的选择器策略
现在很多 H5 项目不是单端的,同样的页面可能跑在微信浏览器、百度小程序内嵌 H5、App 内嵌 WebView 里。这些环境对 CSS 的支持程度有差异,比如某些 WebView 的 position: sticky 表现不一致,或者 100vh 在移动端浏览器里会计算错。这虽然不是命名规范能直接解决的,但命名方式会影响你在多端问题上的排查效率。
我在团队里做了一个小的约定:所有为特定环境做的兼容类统一用环境前缀标识。比如 .wx-fix-bottom 表示针对微信浏览器做的底部悬浮适配,.app-safe-area 表示针对 App 内 WebView 的安全区域适配。这样在排查问题时,一眼就能看出哪些样式是环境专用的,不会误删正常逻辑的样式。
另外,多端项目的埋点需求特别多。产品经理要统计某个按钮的点击量、某个 banner 的曝光量,通常做法是在元素上绑定自定义属性或者加一个埋点用的类名。我的建议是埋点钩子统一用 data- 自定义属性,比如 data-track="home-banner-click",不要用 CSS 类来承载埋点逻辑。原因是埋点的标识经常是后端下发的字符串,格式不一定符合 CSS 类名的命名规则,而且埋点类多了之后会污染 CSS 文件的语义。
4.3 从H5性能角度看命名对样式计算的影响
部分开发者会关心类名的长短会不会影响 CSS 解析性能。说实话,类名多几个字符对性能的影响可以忽略不计,真正影响性能的是选择器的复杂性。
但是命名规范和性能之间存在一个间接关系:好的命名规范会自然引导开发者写出更短、更扁平的选择器。遵循 BEM 之后,你会发现大部分选择器都可以写成单个类名,几乎不会出现 .page .content .wrapper .card .btn 这种六级嵌套。嵌套少了,浏览器的样式匹配计算自然就快了。
在我优化过的 H5 页面里,有一个用百度小程序 WebView 加载的活动页,原本 CSS 文件里有个选择器写了八层嵌套,页面在低端安卓机上滚动时有明显的掉帧。排查后发现是某次样式覆盖时为了压过前面的规则,用大长链的方式加权重,越加越大。后来我们按 BEM 重构了那一块的类名,把所有嵌套选择器都拍平成了单类名,掉帧问题明显缓解。这不是命名规范直接带来的性能提升,但它改变了写代码的方式,让你不会走上嵌套选择器的歪路。
5. 常见问题与排查技巧实录
5.1 样式莫名其妙不生效的排查思路
项目里最常遇到的情况是:明明写了一个类名,浏览器里就是不生效,打开 DevTools 一看,样式被划掉了。排查这个问题,我有一套固定的流程。
第一步看权重。用计算面板检查实际生效的规则,如果发现冲突规则来自另一个类名,检查两个类名的权重是否相同。如果相同,看谁在样式表里靠后;如果不同,大概率是有人把选择器写复杂了,比如用了 #id 或者加了 .parent .child 这种结构。
第二步查命名空间。如果项目用了 BEM,检查是不是块名撞了。比如两个不同的页面里都有 .card,你们在工程里没有做页面类的前缀隔离,就极易产生跨页面的样式污染。解决方法是给每个页面或模块加一个顶层类,比如 .page-home .card 或者干脆在组件类名里加模块前缀。
第三步看编译。工程化项目里样式是经过打包工具处理的,有时本地开发环境正常,上了测试环境就出问题。这时候要检查是不是有样式被 tree-shaking 之类的优化过程干掉了,或者 CSS Modules 的类名映射出了问题。别一上来就怀疑命名不规范,先确认编译链路是完整的。
5.2 类名太长导致代码体积膨胀,需要担心吗
BEM 的完整写法确实会带来较长的类名,比如 .mod-article-comment-list__item--highlight,一个类名 50 个字符,一个组件里出现十几二十次也是常事。有些团队会因为这个问题放弃 BEM,我觉得这是因小失大。
先算一笔账:一个普通的 H5 项目,模板加样式里的类名总量大约在几千到上万次。假设 BEM 比短命名平均每个类名多 20 个字符,总字符数增加两三十 KB。如果走 HTTP 压缩,这些重复文本的压缩率极高,实际多出来的传输量可能只有几 KB。这在一个动辄几百 KB 的页面面前,占比微乎其微。
真正值得关心的是构建后的代码能不能被高效压缩。很多工程化项目会用 CSS Modules 把类名编译成短哈希,但这种情况下的可读性就留给了源码文件,编译产物的长度问题不再是问题。如果你是在纯 HTML 里手工写类名,那才需要权衡一下类名长度和可读性的关系。我的建议是:追求语义完整,但允许对高频的固定词汇使用缩写,比如 btn 代替 button、desc 代替 description,同时维护一份缩写词典,保证团队的认知一致。
5.3 团队规范落地时的两个心得
规范好不好,不是写出来就完了,关键在于能不能真正落到代码里。我经历过的失败案例是:团队定了一份 20 页的命名规范文档,没人看,也没人执行,最后形同虚设。后来我调整了落地策略,有三点心得值得分享。
第一点是规范要配工具。纯靠人自觉在 review 代码时一个一个看类名,效率太低。可以用 stylelint 配置选择器命名规则,比如强制类名使用 kebab-case,禁止使用 id 作为样式钩子,这样大部分命名问题在代码检查阶段就能暴露。团队规范里只写机器检查不了的内容,比如“修饰符只表达视觉状态”这种语义层面的约定。
第二点是先用代码评审守住第一道线。尤其是新项目启动的前两三个月,一定要有经验比较丰富的人重点盯样式文件的评审。这个阶段的每一行 CSS 都会成为后续开发的模板,前面烂了后面全跟着烂。前几周捂得紧一点,后面就省心了。
第三点是定期做“样式债”清理。哪怕规范执行得再好,也会有一些历史遗留或者紧急修复留下的坏味道。每个迭代或者每个月,抽出半个小时到一小时,专门检查一下样式文件里有没有超长选择器、有没有非规范类名、有没有重复率极高的样式块。清理这种债比写新功能还要值。
最后分享一点个人心得
写 CSS 和写任何代码一样,命名是最能体现工程素养的地方之一。一个 .card--active 摆在那里,谁都能看出它的含义;一个 .a1 扔在那里,除了作者没人知道它是干嘛的。命名规范的价值不是给代码找麻烦,而是给代码找一个稳定的表达框架。我在实际项目里用 BEM 变体这套打法已经两三年了,踩过坑,也调整过细节,现在的状态是:新同学入职基本一天就能上手,改需求的时候能直接定位类名,不用全局搜索碰运气。如果你现在的项目还深陷类名泥潭,强烈建议从下一个需求开始,试着用这套思路重构一个组件,感受一下“看到一个类名就知道它的结构和状态”是种什么体验。
