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 插件:

一个关键的优化:当文件不包含 @import、不包含 url()、不是 CSS Module、且没有用户配置的 PostCSS 插件时,整个 PostCSS 处理会被跳过:

typescript
if (
  lang !== 'sss' &&
  !postcssConfig &&
  !isModule &&
  !needInlineImport &&
  !hasUrl
) {
  return  // 直接返回 undefined,跳过 PostCSS
}

postcss-import 的深度集成

当 CSS 文件包含 @import 时,Vite 使用 postcss-import 插件来内联导入的文件。但 Vite 对其进行了深度定制:

typescript
postcssPlugins.unshift(
  (await importPostcssImport()).default({
    async resolve(id, basedir) {
      // 优先检查公共目录
      const publicFile = checkPublicFile(id, config)
      if (publicFile) return publicFile

      // 使用 Vite 的解析器(而非 postcss-import 默认的 Node resolve)
      const resolved = await atImportResolvers.css(
        environment, id, path.join(basedir, '*'),
      )
      if (resolved) return path.resolve(resolved)

      return id
    },

    async load(id) {
      const code = await fs.promises.readFile(id, 'utf-8')
      const lang = CSS_LANGS_RE.exec(id)?.[1]
      if (isPreProcessor(lang)) {
        // 如果 @import 引入的是 Sass/Less 文件,先编译
        const result = await compileCSSPreprocessors(
          environment, id, lang, code, workerController,
        )
        result.deps?.forEach((dep) => deps.add(dep))
        return result.code
      }
      return code
    },

    nameLayer(index) {
      return `vite--anon-layer-${getHash(id)}-${index}`
    },
  }),
)

这里有三个重要的定制:

  1. 路径解析:使用 Vite 自己的解析器替代 postcss-import 默认的 resolve 包,支持 Vite 的别名和条件导出
  2. 预处理器嵌套:当 CSS 文件 @import 一个 .scss 文件时,会先通过 Sass 预处理器编译后再内联
  3. Layer 命名:为匿名 @layer 生成基于文件哈希的稳定名称

URL 重写

UrlRewritePostcssPlugin 是 Vite 自定义的 PostCSS 插件,负责重写 CSS 中的 url()image-set() 引用:

typescript
const UrlRewritePostcssPlugin: PostCSS.PluginCreator<{
  resolver: CssUrlResolver
  deps: Set<string>
  logger: Logger
}> = (opts) => {
  return {
    postcssPlugin: 'vite-url-rewrite',
    Once(root) {
      const promises: Promise<void>[] = []
      root.walkDecls((declaration) => {
        const isCssUrl = cssUrlRE.test(declaration.value)
        const isCssImageSet = cssImageSetRE.test(declaration.value)
        if (isCssUrl || isCssImageSet) {
          promises.push(
            rewriteCssUrls(declaration.value, replacer)
              .then((url) => { declaration.value = url })
          )
        }
      })
      if (promises.length) return Promise.all(promises)
    },
  }
}

URL 重写的逻辑需要处理多种边界情况:外部 URL 和 data URL 需要跳过,已经是 Vite 占位符的 URL(_​_VITE_ASSET_​_)也需要跳过。对于带引号和不带引号的 URL,替换逻辑需要正确处理转义:

typescript
async function doUrlReplace(rawUrl, matched, replacer, funcName = 'url') {
  let wrap = ''
  const first = rawUrl[0]
  let unquotedUrl = rawUrl
  if (first === '"' || first === "'") {
    wrap = first
    unquotedUrl = rawUrl.slice(1, -1)
  }
  if (skipUrlReplacer(unquotedUrl)) return matched

  // 移除转义序列以获取实际文件名
  unquotedUrl = unquotedUrl.replace(/\\(\W)/g, '$1')

  let newUrl = await replacer(unquotedUrl, rawUrl)

  // 新 URL 可能需要引号包裹(包含空格或特殊字符)
  if (wrap === '' && (newUrl !== encodeURI(newUrl) || newUrl.includes(')'))) {
    wrap = '"'
  }
  return `${funcName}(${wrap}${newUrl}${wrap})`
}

CSS Modules

CSS Modules 通过 postcss-modules 插件实现。当文件名匹配 .module.css(或 .module.scss 等)模式时,自动启用作用域化处理:

typescript
const cssModuleRE = new RegExp(`\\.module${CSS_LANGS_RE.source}`)

if (isModule) {
  postcssPlugins.unshift(
    (await importPostcssModules()).default({
      ...modulesOptions,
      getJSON(cssFileName, _modules, outputFileName) {
        modules = _modules  // 捕获类名映射
        if (modulesOptions?.getJSON) {
          modulesOptions.getJSON(cssFileName, _modules, outputFileName)
        }
      },
      async resolve(id, importer) {
        // 使用 Vite 的解析器处理 composes 引用
        for (const key of getCssResolversKeys(atImportResolvers)) {
          const resolved = await atImportResolvers[key](
            environment, id, importer,
          )
          if (resolved) return path.resolve(resolved)
        }
        return id
      },
    }),
  )
}

CSS Modules 的类名映射(例如 { title: '_title_1x2y3' })被缓存在 cssModulesCache 中,供 cssPostPlugin 在后续阶段生成 JavaScript 导出代码。

cssPostPlugin:输出阶段

cssPostPlugin 是 CSS 处理的"分叉点"——开发模式和构建模式在这里走上完全不同的路径。

开发模式:CSS 转 JS

在开发模式下,CSS 被转换为注入样式的 JavaScript 模块。这是 Vite "一切皆模块"理念的体现:

typescript
if (config.command === 'serve') {
  if (isDirectCSSRequest(id)) {
    return null  // 直接 CSS 请求,不转换
  }
  if (inlined) {
    return `export default ${JSON.stringify(css)}`
  }
  if (this.environment.config.consumer === 'server') {
    return modulesCode || 'export {}'  // SSR 不需要注入样式
  }

  const cssContent = await getContentWithSourcemap(css)
  const code = [
    `import { updateStyle, removeStyle } from ${clientPath}`,
    `const __vite__id = ${JSON.stringify(id)}`,
    `const __vite__css = ${JSON.stringify(cssContent)}`,
    `updateStyle(__vite__id, __vite__css)`,
    // CSS Modules 导出类名映射;非 Module 的 CSS 可以自接受 HMR
    `${modulesCode || 'import.meta.hot.accept()'}`,
    `import.meta.hot.prune(() => removeStyle(__vite__id))`,
  ].join('\n')
  return { code, map: { mappings: '' }, moduleType: 'js' }
}

这段生成的代码包含了完整的 HMR 生命周期:

  • updateStyle:创建或更新 <style> 标签
  • import.meta.hot.accept():声明自接受热更新
  • import.meta.hot.prune():模块被卸载时移除样式

CSS Modules 的情况有所不同——它们不能自接受 HMR,因为类名映射的变化需要通知所有引用方。此时由 cssAnalysisPluginupdateModuleInfo 中设置 isSelfAccepting: false

typescript
const isSelfAccepting =
  !cssModulesCache.get(config)?.get(id) &&
  !inlineRE.test(id) &&
  !htmlProxyRE.test(id)

构建模式:代码分割与提取

构建模式下的 CSS 处理复杂得多。cssPostPlugin 需要处理多个环节:

transform 阶段,CSS 内容被记录到 styles Map 中,返回的 JavaScript 代码可能是:

  • CSS Module:返回 modulesCode(类名映射的 export
  • 内联 CSS(?inline):返回 export default "css content"
  • 普通 CSS:返回空字符串,但设置 moduleSideEffects: 'no-treeshake' 防止被 tree-shaking 删除
typescript
// 构建模式的 transform
if (!inlined) {
  styles.set(id, css)  // 记录 CSS 内容
}

let code: string
if (modulesCode) {
  code = modulesCode
} else if (inlined) {
  let content = css
  if (config.build.cssMinify) {
    content = await minifyCSS(content, config, true)
  }
  code = `export default ${JSON.stringify(content)}`
} else {
  code = ''  // 空模块,但不会被 tree-shake
}

return {
  code,
  moduleSideEffects: modulesCode || inlined ? false : 'no-treeshake',
  moduleType: 'js',
}

renderChunk 中的 CSS 收集

renderChunk 钩子是构建模式 CSS 处理的核心。它遍历 chunk 的所有模块,收集关联的 CSS,然后决定如何输出:

typescript
async renderChunk(code, chunk, opts, meta) {
  let chunkCSS: string | undefined
  let isPureCssChunk = chunk.exports.length === 0

  for (const id of Object.keys(chunk.modules)) {
    if (styles.has(id)) {
      if (transformOnlyRE.test(id)) continue  // ?transform-only 跳过

      // 检查 cssScopeTo:CSS 是否只在特定导出使用
      const cssScopeTo = this.getModuleInfo(id)?.meta?.vite?.cssScopeTo
      if (cssScopeTo && !isCssScopeToRendered(cssScopeTo, renderedModules)) {
        continue  // 如果关联的导出未被使用,跳过此 CSS
      }

      if (cssModuleRE.test(id)) {
        isPureCssChunk = false  // CSS Module 包含 JS 代码
      }

      chunkCSS = (chunkCSS || '') + styles.get(id)
    }
  }
  // ...
}

如果启用了 cssCodeSplit(默认),每个 chunk 对应的 CSS 会被单独输出为一个 .css 文件。如果禁用了代码分割,所有 CSS 会合并为一个文件。

纯 CSS Chunk 的清理

当一个 JavaScript chunk 只包含 CSS 导入(没有 JS 导出)时,它被标记为"纯 CSS chunk"。generateBundle 阶段会清理这些空的 JS 文件:

typescript
if (pureCssChunks.size) {
  const replaceEmptyChunk = getEmptyChunkReplacer(
    pureCssChunkNames, opts.format,
  )

  for (const file in bundle) {
    const chunk = bundle[file]
    if (chunk.type === 'chunk') {
      // 从其他 chunk 的 imports 中移除纯 CSS chunk
      chunk.imports = chunk.imports.filter((file) => {
        if (pureCssChunkNames.includes(file)) {
          // 将 CSS 资源的归属转移到引用方
          const { importedCss } = (bundle[file] as OutputChunk).viteMetadata!
          importedCss.forEach((f) =>
            chunk.viteMetadata!.importedCss.add(f))
          return false
        }
        return true
      })
      if (chunkImportsPureCssChunk) {
        chunk.code = replaceEmptyChunk(chunk.code)
      }
    }
  }

  // 从 bundle 中删除纯 CSS chunk
  pureCssChunkNames.forEach((fileName) => {
    delete bundle[fileName]
  })
}

getEmptyChunkReplacer 函数根据输出格式生成不同的替换正则:

typescript
export function getEmptyChunkReplacer(
  pureCssChunkNames: string[],
  outputFormat: InternalModuleFormat,
): (code: string) => string {
  const emptyChunkFiles = pureCssChunkNames.map(escapeRegex).join('|')

  const emptyChunkRE = new RegExp(
    outputFormat === 'es'
      ? `\\bimport\\s*["'][^"']*(?:${emptyChunkFiles})["'];`
      : `(\\b|,\\s*)require\\(\\s*["'\`][^"'\`]*(?:${emptyChunkFiles})["'\`]\\)(;|,)`,
    'g',
  )

  return (code) =>
    code.replace(emptyChunkRE, (m) =>
      `/* empty css ${''.padEnd(m.length - 15)}*/`)
}

替换时保持了与原始导入语句相同的字符长度(通过 padEnd),这样 source map 不会偏移。

CSS HMR 机制

cssAnalysisPlugin 的角色

cssAnalysisPlugin 在开发模式下跟踪 CSS 文件的 @import 依赖关系。当一个被 @import 的文件发生变化时,导入它的主文件需要重新编译。

typescript
export function cssAnalysisPlugin(config: ResolvedConfig): Plugin {
  return {
    name: 'vite:css-analysis',
    transform: {
      filter: { id: { include: CSS_LANGS_RE } },
      async handler(_, id) {
        const { moduleGraph } = this.environment as DevEnvironment
        const thisModule = moduleGraph.getModuleById(id)

        if (thisModule) {
          const isSelfAccepting =
            !cssModulesCache.get(config)?.get(id) &&
            !inlineRE.test(id) &&
            !htmlProxyRE.test(id)

          // pluginContainer.addWatchFile 附加的文件
          const pluginImports = (this as any)._addedImports
          if (pluginImports) {
            const depModules = new Set<string | EnvironmentModuleNode>()
            for (const file of pluginImports) {
              depModules.add(
                moduleGraph.createFileOnlyEntry(file)
              )
            }
            moduleGraph.updateModuleInfo(
              thisModule,
              depModules,        // @import 依赖
              null,
              new Set(),
              null,
              isSelfAccepting,
            )
          } else {
            thisModule.isSelfAccepting = isSelfAccepting
          }
        }
      },
    },
  }
}

HMR 更新流程

普通 CSS 文件可以自接受 HMR 更新——updateStyle 函数直接替换 <style> 标签的内容即可。但 CSS Modules 不能自接受,因为类名映射的变化需要通知所有使用这些类名的组件重新渲染。这种情况下,HMR 更新会沿着依赖链向上传播。

CSS 压缩

Vite 支持两种 CSS 压缩引擎:

typescript
async function minifyCSS(css: string, config: ResolvedConfig, inlined: boolean) {
  if (config.build.cssMinify === 'esbuild') {
    const { transform } = await importEsbuild()
    const { code } = await transform(css, {
      loader: 'css',
      target: config.build.cssTarget || undefined,
      ...resolveMinifyCssEsbuildOptions(config.esbuild || {}),
    })
    return inlined ? code.trimEnd() : code
  }

  // 默认使用 Lightning CSS
  const { code } = (await importLightningCSS()).transform({
    ...config.css.lightningcss,
    targets: convertTargets(config.build.cssTarget),
    filename: defaultCssBundleName,
    code: Buffer.from(css),
    minify: true,
  })
  return decoder.decode(code) + (inlined ? '' : '\n')
}

inlined 参数影响是否保留末尾换行符——内联 CSS 不需要尾部换行,而独立文件应以换行符结尾。

Lightning CSS 集成

Lightning CSS 是一个基于 Rust 的高性能 CSS 处理器,可以替代 PostCSS 作为 Vite 的 CSS transformer:

typescript
export function resolveCSSOptions(options: CSSOptions | undefined) {
  const resolved = mergeWithDefaults(_cssConfigDefaults, options ?? {})
  if (resolved.transformer === 'lightningcss') {
    resolved.lightningcss ??= {}
    resolved.lightningcss.targets ??= convertTargets(
      ESBUILD_BASELINE_WIDELY_AVAILABLE_TARGET,
    )
  }
  return resolved
}

css.transformer 设为 'lightningcss' 时,compileCSS 函数会走 compileLightningCSS 分支而非 compilePostCSS。Lightning CSS 的优势在于:

  1. 更快的速度:Rust 实现比 JavaScript 的 PostCSS 快一个数量级
  2. 内置功能:原生支持 CSS Modules、嵌套、自定义属性等,不需要额外插件
  3. 更好的压缩:在代码压缩方面通常优于 esbuild

但代价是放弃了 PostCSS 丰富的插件生态——大部分 PostCSS 插件不能与 Lightning CSS 一起使用。

构建模式与开发模式的差异总结

设计决策

为什么 CSS 插件是最大的文件

CSS 处理的复杂性来自多个维度的组合爆炸:

  • 3 种预处理器 x 2 种 CSS transformer x 2 种运行模式 = 至少 12 种代码路径
  • URL 重写需要处理 url()image-set()@import 三种语法
  • 纯 CSS chunk 清理需要针对 ES、CJS 两种格式生成不同的替换模式
  • source map 合并需要在预处理器、PostCSS、Vite 三层之间正确传递

将所有这些逻辑放在一个文件中虽然看起来庞大,但保证了跨功能的一致性——例如 @import 的解析逻辑在 PostCSS 和预处理器之间是共享的。

为什么预处理器使用 Worker 池

Sass、Less 等预处理器通常是同步阻塞的 CPU 密集型操作。在大型项目中,可能有几百个需要编译的 Sass 文件。如果串行处理,会严重拖慢构建速度。Worker 池允许并行编译,充分利用多核 CPU。

preprocessorMaxWorkers: true 的默认值意味着 Vite 会自动使用所有可用的 CPU 核心(减 1,为主线程留空间)。对于 CI 环境或资源受限的容器,可以通过设置具体数字来控制并行度。

为什么 CSS Modules 不能自接受 HMR

CSS Modules 的输出不仅是样式,还包含类名映射。当类名映射发生变化时(例如添加了新的类名),所有使用这些类名的组件都需要重新渲染。如果 CSS Module 自接受 HMR,只有样式会更新,而引用方仍然使用旧的类名映射,导致样式不一致。

因此 isSelfAccepting 对 CSS Modules 设为 false,让 HMR 更新向上传播到引用方。虽然这可能导致更大范围的更新,但保证了一致性。

为什么不将 ?url CSS 直接当作资源处理

?url 查询的 CSS 文件需要特殊处理。在构建模式下,CSS 内容需要先经过 PostCSS 处理(重写 url() 路径、处理 CSS Modules 等),然后才能作为资源输出。因此 cssPluginload 钩子拦截 ?url 请求,注入一个中间层来确保 CSS 处理管线正确执行。

小结

CSS 处理引擎是 Vite 中最复杂也最精密的子系统。三个插件——cssPlugincssPostPlugincssAnalysisPlugin——各司其职,共同实现了从预处理到最终输出的完整管线。

compileCSS 函数是核心调度器,它将预处理器编译、PostCSS/Lightning CSS 转换、CSS Modules 处理和 source map 合并编排为一条清晰的流水线。开发模式和构建模式在 cssPostPlugin 中分叉,走上截然不同的输出路径。

Worker 池化的预处理器执行、PostCSS 的条件跳过、构建模式下纯 CSS chunk 的智能清理——这些优化确保了 CSS 处理在任何规模的项目中都能保持高性能。

理解了 CSS 引擎的设计,我们就掌握了 Vite 中最复杂的转换逻辑。下一章我们将转向 HTML 处理——Vite 如何解析 HTML 入口文件,提取脚本和样式引用,并在开发与构建模式下正确处理资源路径。

基于 VitePress 构建