CSS Modules 与 CSS extract 深入说明

为什么 CSS 复杂

库构建里的 CSS 不只是把 .css 文件复制出去。至少有三类场景:

import './global.css';
import styles from './Button.module.css';
import './theme.scss';

它们的产物语义不同:

  • 全局 CSS 应输出 CSS 文件。
  • CSS Modules 应输出 JS mapping。
  • 预处理器需要经过 loader。
  • CSS 中的 asset url 需要保持资源路径。

Bundleless 下这些差异更明显,因为源码 import 会被保留下来。

CSS Modules 为什么输出 JS

CSS Modules 的 import 值是对象:

import styles from './Button.module.css';

button.className = styles.primary;

所以输出不能只是:

import './Button.module.css';

它需要一个 JS module 导出 class name mapping。因此 bundleless 下 CSS Modules request 会被改写到 JS 扩展名:

import styles from './Button.module.js';

而全局 CSS 才是:

import './global.css';

cssExternalHandler

cssExternalHandler 做请求级判断:

  • css-loader helper 不 external。
  • CSS 文件内部 asset import 不 external。
  • CSS Modules 改成 JS extension。
  • 全局 CSS 改成 .css

它需要知道:

  • jsExtension
  • output.cssModules.auto
  • redirect.style.path
  • redirect.style.extension
  • issuer

这说明 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,例如:

sass-1
less-2

pluginLibCss 中有 isPreprocessorRule 兼容这种后缀。否则在多个预处理器插件共存时,Rslib 可能找不到需要替换 loader 的 rule。

CSS 中 asset url

CSS 中:

.logo {
  background: url('./logo.svg');
}

这个 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-modulesvueassetbundle-false