唯一稳定生成合法BEM类名的方式是@mixin b()配合@at-root #{&}__,因&始终代表完整选择器而非块名,嵌套或拼接写法在层级变化、伪类或复用时必然崩坏。

直接用 @mixin b() + @at-root #{&}__ 是目前唯一能稳定产出合法 BEM 类名的路径,所有试图靠嵌套或字符串拼接绕过 @at-root 的写法,都会在嵌套层级变化、伪类介入或混入复用时崩坏。
为什么不能直接写 &__element?
SCSS 的 & 永远代表“当前完整选择器字符串”,不是“块名”。在 .card { &__header { } } 中,它看似编译出 .card__header,但本质是 SCSS 把 &__header 当作一个新选择器拼接——这仅在块顶层“碰巧”成立。一旦你加一层 .card { .body { &__title { } } },& 就变成 .card .body,结果是 .card .body__title,完全违背 BEM 扁平类名原则。
-
&:hover { &__icon { } }编译为非法类名.card:hover__icon,浏览器直接忽略 -
@media (min-width: 768px) { .card { &__footer { } } }会把规则塞进媒体查询内部,输出顺序错乱且无法提级 - HTML 中只写
class="card__header"而漏掉card,样式立刻失效——这不是编译问题,是语义断裂
@mixin b() 必须最先定义并绑定 &
b() 不只是生成块名,它的核心作用是初始化 & 的值,让后续 e() 和 m() 能可靠引用。没有它,& 可能指向外层任意父选择器(比如 .sidebar .card),导致 @include e("title") 输出 .sidebar .card__title 或更糟的 .sidebar__title。
- 必须用变量拼接命名空间:
$B: #{$namespace}#{$block-separator}#{$block},避免硬编码 -
@mixin b($block) { .#{$B} { @content; } }这一步必须出现在最外层,不能包裹在其他选择器内 - 禁止在
b()外部再调用e()或m(),否则&无定义
@mixin e($name) 和 @mixin m($name) 必须用 @at-root 提级
这是唯一能确保 .block__element 和 .block--modifier 与块同级输出的方式。任何不带 @at-root 的写法,都会被 SCSS 解释为后代关系,产出带空格的选择器,破坏 BEM 的解耦前提。
立即学习“前端免费学习笔记(深入)”;
@mixin e($name) { @at-root #{&}#{$element-separator}#{$name} { @content; } }@mixin m($name) { @at-root #{&}#{$modifier-separator}#{$name} { @content; } }- 分隔符必须统一定义为变量(如
$element-separator: "__"),不能散落在 mixin 内部 - 禁止在
&:hover、@media或子容器内调用e()/m(),@at-root会脱离上下文,导致 CSS 输出位置不可控
循环批量生成元素类名时,别依赖 &
@each 中的 & 不会随循环项切换,它始终指向循环外最近的父选择器。想生成 .card__header、.card__footer,不能写在 .card { @each ... { &__#{$part} { } } } 里——那会产出嵌套结构混乱的 CSS,且 & 值不可控。
- 正确做法是提前绑定块名:
$block: "card"; @each $part in ("header", "footer") { .#{$block}#{$element-separator}#{$part} { } } - 字符串拼接必须用
+:#{$block} + $element-separator + $part,写成$block$element-separator$part会语法报错 - 如果需要动态命名空间,用
#{$namespace} + "-" + $block + $element-separator + $part
BEM 的解耦效果不来自 SCSS 语法糖,而来自对类名显式归属的强制约束:哪怕编译器输出了正确的 CSS,只要 HTML 没同步写全 card card__title card__title--large,整个链路就断了——这种缺失比逻辑错误更隐蔽,调试时极难定位。


















