5 分钟带你快速读懂 vite 打包过程 | 源码解读

18,827 阅读2分钟

前言

下一个时代的打包工具 esbuild 中,我给大家介绍了什么是「esbuild」,以及如何使用它实现一个 bundle 打包。今天,在这个特别的节日(1024),我拎出了「vite」打包实现的核心逻辑,分别进行图解和代码分析,目标只有一个:

5 分钟带大家快速读懂「vite」打包过程!

(正文开始~)

1 npm run vite build 过程发生了什么?

当我们需要打包基于「vite」的项目时,我们需要运行 npm run vite build 命令。实际上,它对应于源码中的 runBuild 方法:

// vite/src/node/cli.ts
async function runBuild(options: UserConfig) {
  try {
    await require('./build').build(options)
    process.exit(0)
  } catch (err) {
    console.error(chalk.red(`[vite] Build errored out.`))
    console.error(err)
    process.exit(1)
  }
}

可以看到,这里我们会调用 ./build/index.ts 中的 build() 方法来进行打包操作:

// vite/src/node/build/index.ts
async function build(options: BuildConfig): Promise<Result> {
  ...
}

而总结起来,build() 方法的实现过程会是这样(时序图):

2 逐行分析 build() 方法实现过程

这里,我们从代码的角度来逐行分析一番 build() 方法的实现细节:

1、移除原先的 outDir 目录(默认情况下是 dist 目录)。

await fs.emptyDir(outDir)

2、解析应用的入口 index.html,创建 buildHtmlPlugin 解析入口 index.html,可以把它理解成 「Webpack」中的 HtmlWebpackPlugin

const { htmlPlugin, renderIndex } = await createBuildHtmlPlugin(
  root,
  indexPath,
  publicBasePath,
  assetsDir,
  assetsInlineLimit,
  resolver,
  shouldPreload
)

3、创建 baseRollupPlugin,它会返回一个 plugin 数组,它包括初始化默认的 plugin 和用户自定义的 plugin,例如 buildResolvePluginesBuildPluginvuePlugin 等等。

const basePlugins = await createBaseRollupPlugins(root, resolver, options)

而这里的 plugins 实际上就是「Rollup」中的 rollupInputOptions 选项的 plugins。所以,如果大家需要自定义 plugin 来实现一些功能,可以参考「Rollup」官网。

4、解析 .env 文件,对于 VITE_ 开头的会通过 import.meta.env 的方式暴露给我们。

Object.keys(env).forEach((key) => {
  if (key.startsWith(`VITE_`)) {
    userEnvReplacements[`import.meta.env.${key}`] = JSON.stringify(env[key])
    userClientEnv[key] = env[key]
  }
})

5、「vite」是通过「Node」的方式使用「Rollup」,所以会调用 rollup.rollup() 生成 bundle。并且,这里会应用上面创建好的 baseRollupPluginbuildHtmlPlugin 以及一些基础的打包选项。

const rollup = require('rollup').rollup as typeof Rollup
const bundle = await rollup({...})

6、调用 bundle.generate 生成 output(对象),它包含每一个 chunk 的内容,例如文件名、文件内容。最后,通过遍历 output 并调用 fs 模块生成对应的 chunk 文件,从而结束整个打包过程。

const { output } = await bundle.generate({
  format: 'es',
  sourcemap,
  entryFileNames: `[name].[hash].js`,
  chunkFileNames: `[name].[hash].js`,
  ...rollupOutputOptions
})

if (write) {
  const cwd = process.cwd()
  const writeFile = async (
    filepath: string,
    content: string | Uint8Array,
    type: WriteType
  ) => {
    ...
  }

  await fs.ensureDir(outDir)

  for (const chunk of output) {
    if (chunk.type === 'chunk') {
      // write chunk
      const filepath = path.join(resolvedAssetsPath, chunk.fileName)
      let code = chunk.code
      if (chunk.map) {
        code += `\n//# sourceMappingURL=${path.basename(filepath)}.map`
      }
      await writeFile(filepath, code, WriteType.JS)
      if (chunk.map) {
        await writeFile(
          filepath + '.map',
          chunk.map.toString(),
          WriteType.SOURCE_MAP
        )
      }
    } else if (emitAssets) {
      if (!chunk.source) continue
      // write asset
      const filepath = path.join(resolvedAssetsPath, chunk.fileName)
      await writeFile(
        filepath,
        chunk.source,
        chunk.fileName.endsWith('.css') ? WriteType.CSS : WriteType.ASSET
      )
    }
  }
  if (indexHtml && emitIndex) {
    await writeFile(
      path.join(outDir, 'index.html'),
      indexHtml,
      WriteType.HTML
    )
  }

需要注意的是,这里我们并没有分析 ssr 对应的打包逻辑,有兴趣的同学可以自行了解。

写在最后

「vite」打包实现的核心就是 build() 方法,通过简单分析这个过程,我想大家对「vite」打包实现应该会建立起一个基本认知。并且,对于如何自定义 plugins 也会知道一二,因为它实际上就是「Rollup」的 plugins。最后,如果文中存在表达不当的地方,欢迎各位同学提 Issue ~

往期文章回顾

下一个时代的打包工具 esbuild

深度解读 Vue3 源码 | 组件创建过程

深度解读 Vue3 源码 | 内置组件 teleport 是什么“来头”?

❤️ 爱心三连击

通过阅读,如果你觉得有收获的话,可以爱心三连击!!!

我是五柳,喜欢创新、捣鼓源码,专注于 Vue3 源码、Vite 源码、前端工程化等技术领域分享,欢迎关注我的微信公众号:Code center