对于预构建是什么?为什么要做预构建?可以看 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 ) |