purgecss删掉动态拼接的bem类名,因其仅扫描静态字面量,无法识别button--${size}等运行时拼接;须通过safelist正则(如/--(large|small)/)或显式枚举保留。

动态拼接的 BEM 类名为什么被 PurgeCSS 删掉了
PurgeCSS 默认只扫描字符串字面量和模板语法中的类名,而 clsx、classnames 或模板字符串里拼出的 button--${size} 属于运行时行为,构建时无法静态推断。它看到的是变量、表达式,不是确定的 button--small。
这不是 PurgeCSS 的 bug,是设计使然——它不执行 JS,只做文本匹配。一旦你用 cn('button', `button--${variant}`),PurgeCSS 就会把所有没在字面量中出现的 button--* 全部当“未使用”干掉。
- 常见现象:开发时样式正常,build 后
button--large失效,DevTools 里查不到对应规则 - 触发条件:修饰符由变量拼接、数组动态过滤、或通过函数生成(如
getModifier('primary')) - 影响范围:所有基于类名匹配的工具都一样,包括
unocss的 safelist 模式、tailwindcss的content扫描
怎么让 PurgeCSS 保留动态 BEM 修饰符
核心是显式告诉工具:“这些模式我确实用了,别删”。不能靠猜,必须配置 safelist 或正则提取器。
- 用正则匹配所有合法修饰符:
safelist: [/--(primary|secondary|large|small|disabled)/] - 如果修饰符来自枚举对象,直接列全:
safelist: ['button--primary', 'button--secondary', 'button--disabled'] - Vite 用户可在
vite.config.ts的build.rollupOptions.plugins中配@fullhuman/postcss-purgecss插件,并启用defaultExtractor - Webpack 用户建议用
purgecss-webpack-plugin,并确保paths覆盖所有 JSX/TSX 模板文件,否则连字面量都扫不全
CSS Modules 下动态类名的安全写法
即使开了 CSS Modules,手拼字符串仍会绕过构建层的类名管理,导致哈希名和源码类名脱节。真正安全的方式是让构建工具全程掌控。
- 禁止:
className={`button ${variant ? 'button--' + variant : ''}`}——button--primary是字面量,但button--${variant}不是 - 推荐:
clsx(styles.button, styles[`button--${variant}`])——styles是 CSS Modules 导出的对象,所有键都经构建处理,button--${variant}在编译期就被展开为真实哈希键 - 更稳做法:把所有可能的修饰符提前定义在 CSS 文件里,哪怕没在当前组件用,也写上
.button--primary {} .button--large {},再用safelist保底 - 注意:
:global()内的类名不会进styles对象,动态引用会undefined,这类情况必须走safelist
为什么加前缀(如 myapp-button--large)反而让问题更隐蔽
加项目前缀本身不解决动态类名识别问题,还可能让正则匹配失效。比如你配了 /--(large|small)/,但实际类名是 myapp-button--large,正则就对不上了。
- 前缀应由构建工具自动注入(如
css-loader的modules: { auto: true, exportLocalsConvention: 'camelCaseOnly' }),而不是手写 - 若必须加前缀,
safelist正则要同步更新:/myapp-button--(primary|large)/ - 最常被忽略的一点:前缀加在构建层,但
safelist配置在打包层,两者不同步就会漏匹配——务必确认 PurgeCSS 运行在 CSS 已加前缀之后
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











