我们知道,Vite 在开发阶段实现了一个按需加载的服务器,每一个文件请求进来都会经历一系列的编译流程,然后 Vite 会将编译结果响应给浏览器。在生产环境下,Vite 同样会执行一系列编译过程,将编译结果交给 Rollup 进行模块打包。这一系列的编译过程指的就是 Vite 的插件工作流水线(Pipeline),而插件功能又是 Vite 构建能力的核心,因此谈到阅读 Vite 源码,我们永远绕不开插件的作用与实现原理。
接下来,我就和你一起分析 Vite 插件流水线的顶层架构,也就是各个插件如何被调度和组织起来的,详细说说 Vite 插件容器(PluginContainer)机制的实现,同时带你一起梳理开发阶段和生产环境各自会用到的插件,并分析各自的功能与实现原理,让你能够全面、准确地认识 Vite 的插件流水线!
# 插件容器
我们知道 Vite 的插件机制是与 Rollup 兼容的,但它在开发和生产环境下的实现稍有差别,你可以回顾一下这张架构图:

我们可以看到:
- 在生产环境中 Vite 直接调用 Rollup 进行打包,所以 Rollup 可以调度各种插件;
- 在开发环境中,Vite 模拟了 Rollup 的插件机制,设计了一个
PluginContainer对象来调度各个插件。
PluginContainer(插件容器)对象非常重要,前两节我们也多次提到了它,接下来我们就把目光集中到这个对象身上,看看 Vite 的插件容器机制究竟是如何实现的。
PluginContainer 的 实现 基于借鉴于 WMR 中的rollup-plugin-container.js,主要分为 2 个部分:
- 实现 Rollup 插件钩子的调度
- 实现插件钩子内部的 Context 上下文对象
首先,你可以通过 container 的定义 来看看各个 Rollup 钩子的实现方式,代码精简后如下:
const container = {
// 异步串行钩子
options: await (async () => {
let options = rollupOptions
for (const plugin of plugins) {
if (!plugin.options) continue
options =
(await plugin.options.call(minimalContext, options)) || options
}
return options;
})(),
// 异步并行钩子
async buildStart() {
await Promise.all(
plugins.map((plugin) => {
if (plugin.buildStart) {
return plugin.buildStart.call(
new Context(plugin) as any,
container.options as NormalizedInputOptions
)
}
})
)
},
// 异步优先钩子
async resolveId(rawId, importer) {
// 上下文对象,后文介绍
const ctx = new Context()
let id: string | null = null
cons