vite-生产构建流程

/post/vite-build article cover image

vite生产构建是通过获取用户配置和预设处理合并配置后函数式调用rollup方法(writegenerate)并返回包含了构建结果的Promise以供后续对应钩子处理,熟悉生产构建流程有助于编写自定义插件以及实现特定业务需求.

cli-build-command

ts
cli
  .command('build [root]', 'build for production')
  // .option('省略cli参数`)...
  .action(async (root: string, options: BuildOptions & GlobalCLIOptions) => {
    // 配置选项去重处理
    filterDuplicateOptions(options)
    const { build } = await import('./build')
    // 移除全局标识
    const buildOptions: BuildOptions = cleanOptions(options)

    try {
      await build({
        root,
        base: options.base,
        mode: options.mode,
        configFile: options.config,
        logLevel: options.logLevel,
        clearScreen: options.clearScreen,
        optimizeDeps: { force: options.force },
        build: buildOptions,
      })
    } catch (e) {
      createLogger(options.logLevel).error(
        colors.red(`error during build:\n${e.stack}`),
        { error: e },
      )
      process.exit(1)
    } finally {
      stopProfiler((message) => createLogger(options.logLevel).info(message))
    }
  })

build-function-process

构建流程主要由resolveConfigbuildOutputOptionsresolveBuildOutputs组成

ts
export async function build (
  inlineConfig: InlineConfig = {}
): Promise<RollupOutput | RollupOutput[] | RollupWatcher> {
  // 解析配置:处理用户配置及预设配置合并(省略部分细节)
  // 1. 解析vite config文件,通过esbuild对config文件进行build并获取处理后的用户配置信息,
  //    如果是函数则传入configEnv进行求值返回否则则直接返回该对象,最终和默认配置进行合并.
  // 2. 对worker plugins和user plugins进行扁平化、根据apply属性进行过滤再按照enforce
  //    的先后顺序进行排序处理.
  // 3. 通过runConfigHook对plugins.config进行hook排序并通过resolvePlugins注入内置plugins,
  //    如果是handler就调用求值并合并,否则将作为对象直接返回
  // 4. 通过resolveOptions对resolve选项进行赋值(mainFields、extensions、alias...)
  // 5. 通过createResolver对optimizer和css中的@imports进行处理
  // 6. 对worker plugins进行合并且根据order排序再runConfigHook,根据返回的config结果对
  //    worker选项进行赋值操作
  // 7. 最终对以上的各个解析处理后的配置选项进行合并汇总最终返回
  const config = await resolveConfig(
    inlineConfig,
    'build',
    'production',
    'production',
  )
  const options = config.build
  const ssr = !!options.ssr
  const libOptions = options.lib

  // 省略building日志...

  const resolve = (p: string) => path.resolve(config.root, p)

  // 针对不同构建类型获取对应入口(lib、ssr、normal input),都没有的话则使用html作为入口文件
  // 并分析index.html中引用的模块
  const input = libOptions
    ? options.rollupOptions?.input ||
      (typeof libOptions.entry === 'string'
        ? resolve(libOptions.entry)
        : Array.isArray(libOptions.entry)
        ? libOptions.entry.map(resolve)
        : Object.fromEntries(
            Object.entries(libOptions.entry).map(([alias, file]) => [
              alias,
              resolve(file),
            ]),
          ))
    : typeof options.ssr === 'string'
    ? resolve(options.ssr)
    : options.rollupOptions?.input || resolve('index.html')

  //  省略当构建ssr时,不能以html文件作为入口判断提示...

  // 构建出口目录
  const outDir = resolve(options.outDir)

  // 对plugin的load、transform钩子注入ssr标识 > { ...options, ssr: true }
  const plugins = (
    ssr ? config.plugins.map((p) => injectSsrFlagToHooks(p)) : config.plugins
  ) as Plugin[]

  const userExternal = options.rollupOptions?.external
  let external = userExternal

  // In CJS, we can pass the externals to rollup as is. In ESM, we need to
  // do it in the resolve plugin so we can add the resolved extension for
  // deep node_modules imports
  if (ssr && config.legacy?.buildSsrCjsExternalHeuristics) {
    external = await cjsSsrResolveExternal(config, userExternal)
  }

  // 当optimizeDeps不为false时,初始化创建依赖优化
  if (isDepsOptimizerEnabled(config, ssr)) {
    // 此步骤主要在dev阶段进行依赖分析和预打包,这里省略..
    await initDepsOptimizer(config)
  }

  const rollupOptions: RollupOptions = {
    context: 'globalThis',
    preserveEntrySignatures: ssr
      ? 'allow-extension'
      : libOptions
      ? 'strict'
      : false,
    cache: config.build.watch ? undefined : false,
    ...options.rollupOptions,
    input,
    plugins,
    external,
    onwarn(warning, warn) {
      onRollupWarning(warning, warn, config)
    }
  }

  // 省略outputBuildError

  let bundle: RollupBuild | undefined
  try {
    // 根据传进的output返回对应构建信息对象包含了mode=lib的情况最终会被push到normalizedOutputs中
    const buildOutputOptions = (output: OutputOptions = {}): OutputOptions => {
      // 省略rollupOptions.output.output弃用提示判断

      const ssrNodeBuild = ssr && config.ssr.target === 'node'
      const ssrWorkerBuild = ssr && config.ssr.target === 'webworker'
      const cjsSsrBuild = ssr && config.ssr.format === 'cjs'

      const format = output.format || (cjsSsrBuild ? 'cjs' : 'es')
      // 对应模块后缀判断(cjs、mjs)
      const jsExt =
        ssrNodeBuild || libOptions
          ? resolveOutputJsExtension(format, getPkgJson(config.root)?.type)
          : 'js'

      return {
        dir: outDir,
        // Default format is 'es' for regular and for SSR builds
        format,
        exports: cjsSsrBuild ? 'named' : 'auto',
        sourcemap: options.sourcemap,
        name: libOptions ? libOptions.name : undefined,
        // es2015 enables `generatedCode.symbols`
        // - #764 add `Symbol.toStringTag` when build es module into cjs chunk
        // - #1048 add `Symbol.toStringTag` for module default export
        generatedCode: 'es2015',
        entryFileNames: ssr
          ? `[name].${jsExt}`
          : libOptions
          ? ({ name }) =>
              resolveLibFilename(libOptions, format, name, config.root, jsExt)
          : path.posix.join(options.assetsDir, `[name]-[hash].${jsExt}`),
        chunkFileNames: libOptions
          ? `[name]-[hash].${jsExt}`
          : path.posix.join(options.assetsDir, `[name]-[hash].${jsExt}`),
        assetFileNames: libOptions
          ? `[name].[ext]`
          : path.posix.join(options.assetsDir, `[name]-[hash].[ext]`),
        inlineDynamicImports:
          output.format === 'umd' ||
          output.format === 'iife' ||
          (ssrWorkerBuild &&
            (typeof input === 'string' || Object.keys(input).length === 1)),
        ...output,
      }
    }

    // 对于类库模式根据output是单个还是多个进行错误情况判断提示
    const outputs = resolveBuildOutputs(
      options.rollupOptions?.output,
      libOptions,
      config.logger,
    )
    const normalizedOutputs: OutputOptions[] = []

    // 将通过buildOutputOptions分别处理后的rollup output配置项存放到数组中
    if (Array.isArray(outputs)) {
      for (const resolvedOutput of outputs) {
        normalizedOutputs.push(buildOutputOptions(resolvedOutput))
      }
    } else {
      normalizedOutputs.push(buildOutputOptions(outputs))
    }

    // 获取出口目录路径
    const outDirs = normalizedOutputs.map(({ dir }) => resolve(dir!))

    // 当使用build命令且watch为true(--watch、-w)时实时构建
    if (config.build.watch) {
      config.logger.info(colors.cyan(`\nwatching for file changes...`))

      const resolvedChokidarOptions = resolveChokidarOptions(
        config,
        config.build.watch.chokidar,
      )

      const { watch } = await import('rollup')
      const watcher = watch({
        ...rollupOptions,
        output: normalizedOutputs,
        watch: {
          ...config.build.watch,
          chokidar: resolvedChokidarOptions,
        },
      })

      watcher.on('event', (event) => {
        if (event.code === 'BUNDLE_START') {
          config.logger.info(colors.cyan(`\nbuild started...`))
          if (options.write) {
            prepareOutDir(outDirs, options.emptyOutDir, config)
          }
        } else if (event.code === 'BUNDLE_END') {
          event.result.close()
          config.logger.info(colors.cyan(`built in ${event.duration}ms.`))
        } else if (event.code === 'ERROR') {
          outputBuildError(event.error)
        }
      })

      return watcher
    }

    // 使用rollup函数式api,对处理后的rollup配置项进行bundle
    const { rollup } = await import('rollup')
    bundle = await rollup(rollupOptions)

    // build阶段会进行文件产出
    if (options.write) {
      prepareOutDir(outDirs, options.emptyOutDir, config)
    }

    const res = []
    // 将根据是否写入磁盘调用对应方法,将构建后的结果放到res中并返回
    for (const output of normalizedOutputs) {
      res.push(await bundle[options.write ? 'write' : 'generate'](output))
    }
    return Array.isArray(outputs) ? res : res[0]
  } catch (e) {
    outputBuildError(e)
    throw e
  } finally {
    if (bundle) await bundle.close()
  }
}