对于预构建是什么?为什么要做预构建?可以看 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
}
可以看出,是一个线性的执行顺序。
- 幂等性保护,通过 _started 属性保证不会重复执行,如果已经在执行中,等待完成即可。
- 获取顶层配置。
- 调用
handleHookPromise和hookParallel函数。 - 等待返回的
_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 函数来说,这四个参数就是:
- buildStart 钩子
- 通过插件获取上下文的函数
- 获取执行参数的函数
- 条件判断函数
这里的上下文函数 _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) |