唯一安全的bem类名生成方式是用@at-root #{&}__xxx强制提级拼接,禁用嵌套中的&__和&--,所有scss块名需显式传参、分隔符参数化,并与js层bem函数严格对齐。

直接导出扁平类名,不嵌套、不拼接、不依赖 HTML 结构 —— 这是唯一能通过 PurgeCSS、stylelint-selector-bem-pattern 和 VS Code 类名跳转校验的方式。
SCSS 中所有 &__ 和 &-- 必须出现在块作用域顶层
写在子选择器或伪类内部会产出非法类名或空格后代选择器,破坏 BEM 的语义隔离。
-
.card { &__header { } }→ 编译为.card__header✅ -
.card { .header { &__title { } } }→ 编译为.card .header__title❌(不是 BEM 元素) -
.button { &:hover { &__icon { } } }→ 编译为.button:hover__icon❌(浏览器忽略) - 真正安全的修饰符写法:
.button { &--primary { } &--disabled { } }✅
@at-root #{&}__xxx 是生成元素类名的唯一可靠方式
用 &__xxx 看似简洁,但 SCSS 嵌套机制默认加空格;@at-root 强制提级 + 插值才是可控拼接。
- 错误:
.card { &__title { color: #333; } }→ 实际输出.card .card__title(带空格) - 正确:
.card { @at-root #{&}__title { color: #333; } }→ 输出.card__title - 变量块名也适用:
$block: "dialog"; @at-root #{$block}__footer { padding: 12px; } - 禁止在
@media或&:focus内部再用@at-root #{&}__xxx,顺序和语义都会失控
封装 @mixin b() 时必须显式传入块名,禁用隐式 &
混入里用 & 会继承调用上下文,导致 @at-root #{&}__item 拼出 .sidebar .widget__item 这类语义错乱的类名。
- 正确结构:
@mixin b($block) { $B: "ui-" + $block; .#{$B} { @content; } } - 调用必须显式:
@include b("button") { @include element("icon") { … } } -
@mixin element($name)内部必须用@at-root #{$selector + "__" + $name},不能用&__ - 分隔符必须参数化:
$element-separator: "__",避免硬编码导致跨项目迁移困难
导出类名给 JS 使用时,Sass 变量和 JS 函数必须严格对齐
Vue 或 React 组件中动态拼接类名时,若 Sass 和 JS 的命名逻辑不一致,clsx 推导失效、TypeScript 类型报错、PurgeCSS 误删都会发生。
- Sass 层定义常量:
$namespace: "ui"; $element-separator: "__"; - JS 层实现同逻辑函数:
const bem = (block, el?, mod?) => [`${ns}-${block}`, el && `${ns}-${block}${elSep}${el}`, mod && `${ns}-${block}${modSep}${mod}`].filter(Boolean).join(" ") - 禁止在 JS 里手写
"ui-button__icon"—— 一旦 Sass 改了$namespace,JS 就脱节 - Vite 项目需配置
sassOptions.additionalData注入全局变量,确保每个.scss文件都能读到$namespace
最易被忽略的是:BEM 类名不是“字符串拼出来就行”,而是组件边界的契约声明。一个 button__icon 意味着它永远不能脱离 button 单独存在;一旦你在 JS 里允许 iconOnly: true 并渲染 button__icon 而不带 button,整个 BEM 约束就坍塌了 —— 工具链失效、协作断链、重构成本翻倍。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











