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
相关推荐
用户47949283569157 分钟前
别再当 AI 的"人肉定位器"了:一个工具让 React 组件秒定位
前端·aigc·ai编程
Nan_Shu_61424 分钟前
学习:Sass
javascript·学习·es6
WYiQIU1 小时前
面了一次字节前端岗,我才知道何为“造火箭”的极致!
前端·javascript·vue.js·react.js·面试
qq_316837751 小时前
uniapp 观察列表每个元素的曝光时间
前端·javascript·uni-app
小夏同学呀1 小时前
在 Vue 2 中实现 “点击下载条码 → 打开新窗口预览 → 自动唤起浏览器打印” 的功能
前端·javascript·vue.js
芳草萋萋鹦鹉洲哦1 小时前
【vue】导航栏变动后刷新router的几种方法
前端·javascript·vue.js
zero13_小葵司1 小时前
JavaScript性能优化系列(八)弱网环境体验优化 - 8.3 数据预加载与缓存:提前缓存关键数据
javascript·缓存·性能优化
1***y1781 小时前
Vue项目性能优化案例
前端·vue.js·性能优化
Irene19911 小时前
FileList 对象总结(附:不支持迭代的类数组对象表)
javascript·类数组对象·filelist·不支持迭代
谢尔登2 小时前
【CSS】样式隔离
前端·css