Skip to content

第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 插件的三层架构:cssPlugincssPostPlugincssAnalysisPlugin
  • 预处理器集成的 Worker 池化设计
  • PostCSS 处理管线与插件编排
  • CSS Modules 的作用域隔离实现
  • 开发模式下的 CSS HMR 机制
  • 构建模式下的 CSS 代码分割与资源提取
  • Lightning CSS 作为替代 transformer 的集成方式
  • url()@import 的路径重写策略

CSS 插件的三层架构

CSS 处理不是由一个插件完成的,而是由三个协作的插件组成:

cssPluginvite:css:在用户插件之前执行。负责预处理器编译(Sass -> CSS)和 PostCSS 转换。它的输出是纯 CSS 文本。

cssPostPluginvite:css-post:在用户插件之后执行。将 CSS 文本转换为 JavaScript 模块(开发模式),或提取为独立文件(构建模式)。

cssAnalysisPluginvite:css-analysis:仅在开发模式下生效。负责跟踪 CSS 的 @import 依赖关系,用于 HMR。

这种三层分离的设计有一个重要的原因:用户插件可能需要在预处理和最终输出之间插入自己的 CSS 转换逻辑。如果所有功能都在一个插件中,用户将无法在这些阶段之间介入。

cssPlugin:预处理与 PostCSS

初始化与 Worker 池

cssPluginbuildStart 钩子中初始化预处理器的 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 插件:

基于 VitePress 构建