BEM通过命名约定显式声明作用域,避免深层后代选择器因DOM结构调整而失效、语义模糊、性能下降及协作混乱等问题。

为什么 .header .nav .item .link 这类选择器越来越难维护
深层后代选择器本质是把 DOM 结构耦合进了 CSS,一旦 HTML 层级微调(比如加个 div 包裹、抽离组件),样式就悄无声息地失效;更麻烦的是,它无法体现语义归属——你根本看不出这个 .link 到底属于导航、还是用户菜单、还是页脚。
- 浏览器匹配时需逐层向上回溯,层级越深性能损耗越明显(尤其在动态插入节点后重排)
- 多人协作中,不同开发者对“该不该加一层 wrapper”没有共识,导致同类模块的 DOM 深度不一致
- 使用 CSS-in-JS 或 Shadow DOM 时,这类选择器直接失效或需要额外穿透配置
用 BEM 命名直接替代后代关系表达
BEM 不是强制规范,而是用命名约定显式声明作用域:一个块(Block)内部的元素(Element)和修饰符(Modifier)靠名字本身关联,不再依赖 HTML 层级。
-
.nav是块,.nav__item和.nav__link是它的元素,中间双下划线__就是“属于”的语法糖 - 即使
.nav__link在 DOM 中被嵌套进 5 层div,只要 class 名不变,样式就稳住 - 避免用
.nav .link这种模糊写法——.link可能来自其他块,冲突风险高
示例对比:
/* ❌ 后代模式,脆弱 */
.header .nav .item .link { color: blue; }
<p>/<em> ✅ BEM 模式,明确且解耦 </em>/
.header { }
.nav { }
.nav<strong>item { }
.nav</strong>link { color: blue; } /<em> 不依赖父级是否存在,也不关心它在 DOM 里嵌多深 </em>/BEM 实际落地时最常踩的三个坑
不是起对名字就完事,关键在约束力和一致性。
立即学习“前端免费学习笔记(深入)”;
- 别把 Block 名起成泛义词:
.list、.box、.container—— 它们缺乏上下文,容易跨模块复用错,应改为.product-list、.sidebar-box - Element 不能嵌套 Element:
.nav__item__icon是反模式,正确是.nav__item+.nav__item-icon(同一 Block 下平级) - Modifier 必须依附于 Block 或 Element:
.nav--vertical可以,但单独写.vertical就失去语义,也难定位来源
已有项目怎么渐进式迁移到 BEM
全量重命名不现实,优先从高频修改、多人共用、易出问题的模块切入。
- 先扫描 CSS 文件中出现 3 次以上的深层选择器,比如
.modal .content .form .input,把它对应模块定为迁移目标 - 新写的组件/样式,一律用 BEM 命名;旧代码不动,但禁止新增同类后代选择器
- 用 ESLint 插件
stylelint-selector-bem-pattern检查新提交的 CSS,拦截非 BEM 写法 - 注意:BEM 不解决样式隔离,若需更强封装,得配合
scoped(Vue)或:host(Shadow DOM)
BEM 的核心不是让 class 名变长,而是让每个 class 的职责和归属一目了然。真正难的不是命名规则,是团队在每次加 wrapper、抽子组件、改结构时,都愿意多想半秒:“这个 class 该属于谁?”










