Vite8 关于 vite build 命令构建过程

0 阅读5分钟

在 Vite 8 中,vite build 命令已经从传统的 Rollup 打包,彻底转向了由 Rust 驱动的全新工具链。

Vite 8 最大的改变,是其构建流程的底层核心被完全重写,统一使用 Rust 生态的工具。

  • 单一打包器 Rolldown:此前,Vite 在开发环境使用 esbuild 追求速度,在生产环境使用 Rollup 追求能力,这导致了行为不一致。Vite 8 使用一个名为 Rolldown 的 Rust 打包器,统一了开发和生产环境的构建链路。它完全兼容 Rollup 的插件 API,使得绝大多数现有 Vite 插件无需修改即可在 Vite 8 中运行。
  • 高性能引擎 Oxc:Rolldown 本身构建于 Oxc(另一个用 Rust 编写的工具集)之上。Oxc 为 Rolldown 提供了极快的解析、转换能力,使其在处理 TypeScript 和 JSX 文件时性能大幅领先。

vite build 有哪些命令行参数?

// build
cli
  .command('build [root]', 'build for production')
  .option(
    '--target <target>',
    `[string] transpile target (default: 'baseline-widely-available')`,
  )
  .option('--outDir <dir>', `[string] output directory (default: dist)`)
  .option(
    '--assetsDir <dir>',
    `[string] directory under outDir to place assets in (default: assets)`,
  )
  .option(
    '--assetsInlineLimit <number>',
    `[number] static asset base64 inline threshold in bytes (default: 4096)`,
  )
  .option(
    '--ssr [entry]',
    `[string] build specified entry for server-side rendering`,
  )
  .option(
    '--sourcemap [output]',
    `[boolean | "inline" | "hidden"] output source maps for build (default: false)`,
  )
  .option(
    '--minify [minifier]',
    `[boolean | "terser" | "esbuild"] enable/disable minification, ` +
      `or specify minifier to use (default: esbuild)`,
  )
  .option('--manifest [name]', `[boolean | string] emit build manifest json`)
  .option('--ssrManifest [name]', `[boolean | string] emit ssr manifest json`)
  .option(
    '--emptyOutDir',
    `[boolean] force empty outDir when it's outside of root`,
  )
  .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`)
  .option('--app', `[boolean] same as \`builder: {}\``)

vite build 接收的 options 有哪些?

image.png

源码

createBuilder

/**
 * Creates a ViteBuilder to orchestrate building multiple environments.
 * 创建和配置 vite构建器
 * @experimental
 * params inlineConfig 行内配置
 * params useLegacyBuilder 是否使用旧版构建器
 */
export async function createBuilder(
  inlineConfig: InlineConfig = {},
  useLegacyBuilder: null | boolean = false,
): Promise<ViteBuilder> {

  // 处理旧版兼容
  const patchConfig = (resolved: ResolvedConfig) => {
    if (!(useLegacyBuilder ?? !resolved.builder)) return

    // Until the ecosystem updates to use `environment.config.build` instead of `config.build`,
    // we need to make override `config.build` for the current environment.
    // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later
    // remove the default values that shouldn't be used at all once the config is resolved
    const environmentName = resolved.build.ssr ? 'ssr' : 'client'
    ;(resolved.build as ResolvedBuildOptions) = {
      ...resolved.environments[environmentName].build,
    }
  }
  // 配置解析
  const config = await resolveConfigToBuild(inlineConfig, patchConfig)
  // 是否使用旧版构建器
  useLegacyBuilder ??= !config.builder
  // 构建器配置
  const configBuilder = config.builder ?? resolveBuilderOptions({})!

  const environments: Record<string, BuildEnvironment> = {}

  // 创建 ViteBuilder 对象
  const builder: ViteBuilder = {
    environments,
    config,
    /**
     * 构建整个应用
     */
    async buildApp() {
      // 创建插件上下文
      const pluginContext = new BasicMinimalPluginContext(
        { ...basePluginContextMeta, watchMode: false },
        config.logger,
      )

      // order 'pre' and 'normal' hooks are run first, then config.builder.buildApp, then 'post' hooks
      // 是否已调用配置构建器的 buildApp 方法
      let configBuilderBuildAppCalled = false

      // 执行插件的 buildApp 钩子
      for (const p of config.getSortedPlugins('buildApp')) {
        const hook = p.buildApp
        if (
          !configBuilderBuildAppCalled &&
          typeof hook === 'object' &&
          hook.order === 'post' // 只在 post 阶段调用
        ) {
          configBuilderBuildAppCalled = true
          await configBuilder.buildApp(builder)
        }
        const handler = getHookHandler(hook)
        await handler.call(pluginContext, builder)
      }
      // 如果未调用配置构建器的 buildApp 方法,调用默认 buildApp 方法
      if (!configBuilderBuildAppCalled) {
        await configBuilder.buildApp(builder)
      }
      // fallback to building all environments if no environments have been built
      // 检查是否有环境被构建
      if (
        Object.values(builder.environments).every(
          (environment) => !environment.isBuilt,
        )
      ) {
        for (const environment of Object.values(builder.environments)) {
          // 构建所有环境
          await builder.build(environment)
        }
      }
    },
    /**
     * 构建环境
     * @param environment 
     * @returns 
     */
    async build(
      environment: BuildEnvironment,
    ): Promise<RolldownOutput | RolldownOutput[] | RolldownWatcher> {
      const output = await buildEnvironment(environment)
      environment.isBuilt = true
      return output
    },
    async runDevTools() {
      const devtoolsConfig = config.devtools
      if (devtoolsConfig.enabled) {
        try {
          const { start } = await import(`@vitejs/devtools/cli-commands`)
          await start(devtoolsConfig.config)
        } catch (e) {
          config.logger.error(
            colors.red(`Failed to run Vite DevTools: ${e.message || e.stack}`),
            { error: e },
          )
        }
      }
    },
  }

  /**
   * 环境设置函数
   */
  async function setupEnvironment(name: string, config: ResolvedConfig) {
    const environment = await config.build.createEnvironment(name, config)
    await environment.init()
    environments[name] = environment
  }

  // 环境初始化
  // 使用旧版构建器
  if (useLegacyBuilder) {
    await setupEnvironment(config.build.ssr ? 'ssr' : 'client', config)
  } else {
    // 新版构建器
    const environmentConfigs: [string, ResolvedConfig][] = []

    for (const environmentName of Object.keys(config.environments)) {
      // We need to resolve the config again so we can properly merge options
      // and get a new set of plugins for each build environment. The ecosystem
      // expects plugins to be run for the same environment once they are created
      // and to process a single bundle at a time (contrary to dev mode where
      // plugins are built to handle multiple environments concurrently).
      let environmentConfig = config
      if (!configBuilder.sharedConfigBuild) {
        const patchConfig = (resolved: ResolvedConfig) => {
          // Until the ecosystem updates to use `environment.config.build` instead of `config.build`,
          // we need to make override `config.build` for the current environment.
          // We can deprecate `config.build` in ResolvedConfig and push everyone to upgrade, and later
          // remove the default values that shouldn't be used at all once the config is resolved
          ;(resolved.build as ResolvedBuildOptions) = {
            ...resolved.environments[environmentName].build,
          }
        }
        const patchPlugins = (resolvedPlugins: Plugin[]) => {
          // Force opt-in shared plugins
          let j = 0
          for (let i = 0; i < resolvedPlugins.length; i++) {
            const environmentPlugin = resolvedPlugins[i]
            if (
              configBuilder.sharedPlugins ||
              environmentPlugin.sharedDuringBuild
            ) {
              for (let k = j; k < config.plugins.length; k++) {
                if (environmentPlugin.name === config.plugins[k].name) {
                  resolvedPlugins[i] = config.plugins[k]
                  j = k + 1
                  break
                }
              }
            }
          }
        }
        // 为每个环境名称创建环境配置
        environmentConfig = await resolveConfigToBuild(
          inlineConfig,
          patchConfig,
          patchPlugins,
        )
      }
      
      environmentConfigs.push([environmentName, environmentConfig])
    }
    // 并行初始化所有环境
    await Promise.all(
      environmentConfigs.map(
        async ([environmentName, environmentConfig]) =>
          await setupEnvironment(environmentName, environmentConfig),
      ),
    )
  }

  return builder
}

image.png

image.png

buildEnvironment

buildEnvironment 函数是 Vite 8 中为单个环境(如 client 或 ssr)执行生产构建的核心函数:

  1. 首先解析 Rolldown 打包配置。
  2. 然后根据是否开启监听模式(options.watch)分别创建 Rolldown 的 watcher 以持续构建并监听文件变化,或一次性调用 Rolldown 完成打包。
  3. 构建过程中会收集每个输出 chunk 的元数据,支持多输出配置(如同时输出 ESM 和 CJS),并最终将产物写入磁盘或返回结果对象。
  4. 同时提供详细的日志输出和错误增强处理,在结束前确保关闭 Rolldown 实例以释放资源。

/**
 * Build an App environment, or a App library (if libraryOptions is provided)
 * Vite 8 中负责生产构建单个环境(如 client、ssr)的核心函数。
 * 基于 Rolldown(Rust 打包器)执行打包,支持普通构建和监听模式(watch)
 **/
async function buildEnvironment(
  environment: BuildEnvironment,
): Promise<RolldownOutput | RolldownOutput[] | RolldownWatcher> {
  const { logger, config } = environment
  const { root, build: options } = config

  // 记录开始构建的日志
  logger.info(
    colors.cyan(
      `vite v${VERSION} ${colors.green(
        `building ${environment.name} environment for ${environment.config.mode}...`,
      )}`,
    ),
  )

  let bundle: RolldownBuild | undefined
  let startTime: number | undefined
  try {
    // 收集每个输出 chunk 的元数据(如模块 ID、文件大小等)
    const chunkMetadataMap = new ChunkMetadataMap()
    // 解析 Rolldown 选项
    const rollupOptions = resolveRolldownOptions(environment, chunkMetadataMap)

    // watch file changes with rollup
    // 监视文件变化
    if (options.watch) {
      logger.info(colors.cyan(`\nwatching for file changes...`))

      const resolvedOutDirs = getResolvedOutDirs(
        root,
        options.outDir,
        options.rollupOptions.output,
      )
      const emptyOutDir = resolveEmptyOutDir(
        options.emptyOutDir,
        root,
        resolvedOutDirs,
        logger,
      )
      const resolvedChokidarOptions = resolveChokidarOptions(
        {
          // @ts-expect-error chokidar option does not exist in rolldown but used for backward compat
          ...(rollupOptions.watch || {}).chokidar,
          // @ts-expect-error chokidar option does not exist in rolldown but used for backward compat
          ...options.watch.chokidar,
        },
        resolvedOutDirs,
        emptyOutDir,
        environment.config.cacheDir,
      )

      const { watch } = await import('rolldown')
      // 调用 rolldown.watch 创建监听器
      const watcher = watch({
        ...rollupOptions,
        watch: {
          ...rollupOptions.watch,
          ...options.watch,
          notify: convertToNotifyOptions(resolvedChokidarOptions),
        },
      })

      watcher.on('event', (event) => {
        if (event.code === 'BUNDLE_START') {
          logger.info(colors.cyan(`\nbuild started...`))
          chunkMetadataMap.clearResetChunks()
        } else if (event.code === 'BUNDLE_END') {
          event.result.close()
          logger.info(colors.cyan(`built in ${event.duration}ms.`))
        } else if (event.code === 'ERROR') {
          const e = event.error
          enhanceRollupError(e)
          clearLine()
          logger.error(e.message, { error: e })
        }
      })

      return watcher
    }

    // 普通构建
    // write or generate files with rolldown
    const { rolldown } = await import('rolldown')
    startTime = Date.now()
    // 创建 Rolldown 构建实例
    bundle = await rolldown(rollupOptions)

    // 多个输出配置
    const res: RolldownOutput[] = []

    for (const output of arraify(rollupOptions.output!)) {
      // bundle.write(outputOptions) 将产物写入磁盘
      // bundle.generate(outputOptions) 仅返回产物对象
      res.push(await bundle[options.write ? 'write' : 'generate'](output))
    }
    for (const output of res) {
      for (const chunk of output.output) {
        // 注入 chunk 元数据
        injectChunkMetadata(chunkMetadataMap, chunk)
      }
    }
    logger.info(
      `${colors.green(`✓ built in ${displayTime(Date.now() - startTime)}`)}`,
    )

    // 返回构建结果
    return Array.isArray(rollupOptions.output) ? res : res[0]
  } catch (e) {
    enhanceRollupError(e)
    clearLine()
    if (startTime) {
      logger.error(
        `${colors.red('✗')} Build failed in ${displayTime(Date.now() - startTime)}`,
      )
      startTime = undefined
    }
    throw e
  } finally {
    // 关闭 Rolldown 构建实例
    if (bundle) await bundle.close()
  }
}

image.png

image.png

image.png

Vite 8 的生产构建底层完全基于 Rolldown(Rust 打包器),支持两种构建模式:一次性打包(默认 vite build)和 监听打包vite build --watch)。

image.png

命令分析

"build": "run-p type-check \"build-only {@}\" --"
"build-only": "vite build",

run-p:来自 npm-run-all,表示并行执行后面的脚本

  • type-check:第一个要运行的脚本(通常用于 TypeScript 类型检查)。
  • "build-only {@}" :第二个要运行的脚本。
    • build-only 是另一个 npm 脚本(自定义,例如 vite build)。
    • {@} 是 npm-run-all 的特殊占位符,代表传递给当前 build 命令的所有原始参数

测试

{
    build: {
    emptyOutDir:true, // 清空目录
    copyPublicDir: true,
    reportCompressedSize: true,//启用/禁用 gzip 压缩大小报告
    chunkSizeWarningLimit:500,// 规定触发警告的 chunk 大小。(以 kB 为单位)。
    assetsInlineLimit:4096,// 4kb 小于此阈值的导入或引用资源将内联为 base64 编码,以避免额外的 http 请求
    // baseline-widely-available 具体来说,它是 `['chrome111', 'edge111', 'firefox114', 'safari16.4']`
    // esnext —— 即假设有原生动态导入支持,并只执行最低限度的转译。
    target: 'baseline-widely-available',
    // 如果禁用,整个项目中的所有 CSS 将被提取到一个 CSS 文件中。
    cssCodeSplit: true,// 启用,在异步 chunk 中导入的 CSS 将内联到异步 chunk 本身,并在其被加载时一并获取。
    cssMinify: 'lightningcss',// Vite 默认使用 [Lightning CSS](https://lightningcss.dev/minification.html) 来压缩 CSS
    // true,将会创建一个独立的 source map 文件
    // inline,source map 将作为一个 data URI 附加在输出文件中
    sourcemap:false,
    license:true, // true,构建过程将生成一个 .vite/license.md文件,
    }
}

示例 build.outDir 、build.assetsDir

build.outDir默认值 dist,build.assetsDir默认值 assets

image.png

  build: {
    outDir: 'dist-cube',
    assetsDir: 'public',
  },

image.png

image.png

示例 build.minify

1、默认情况。

minify 默认压缩,客户端构建默认为'oxc'

image.png

2、配置不压缩。

  build: {
    outDir: 'dist-cube',
    assetsDir: 'public',
    minify: false, // 不压缩
  },

image.png

3、配置 esbuild 。

  build: {
    outDir: 'dist-cube',
    assetsDir: 'public',
    minify: 'esbuild',
  },

提示 在 vite8 中 esbuild 已废弃。建议使用 oxc 。

image.png

4、 配置 terser。

  build: {
    outDir: 'dist-cube',
    assetsDir: 'public',
    minify: 'terser',
  },

image.png

当设置为 'esbuild' 或 'terser' 时,必须分别安装 esbuild 或 Terser。

npm add -D esbuild
npm add -D terser

示例 build.manifest / ssrManifest

manifest 设置为 true 时,路径将是 .vite/manifest.json
ssrManifest 设置为 true 时,路径将是 .vite/ssr-manifest.json

image.png

image.png

vue3-vite-cube/dist-cube/index.html

<!doctype html>
<html lang="">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App菜单</title>
    <script type="module" crossorigin src="/public/index-B9iM-AOo.js"></script>
    <link rel="modulepreload" crossorigin href="/public/runtime-core.esm-bundler-HXD8ebTp.js">
    <link rel="stylesheet" crossorigin href="/public/index-DuS5nk76.css">
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

最后

  1. rolldown 配置
  2. vite 配置