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. 调用 handleHookPromisehookParallel 函数。
  4. 等待返回的 _buildStartPromise 完成。

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

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配置,将模块路径别名(如@/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
相关推荐
我只会写Bug啊3 小时前
Vue文件预览终极方案:PNG/EXCEL/PDF/DOCX/OFD等10+格式一键渲染,开源即用!
前端·vue.js·pdf·excel·预览
扯蛋4384 小时前
LangChain的学习之路( 一 )
前端·langchain·mcp
Mr.Jessy4 小时前
Web APIs学习第一天:获取 DOM 对象
开发语言·前端·javascript·学习·html
午安~婉4 小时前
javaScript八股问题
开发语言·javascript·原型模式
西西学代码5 小时前
Flutter---个人信息(5)---持久化存储
java·javascript·flutter
芝麻开门-新起点5 小时前
flutter 生命周期管理:从 Widget 到 State 的完整解析
开发语言·javascript·ecmascript
ConardLi5 小时前
Easy Dataset 已经突破 11.5K Star,这次又带来多项功能更新!
前端·javascript·后端
冴羽6 小时前
10 个被严重低估的 JS 特性,直接少写 500 行代码
前端·javascript·性能优化
rising start6 小时前
四、CSS选择器(续)和三大特性
前端·css
一 乐6 小时前
高校后勤报修系统|物业管理|基于SprinBoot+vue的高校后勤报修系统(源码+数据库+文档)
java·前端·javascript·数据库·vue.js·毕设