CSS Modules 与 CSS extract 深入说明
为什么 CSS 复杂
库构建里的 CSS 不只是把 .css 文件复制出去。至少有三类场景:
它们的产物语义不同:
- 全局 CSS 应输出 CSS 文件。
- CSS Modules 应输出 JS mapping。
- 预处理器需要经过 loader。
- CSS 中的 asset url 需要保持资源路径。
Bundleless 下这些差异更明显,因为源码 import 会被保留下来。
CSS Modules 为什么输出 JS
CSS Modules 的 import 值是对象:
所以输出不能只是:
它需要一个 JS module 导出 class name mapping。因此 bundleless 下 CSS Modules request 会被改写到 JS 扩展名:
而全局 CSS 才是:
cssExternalHandler
cssExternalHandler 做请求级判断:
- css-loader helper 不 external。
- CSS 文件内部 asset import 不 external。
- CSS Modules 改成 JS extension。
- 全局 CSS 改成
.css。
它需要知道:
jsExtensionoutput.cssModules.autoredirect.style.pathredirect.style.extensionissuer
这说明 CSS 处理不是独立模块,它依赖 bundleless external 和输出扩展名。
为什么替换 CSS extract loader
默认 CSS extract 是应用构建导向。Rslib 需要更适合库产物的行为:
- 保留相对路径。
- 区分全局 CSS 和 CSS Modules。
- 处理 CSS entry 产生的虚拟 JS asset。
- 在 bundleless 中处理 CSS import 的外部化。
- 支持 banner/footer。
所以 Rslib 用 libCssExtractLoader 替换默认 extract loader,并用 LibCssExtractPlugin 替换默认插件。
css-loader helper 为什么不能 external
css-loader 会生成一些 runtime helper import,例如 noSourceMaps、api 等。CSS extract 在 Node side 通过 importModule 执行这些结果。
如果 Rslib 把这些 helper external 掉,CSS extract 阶段执行会失败。所以 cssExternalHandler 遇到 compiled/css-loader/ 直接放行。
这是典型“看起来像普通 import,但其实是 loader 内部执行依赖”的场景,和 Vue loader helper 有相似性。
全局 CSS entry 的空 JS asset
当一个全局 CSS 文件作为 entry 时,bundler 可能会产生一个对应 JS asset。但对库产物来说,这个 JS asset 没有业务意义。
Rslib 通过 RSLIB_CSS_ENTRY_FLAG 标记全局 CSS entry,之后在 processAssets 阶段删除带标记的 JS asset。
CSS Modules 不删除,因为它的 JS asset 是 mapping。
预处理器 rule id
Rsbuild 可能为多个 sass/less 插件生成带数字后缀的 rule id,例如:
pluginLibCss 中有 isPreprocessorRule 兼容这种后缀。否则在多个预处理器插件共存时,Rslib 可能找不到需要替换 loader 的 rule。
CSS 中 asset url
CSS 中:
这个 asset import 不能被 JS asset preserve 规则处理。CSS url 的相对路径要保持 CSS 语义。因此 assetConfig 会为 CSS issuer 创建单独 asset rule。
修改风险
CSS 子系统改动容易影响:
- React/Vue/Svelte 组件样式。
- CSS Modules import。
- 全局 CSS side effect import。
- CSS 预处理器。
- CSS 中图片和字体。
- bundleless 和 bundle 两条路径。
- banner/footer。
测试应至少覆盖 style/css-modules、vue、asset、bundle-false。