跳到主要内容

认识

2023年12月28日
柏拉文
越努力,越幸运

一、认识


Vite 在开发阶段实现了一个按需加载的服务器,每一个文件请求进来都会经历一系列的编译流程,然后 Vite 会将编译结果响应给浏览器。在生产环境下,Vite 同样会执行一系列编译过程,将编译结果交给 Rollup 进行模块打包。这一系列的编译过程指的就是 Vite 的插件工作流水线(Pipeline),

Preview

我们可以看到:

  • 在生产环境中 Vite 直接调用 Rollup 进行打包,所以 Rollup 可以调度各种插件

  • 在开发环境中,Vite 模拟了 Rollup 的插件机制,设计了一个 PluginContainer 对象来调度各个插件

二、工作


三、PluginContainer


PluginContainer 的 实现基于借鉴于 WMR 中的 rollup-plugin-container.js,主要分为 2 个部分:

  • 实现 Rollup 插件钩子的调度

  • 实现插件钩子内部的 Context 上下文对象

3.1 实现

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
const partial: Partial<PartialResolvedId> = {}
for (const plugin of plugins) {
const result = await plugin.resolveId.call(
ctx as any,
rawId,
importer,
{ ssr }
)
if (!result) continue;
return result;
}
}
// 异步优先钩子
async load(id, options) {
const ctx = new Context()
for (const plugin of plugins) {
const result = await plugin.load.call(ctx as any, id, { ssr })
if (result != null) {
return result
}
}
return null
},
// 异步串行钩子
async transform(code, id, options) {
const ssr = options?.ssr
// 每次 transform 调度过程会有专门的上下文对象,用于合并 SourceMap,后文会介绍
const ctx = new TransformContext(id, code, inMap as SourceMap)
ctx.ssr = !!ssr
for (const plugin of plugins) {
let result: TransformResult | string | undefined
try {
result = await plugin.transform.call(ctx as any, code, id, { ssr })
} catch (e) {
ctx.error(e)
}
if (!result) continue;
// 省略 SourceMap 合并的逻辑
code = result;
}
return {
code,
map: ctx._getCombinedSourcemap()
}
},
// close 钩子实现省略
}

四、PluginContext


Rollup 钩子函数中,我们可以调用this.emitFilethis.resolve 等诸多的上下文方法,因此,Vite 除了要模拟各个插件的执行流程,还需要模拟插件执行的上下文对象,代码中的 Context 对象就是用来完成这件事情的。

4.1 实现

import { RollupPluginContext } from 'rollup';
type PluginContext = Omit<
RollupPluginContext,
// not documented
| 'cache'
// deprecated
| 'emitAsset'
| 'emitChunk'
| 'getAssetFileName'
| 'getChunkFileName'
| 'isExternal'
| 'moduleIds'
| 'resolveId'
| 'load'
>

const watchFiles = new Set<string>()

class Context implements PluginContext {
// 实现各种上下文方法
// 解析模块 AST(调用 acorn)
parse(code: string, opts: any = {}) {
return parser.parse(code, {
sourceType: 'module',
ecmaVersion: 'latest',
locations: true,
...opts
})
}
// 解析模块路径
async resolve(
id: string,
importer?: string,
options?: { skipSelf?: boolean }
) {
let skip: Set<Plugin> | undefined
if (options?.skipSelf && this._activePlugin) {
skip = new Set(this._resolveSkips)
skip.add(this._activePlugin)
}
let out = await container.resolveId(id, importer, { skip, ssr: this.ssr })
if (typeof out === 'string') out = { id: out }
return out as ResolvedId | null
}

// 以下两个方法均从 Vite 的模块依赖图中获取相关的信息
// 我们将在下一节详细介绍模块依赖图,本节不做展开
getModuleInfo(id: string) {
return getModuleInfo(id)
}

getModuleIds() {
return moduleGraph
? moduleGraph.idToModuleMap.keys()
: Array.prototype[Symbol.iterator]()
}

// 记录开发阶段 watch 的文件
addWatchFile(id: string) {
watchFiles.add(id)
;(this._addedImports || (this._addedImports = new Set())).add(id)
if (watcher) ensureWatchedFile(watcher, id, root)
}

getWatchFiles() {
return [...watchFiles]
}

warn() {
// 打印 warning 信息
}

error() {
// 打印 error 信息
}

// 其它方法只是声明,并没有具体实现,这里就省略了
}

五、resolvePlugins


resolvePlugins 生成插件流水线

5.1 实现

让我们把目光集中在 resolvePlugins 的实现上,Vite 所有的插件就是在这里被收集起来的。具体实现如下:

export async function resolvePlugins(
config: ResolvedConfig,
prePlugins: Plugin[],
normalPlugins: Plugin[],
postPlugins: Plugin[]
): Promise<Plugin[]> {
const isBuild = config.command === 'build'
// 收集生产环境构建的插件,后文会介绍
const buildPlugins = isBuild
? (await import('../build')).resolveBuildPlugins(config)
: { pre: [], post: [] }

return [
// 1. 别名插件
isBuild ? null : preAliasPlugin(),
aliasPlugin({ entries: config.resolve.alias }),
// 2. 用户自定义 pre 插件(带有`enforce: "pre"`属性)
...prePlugins,
// 3. Vite 核心构建插件
// 数量比较多,暂时省略代码
// 4. 用户插件(不带有 `enforce` 属性)
...normalPlugins,
// 5. Vite 生产环境插件 & 用户插件(带有 `enforce: "post"`属性)
definePlugin(config),
cssPostPlugin(config),
...buildPlugins.pre,
...postPlugins,
...buildPlugins.post,
// 6. 一些开发阶段特有的插件
...(isBuild
? []
: [clientInjectionsPlugin(config), importAnalysisPlugin(config)])
].filter(Boolean) as Plugin[]
}

从上述代码中我们可以总结出 Vite 插件的具体执行顺序:

  1. 别名插件包括 vite:pre-alias 和@ rollup/plugin-alias,用于路径别名替换

  2. 用户自定义 pre 插件,也就是带有 enforce: "pre" 属性的自定义插件

  3. Vite 核心构建插件,这部分插件为 Vite 的核心编译插件,数量比较多,我们在下部分一一拆解

  4. 用户自定义的普通插件,即不带有 enforce 属性的自定义插件

  5. Vite 生产环境插件和用户插件中带有 enforce: "post" 属性的插件

  6. 一些开发阶段特有的插件,包括环境变量注入插件 clientInjectionsPluginimport 语句分析及重写插件importAnalysisPlugin