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
相关推荐
wsWmsw3 小时前
[译] 浏览器里的 Liquid Glass:利用 CSS 和 SVG 实现折射
前端·css·svg
用户47949283569153 小时前
还不知道'use strict'的作用?这篇文章给你讲清楚
前端·javascript·typescript
银安3 小时前
CSS排版布局篇(2):文档流(Normal Flow)
前端·css
用户47949283569153 小时前
面试官:讲讲这段react代码的输出(踩坑)
前端·javascript·react.js
jump6803 小时前
闭包详细解析
前端
观默3 小时前
AI看完你的微信,发现了些秘密?
前端·开源
一嘴一个橘子4 小时前
useTemplateRef Vue3.5
javascript·vue.js
林希_Rachel_傻希希4 小时前
《DOM元素获取全攻略:为什么 querySelectorAll() 拿不到新元素?一文讲透动态与静态集合》
前端·javascript
PHP武器库4 小时前
从零到一:用 Vue 打造一个零依赖、插件化的 JS 库
前端·javascript·vue.js