vite 源码 - 执行 buildStart 钩子

96 阅读4分钟

入口函数

入口函数名为 buildStart。定义在 EnvironmentPluginContainer 类中。在服务真正启动监听前调用。

// 为了向后兼容,我们在初始化服务器时会为 client 环境调用 buildStart。  
// 对于其他环境,buildStart 将在首次请求被转换(transform)时才调用。
await environments.client.pluginContainer.buildStart()

对于 buildStart 函数,逻辑相对简单。

async buildStart(_options?: InputOptions): Promise<void> {
    if (this._started) {
      if (this._buildStartPromise) {
        await this._buildStartPromise
      }
      return
    }
    this._started = true
    const config = this.environment.getTopLevelConfig()
    this._buildStartPromise = this.handleHookPromise(
      this.hookParallel(
        'buildStart',
        (plugin) => this._getPluginContext(plugin),
        () => [this.options as NormalizedInputOptions],
        (plugin) =>
          this.environment.name === 'client' ||
          config.server.perEnvironmentStartEndDuringDev ||
          plugin.perEnvironmentStartEndDuringDev,
      ),
    ) as Promise<void>
    await this._buildStartPromise
    this._buildStartPromise = undefined
}

可以看出,是一个线性的执行顺序。

  1. 幂等性保护,通过 _started 属性保证不会重复执行,如果已经在执行中,等待完成即可。
  2. 获取顶层配置。
  3. 调用 handleHookPromisehookParallel 函数。
  4. 等待返回的 _buildStartPromise 完成。

这几步中,不知道是干什么的其实就是 handleHookPromisehookParallel 这两个函数。接下来学习这两个函数是做什么的。

handleHookPromise

这个函数的作用在源码中通过注释有说明:记录(追踪)所有钩子(hook)返回的 Promise,以便在关闭服务器时可以等待它们全部执行完毕。

所以这个函数的主要逻辑就是通过一个集合缓存传入的 Promise,并为该 Promise 添加 finally 回调函数,在 Promise 状态改变后从集合中删除。

// keeps track of hook promises so that we can wait for them all to finish upon closing the server
private handleHookPromise<T>(maybePromise: undefined | T | Promise<T>) {
    // 不是 Promise ,原样返回
    if (!(maybePromise as any)?.then) {
      return maybePromise
    }
    const promise = maybePromise as Promise<T>
    this._processesing.add(promise)
    return promise.finally(() => this._processesing.delete(promise))
}

hookParallel

根据这个函数名,其实可以猜测,这个函数的作用就是并行处理钩子函数。在这里需要了解 vite 中插件和钩子的关系,以及它们的执行顺序是如何的。可以通过官网 了解。

private async hookParallel<H extends AsyncPluginHooks & ParallelPluginHooks>(
    hookName: H,
    context: (plugin: Plugin) => ThisType<FunctionPluginHooks[H]>,
    args: (plugin: Plugin) => Parameters<FunctionPluginHooks[H]>,
    condition?: (plugin: Plugin) => boolean | undefined,
  ): Promise<void> {
    const parallelPromises: Promise<unknown>[] = []
    for (const plugin of this.getSortedPlugins(hookName)) {
      // 通过传入条件判断函数判断当前插件是否跳过
      if (condition && !condition(plugin)) continue

      const hook = plugin[hookName]
      const handler: Function = getHookHandler(hook)
      if ((hook as { sequential?: boolean }).sequential) {
        await Promise.all(parallelPromises)
        parallelPromises.length = 0
        await handler.apply(context(plugin), args(plugin))
      } else {
        parallelPromises.push(handler.apply(context(plugin), args(plugin)))
      }
    }
    await Promise.all(parallelPromises)
}

通过代码可以看出,这个函数接收 4 个参数:

  • hookName:钩子名
  • context:上下文
  • args:钩子函数执行参数
  • condition:条件判断,判断插件钩子是否执行

首先,会初始化一个变量 parallelPromises,这个变量用来存储绑定了上下文和参数的钩子函数的执行结果。接下来通过 getSortedPlugins 函数按顺序获取所有有 buildStart 钩子的插件。接下来依次执行。

但是这里需要解释一个概念,即 Javascript 中的并发,由于 Javascript 是单线程的,所以在 Javascript 中,并发其实不是多线程并行,而是并发发起多个异步任务。所以在 hookParallel 函数中,默认所有的钩子函数都是异步函数。

同时,在执行钩子函数过程中,还处理了一种特殊情况,即钩子函数 sequential 属性为 true 时。顾名思义,这个属性为 true 则代表这个钩子函数需要顺序执行,所以需要先清空当前 parallelPromises 中已经发起的异步任务,再执行当前钩子函数。

另外,所有的钩子函数的返回值都没有被使用,这一点在源码中通过注释有说明。

this.hookParallel(
    'buildStart',
    (plugin) => this._getPluginContext(plugin),
    () => [this.options as NormalizedInputOptions],
    (plugin) =>
      this.environment.name === 'client' ||
      config.server.perEnvironmentStartEndDuringDev ||
      plugin.perEnvironmentStartEndDuringDev,
)

对于 buildStart 函数来说,这四个参数就是:

  1. buildStart 钩子
  2. 通过插件获取上下文的函数
  3. 获取执行参数的函数
  4. 条件判断函数

这里的上下文函数 _getPluginContext 是一个其实获取的是 EnvironmentPluginContainer 实例。

private _getPluginContext(plugin: Plugin) {
    if (!this._pluginContextMap.has(plugin)) {
      this._pluginContextMap.set(plugin, new PluginContext(plugin, this))
    }
    return this._pluginContextMap.get(plugin)!
}

这里的 this.options 在这时候还是 undefined

截屏2025-10-11 15.24.25.png

预构建插件

根据上面对两个函数的分析,发现所谓的预构建其实就是执行 buildStart 钩子。那么在这个 hmr 调试项目中,执行了哪几个插件的 buildStart 钩子呢?有以下几个:

插件名作用
vite:watch-package-data监听package.json.env等配置文件的变化,触发开发服务器重启(当这些文件影响构建配置时)。
alias处理resolve.alias配置,将模块路径别名(如@/componentssrc/components)在解析阶段重写为真实路径。
vite:css处理.css.scss.less等样式文件:提取 CSS 内容, 支持 CSS Modules, 注入 HMR 逻辑(热更新样式), 在开发模式下通过<style>标签注入,生产模式下提取为.css文件。
vite:worker支持 Web Worker 和 Shared Worker。识别new Worker(new URL('./worker.js', import.meta.url))语法,将 worker 入口文件打包为独立 chunk,在开发模式下提供 HMR 支持。
vite:asset处理静态资源(图片、字体、音视频等)。小文件转 base64(默认 < 4KB),大文件复制到assets目录并返回 URL,支持?url?raw等查询后缀。
vite:import-glob实现import.meta.glob()import.meta.globEager()
vite:client-inject在开发模式下,向 HTML 中注入 Vite 客户端脚本(vite/client