vite 源码 - 执行 buildStart 钩子

对于预构建是什么?为什么要做预构建?可以看 vite 官网的解释预构建。

入口函数

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

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

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

js 复制代码
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. 调用 handleHookPromise 和 hookParallel 函数。
  4. 等待返回的 _buildStartPromise 完成。

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

handleHookPromise

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

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

kotlin 复制代码
// 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 中插件和钩子的关系,以及它们的执行顺序是如何的。可以通过官网 了解。

typescript 复制代码
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 中已经发起的异步任务,再执行当前钩子函数。

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

javascript 复制代码
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 实例。

kotlin 复制代码
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。

预构建插件

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

插件名 作用
vite:watch-package-data 监听package.json和.env等配置文件的变化,触发开发服务器重启(当这些文件影响构建配置时)。
alias 处理resolve.alias配置,将模块路径别名(如@/components→src/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)
相关推荐
A接拉起00730 分钟前
2026 年 JavaScript 开发者需要了解的知识
前端·javascript·后端
_馒头31 分钟前
pixiJs游戏开发实践 - 跳棋小游戏
javascript·vue.js
用户114818678948435 分钟前
面试官:大屏项目里十几个模块同时渲染上万条数据,怎么做到不卡?
前端·性能优化
前端市界38 分钟前
Pro React Admin: 统一工程底座,承载多种应用...
前端·github
葡萄城技术团队1 小时前
dsh-spreadjs-editor 当前支持什么:安装前需要了解的几件事
前端
三*一1 小时前
优化修整算法
前端·javascript·算法
楠楠子呀2 小时前
英语、西语、阿语客户各开一个口:多语言获客的分流方案
开发语言·javascript·ecmascript·跨境电商·whatsapp·营销工具
weifont2 小时前
自动化抓取和上架商品:能不动手,就少动手
运维·前端·自动化
米柆2 小时前
HTML5 audio /video 完整事件监听模板
前端·html·html5
半个落月2 小时前
LangGraph.js 实战:从状态图到分支、循环、检查点与人工中断
javascript·人工智能·langchain