Vite 是如何使用 Rollup 进行构建的

我们都知道,Vite 在生产环境中,会使用 Rollup 进行构建,那么 Vite 是如何做到的呢?本文将讲述,从执行 vite build 到输出构建产物,这期间到底发生了什么?

Vite 的 build 命令

我们直接来看 build 命令的源码

// build
cli
  .command('build [root]', 'build for production')
  .option('--target ', `[string] transpile target (default: 'modules')`)
  .option('--outDir ', `[string] output directory (default: dist)`)
  .option('-w, --watch', `[boolean] rebuilds when modules have changed on disk`)
  .action(async (root: string, options: BuildOptions & GlobalCLIOptions) => {
    // build 命令会执行的内容
    
  })
复制代码

我们来看看 build 命令实际执行的内容:

const { build } = await import('./build')
// 处理 options 参数
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:
${e.stack}`),
        { error: e },
    )
    process.exit(1)
} finally {
    stopProfiler((message) => createLogger(options.logLevel).info(message))
}
复制代码

因此,build 命令实际上就是执行 build 函数

build 函数如下:

let parallelCallCounts = 0
const parallelBuilds: RollupBuild[] = []

export async function build(
  inlineConfig: InlineConfig = {},
): Promise {
  parallelCallCounts++
  try {
    return await doBuild(inlineConfig)
  } finally {
    parallelCallCounts--
    if (parallelCallCounts <= 0) {
      await Promise.all(parallelBuilds.map((bundle) => bundle.close()))
      parallelBuilds.length = 0
    }
  }
}
复制代码

实际上这里调用了 doBuild 函数。doBuild 函数中则是真正的执行构建了。

这里的并行处理的代码,是历史遗留逻辑,如今已经是没有用了。这部分在该 pull request 已经被删除,但截至发文该改动未被合入到 master

执行构建

doBuild 函数中,Vite 利用 Rollup 的 JS API 执行构建。

Rollup JS API 的使用分为两部分:

async function build() {
  // create a bundle
  let  bundle = await rollup(inputOptions);

  // 从 bundle 生成代码并返回,拿到的是字符串,可以进行进一步的处理
  const { output } = await bundle.generate(outputOptions);
  // 或直接从 bundle 生成代码并写入到磁盘,直接生成文件
  await bundle.write(outputOptions)
 
  // closes the bundle
  await bundle.close();
}
复制代码

同样的,Vite 通过 Rollup JS API 生成代码,需要生成 input 和 output 配置,总的流程如下:

标准化 Vite 配置

async function doBuild(
  inlineConfig: InlineConfig = {},
): Promise {
    // 生成一份标准化后的配置
    const config = await resolveConfig(
        inlineConfig,
        'build',
        'production',
        'production',
    )
    // 省略其他逻辑
}
复制代码

doBuild 的第一步,就是标准化 Vite 的配置,这里用的是 resolveConfig 函数,它会读取项目目录的 Vite 配置文件(如 vite.config.ts),并跟 Vite 的一些内容配置进行合并,最终返回。

它的行为与 Vite dev 完全一致。如果对 Vite 的配置解析感兴趣,可以参考我写过的文章《五千字剖析 vite 是如何对配置文件进行解析的》,在该文章中,详细叙述过这个完成的流程。其主要有以下几步:

Rollup input 配置

Vite 生成的 rollup 配置如下:

const rollupOptions: RollupOptions = {
    context: 'globalThis',
    // vite 配置文件的 build.rollupOptions 对象
    ...options.rollupOptions,
    input,
    plugins,
    external,
}
复制代码

我们用 Vite 仓库中自带的示例项目打个断点看看:

可以看到,Rollup 配置中主要有这么几个配置:

const input = 
    //	如果设置了 build.lib 对象,则对 build.lib 进行处理,需要支持多入口构建
    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),
              ]),
          ))
	// 没有设置 build.lib
    : typeof options.ssr === 'string'
        ? resolve(options.ssr)
		// 什么也不填,就会走到这里,默认入口是当前目录的 index.html
        : options.rollupOptions?.input || resolve('index.html')
复制代码

plugin 中,加入了很多 Vite 的内置插件。Vite 的很多开箱即用的能力,都是由这些插件提供的(Rollup 本身没有内置这些能力),例如:

由于篇幅优先,这些插件就不一一介绍了。

在 vite build 与 vite dev 两种模式下,使用的插件都是相同的,Vite 在开发模式下,模仿 Rollup 仿造出了一套拥有相同的 API 的插件架构,使得插件在两种模式下都能正常使用,保证了两种模式下 Vite 有相同的行为。更多细节可以查看文章《Vite 是如何兼容 Rollup 插件生态的》

Rollup output 配置

Rollup 输出产物的代码如下:

const generate = (output: OutputOptions = {}) => {
  // 调用  bundle.write 或 bundle.generate
  return bundle[options.write ? 'write' : 'generate'](output)
}

if (options.write) {
  // 如果需要写入磁盘,就先准备输出目录,确保目录存在,并且清空目录
  prepareOutDir(outDirs, options.emptyOutDir, config)
}

const res = []
// normalizedOutputs 为多个输出配置,因为可能一次构建,会输出多份代码
// 常见于构建 lib,需要分别输出 umd、esm 等多种格式的产物
for (const output of normalizedOutputs) {
  res.push(await generate(output))
}
return Array.isArray(outputs) ? res : res[0]
复制代码

同样的,我们还是打个断点看看:

output 参数中,定义了产物输出目录、产物 js 版本、名称格式等,因此,我们可以看到有以下的构建产物。

output 配置的生成

// 标准化 output 配置,从 vite 配置中生成
const outputs = resolveBuildOutputs(
    options.rollupOptions?.output,
    libOptions,
    config.logger,
)
const normalizedOutputs: OutputOptions[] = []

if (Array.isArray(outputs)) {
  // 处理多入口的情况
  for (const resolvedOutput of outputs) {
    normalizedOutputs.push(buildOutputOptions(resolvedOutput))
  }
} else {
  normalizedOutputs.push(buildOutputOptions(outputs))
}
复制代码

buildOutputOptions 函数如下,主要是包装完整的 output 构建配置(大概看一下就行了):

const buildOutputOptions = (output: OutputOptions = {}): OutputOptions => {
    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')
    const jsExt = ssrNodeBuild || libOptions
        ? resolveOutputJsExtension(format, getPkgJson(config.root)?.type)
        : 'js'
    return {
        dir: outDir,
        format,
        exports: cjsSsrBuild ? 'named' : 'auto',
        sourcemap: options.sourcemap,
        name: libOptions ? libOptions.name : undefined,
        // vite 默认生成 es2015,因此默认是不支持传统老的浏览器
        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,
    }
}
复制代码

至此,整个完整的 Rollup 配置就出来了。

总结

Vite build 的代码量其实非常的少,因为在 build 阶段,Vite 是利用 Rollup 去完成构建,整个过程只需要调用 Rollup 提供的 JS API 即可,整个过程中,Vite 的工作只是在做配置的转换,把 Vite 的配置转换成 Rollup 的 input 和 output 配置。

Vite 通过在 dev 模式时,模拟出一套与 Rollup 相同的插件架构,通过 devbuild 模式使用同一套插件,从而使两个模式下有相同的构建行为

关联阅读

展开阅读全文

页面更新:2024-04-03

标签:千字   磁盘   产物   函数   插件   入口   命令   参数   代码   目录

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top