Appearance
第10章 CSS 处理引擎
开篇引言
plugins/css.ts 是 Vite 整个代码库中最大的单一文件——超过 3500 行代码。这不是偶然的。CSS 处理面临的复杂性远超 JavaScript:预处理器(Sass、Less、Stylus)、PostCSS 生态、CSS Modules、Lightning CSS、url() 路径重写、@import 内联、source map 合并、开发模式的 HMR 注入、构建模式的代码分割和资源提取——所有这些功能都汇聚在一个插件中。
更挑战的是,CSS 在开发模式和构建模式下的处理逻辑截然不同。开发模式下,CSS 被转换为注入 <style> 标签的 JavaScript 模块,支持 HMR 热替换;构建模式下,CSS 被提取为独立文件,支持代码分割和压缩。这两条路径共享预处理和 PostCSS 管线,但在输出阶段完全分叉。
本章将拆解这个庞大文件的内部架构,理解每一层处理的设计意图。
本章要点
- CSS 插件的三层架构:
cssPlugin、cssPostPlugin、cssAnalysisPlugin - 预处理器集成的 Worker 池化设计
- PostCSS 处理管线与插件编排
- CSS Modules 的作用域隔离实现
- 开发模式下的 CSS HMR 机制
- 构建模式下的 CSS 代码分割与资源提取
- Lightning CSS 作为替代 transformer 的集成方式
url()和@import的路径重写策略
CSS 插件的三层架构
CSS 处理不是由一个插件完成的,而是由三个协作的插件组成:
cssPlugin(vite:css):在用户插件之前执行。负责预处理器编译(Sass -> CSS)和 PostCSS 转换。它的输出是纯 CSS 文本。
cssPostPlugin(vite:css-post):在用户插件之后执行。将 CSS 文本转换为 JavaScript 模块(开发模式),或提取为独立文件(构建模式)。
cssAnalysisPlugin(vite:css-analysis):仅在开发模式下生效。负责跟踪 CSS 的 @import 依赖关系,用于 HMR。
这种三层分离的设计有一个重要的原因:用户插件可能需要在预处理和最终输出之间插入自己的 CSS 转换逻辑。如果所有功能都在一个插件中,用户将无法在这些阶段之间介入。
cssPlugin:预处理与 PostCSS
初始化与 Worker 池
cssPlugin 在 buildStart 钩子中初始化预处理器的 Worker 池:
typescript
export function cssPlugin(config: ResolvedConfig): Plugin {
let preprocessorWorkerController: PreprocessorWorkerController | undefined
return {
name: 'vite:css',
buildStart() {
moduleCache = new Map<string, Record<string, string>>()
cssModulesCache.set(config, moduleCache)
preprocessorWorkerController = createPreprocessorWorkerController(
normalizeMaxWorkers(config.css.preprocessorMaxWorkers),
)
},
buildEnd() {
preprocessorWorkerController?.close()
},
// ...
}
}preprocessorMaxWorkers 默认值为 true,表示使用 CPU 核心数减 1 个 Worker。这对大型项目中大量 Sass/Less 文件的并行编译至关重要——预处理器通常是 CPU 密集型的操作。
CSS 编译管线
compileCSS 是整个 CSS 处理的核心调度函数。它编排了从源文件到最终 CSS 的完整流程:
typescript
async function compileCSS(
environment: PartialEnvironment,
id: string,
code: string,
workerController: PreprocessorWorkerController,
urlResolver?: CssUrlResolver,
) {
const lang = CSS_LANGS_RE.exec(id)?.[1] as CssLang | undefined
const deps = new Set<string>()
// 第一阶段:预处理器
let preprocessorMap: ExistingRawSourceMap | undefined
if (isPreProcessor(lang)) {
const preprocessorResult = await compileCSSPreprocessors(
environment, id, lang, code, workerController,
)
code = preprocessorResult.code
preprocessorMap = preprocessorResult.map
preprocessorResult.deps?.forEach((dep) => deps.add(dep))
}
// 第二阶段:CSS 转换器
const transformResult = await (
config.css.transformer === 'lightningcss'
? compileLightningCSS(environment, id, code, deps, ...)
: compilePostCSS(environment, id, code, deps, lang, ...)
)
// 合并 source map
return {
...transformResult,
map: config.css.devSourcemap
? combineSourcemapsIfExists(cleanUrl(id),
transformResult.map, preprocessorMap)
: { mappings: '' },
deps,
}
}预处理器集成
预处理器的调用通过 compileCSSPreprocessors 函数封装。每种预处理器有独立的解析器配置:
typescript
async function compileCSSPreprocessors(
environment: PartialEnvironment,
id: string,
lang: PreprocessLang,
code: string,
workerController: PreprocessorWorkerController,
) {
const { preprocessorOptions, devSourcemap } = config.css
const atImportResolvers = getAtImportResolvers(
environment.getTopLevelConfig(),
)
const opts = {
...((preprocessorOptions && preprocessorOptions[lang]) || {}),
filename: cleanUrl(id),
enableSourcemap: devSourcemap ?? false,
}
const preProcessor = workerController[lang]
const preprocessResult = await preProcessor(
environment, code, config.root, opts, atImportResolvers,
)
if (preprocessResult.error) {
throw preprocessResult.error
}
// 过滤自引用依赖
let deps: Set<string> | undefined
if (preprocessResult.deps.length > 0) {
const normalizedFilename = normalizePath(opts.filename)
deps = new Set(
[...preprocessResult.deps].filter(
(dep) => normalizePath(dep) !== normalizedFilename,
),
)
}
return { code: preprocessResult.code, map: ..., deps }
}@import 解析器
每种 CSS 方言有专属的路径解析器,配置了不同的文件扩展名和 package.json 条件:
typescript
function createCSSResolvers(config: ResolvedConfig): CSSAtImportResolvers {
return {
get css() {
return createBackCompatIdResolver(config, {
extensions: ['.css'],
mainFields: ['style'],
conditions: ['style', DEV_PROD_CONDITION],
tryIndex: false,
preferRelative: true,
})
},
get sass() {
return createBackCompatIdResolver(config, {
extensions: ['.scss', '.sass', '.css'],
mainFields: ['sass', 'style'],
conditions: ['sass', 'style', DEV_PROD_CONDITION],
tryIndex: true,
tryPrefix: '_', // Sass 的 partial 文件约定
preferRelative: true,
skipMainField: true,
})
},
get less() {
return createBackCompatIdResolver(config, {
extensions: ['.less', '.css'],
mainFields: ['less', 'style'],
conditions: ['less', 'style', DEV_PROD_CONDITION],
tryIndex: false,
preferRelative: true,
})
},
}
}注意 Sass 解析器的 tryPrefix: '_' 配置——这支持了 Sass 的 partial 文件命名约定(以下划线开头的文件不会被单独编译)。
PostCSS 处理
compilePostCSS 函数编排 PostCSS 的插件链。它会根据需要动态加载三类 PostCSS 插件: