认识
一、认识
Vite
在开发阶段实现了一个按需加载的服务器,每一个文件请求进来都会经历一系列的编译流程,然后 Vite
会将编译结果响应给浏览器。在生产环境下,Vite
同样会执行一系列编译过程,将编译结果交给 Rollup
进行模块打包。这一系列的编译过程指的就是 Vite
的插件工作流水线(Pipeline
),
我们可以看到:
-
在生产环境中
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.emitFile
、this.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
插件的具体执行顺序:
-
别名插件包括
vite:pre-alias
和@rollup/plugin-alias
,用于路径别名替换 -
用户自定义
pre
插件,也就是带有enforce: "pre"
属性的自定义插件 -
Vite
核心构建插件,这部分插件为Vite
的核心编译插件,数量比较多,我们在下部分一一拆解 -
用户自定义的普通插件,即不带有
enforce
属性的自定义插件 -
Vite
生产环境插件和用户插件中带有enforce: "post"
属性的插件 -
一些开发阶段特有的插件,包括环境变量注入插件
clientInjectionsPlugin
和import
语句分析及重写插件importAnalysisPlugin