因BEM类名是纯字符串,打包工具不解析CSS AST,且JS动态拼接(如card__body--${status})无法被静态扫描捕获;需JS主动声明合法类名供PurgeCSS消费,并用stylelint校验命名、unocss按需生成。

为什么 CSS 打包阶段无法直接检测未使用的 BEM 类
因为 BEM 类名是纯字符串,打包工具(如 Webpack、Vite)默认只分析 JS 中的 className 引用,对 CSS 文件里的类名不作 AST 解析;cssnano、purgecss 这类工具虽能删未用类,但它们依赖“源码中出现过的类名列表”,而 BEM 的 block__elem--mod 组合常由 JS 动态拼接(如 `card__body--${status}`),静态扫描根本抓不到。
真正可行的检测路径:从 JS 入口反向生成合法类名集合
不能指望 CSS 工具自己猜出哪些 BEM 类该存在,得让 JS 主动“声明”——用函数封装所有合法 BEM 类生成逻辑,再在构建时导出白名单供 PurgeCSS 消费。
- 在项目根目录建
src/bem-classes.js,统一导出所有可生成的类名模式:
export const cardClasses = [
'card',
'card__header',
'card__body',
'card__footer',
...['card--hover', 'card--disabled', 'card--loading'].flatMap(mod => [
`card${mod}`,
`card__header${mod}`,
`card__body${mod}`
])
]
- 在
purgecss配置中通过extractors加载该模块,把cardClasses注入白名单 - 禁止在 JSX/Vue 模板里手写拼接类名(如
className={`card__body--${loading ? 'loading' : ''}`}),必须走封装函数:cn('card__body', { 'card__body--loading': loading }) - 该函数需在构建前被 PurgeCSS 的 extractor 识别到,否则白名单为空
stylelint-selector-bem-pattern 能否替代?不能,它只管“写得对不对”,不管“用没用上”
stylelint-selector-bem-pattern 插件的作用是拦截非法命名(如 .userList),但它对 .card__icon--large 是否在 HTML/JS 中真实使用完全无感。它和“未使用类检测”属于两个维度的问题——一个守入口规范,一个查出口冗余。
- 你配置了
"componentName": "[a-z][a-zA-Z0-9]*",它会拦下.Card__header,但不会告诉你.card__avatar在整个项目里一次都没被引用过 - 若同时想兼顾命名合规与体积精简,必须并行启用两套机制:
stylelint校验写法 +purgecss或unocss扫描实际使用 - 注意:PurgeCSS 默认忽略
@layer和:is()内部类名,BEM 修饰符若写成:is(.card--hover, .card--disabled)就会被漏掉
Unocss 是更现代的选择,但需放弃“全量 BEM 类预定义”思维
unocss 不是从 CSS 文件出发,而是按需生成——你在模板里写 class="card__body card__body--loading",它才生成对应 CSS;没写的类,压根不出现在最终包里。这对 BEM 天然友好,但要求你接受“类名即代码”的开发流。
立即学习“前端免费学习笔记(深入)”;
- 启用
@unocss/preset-bem,配置prefix: 'myapp'和separator: '__',它就能识别myapp__card__body并生成规则 - 动态类名仍需显式注册:在
uno.config.ts的shortcuts里加['card-body-loading', 'myapp__card__body myapp__card__body--loading'] - 最大陷阱:它不会校验你是否误写了
myapp__card__header--large(元素名header实际不存在),这类错误只能靠stylelint-selector-bem-pattern补位


















