本文旨在分析vite plugin模块。plugin是vite 的核心功能,通过plugin实现了预构建资源路径替换、解析alias、转译js、转译css、注入define、注入hmr辅助代码等功能。
你将知道
-
plugin的各个hook函数的作用。
-
vite独有的hook函数的执行时期。
-
内置的插件如何使vite对各种文件开箱即用。
-
所有插件集中之后的各个hook函数的使用流程。
vite插件基于rollup插件,插件的hook函数返回值和参数类型完全依照rollup,但并没有全部接受rollup的hook函数。目前只使用了7个hook函数。hook的参数与返回值可以从rollup官网获取到。同时vite提供了5个独有的hook函数。
rollup的hook分四个类型
-
async: 处理promise的异步钩子。
-
first: 如果多个插件实现了相同的钩子函数,那么会串式执行,从头到尾,但是,如果其中某个的返回值不是null也不是undefined的话,会直接终止掉后续插件。
-
sequential: 如果多个插件实现了相同的钩子函数,那么会串式执行,按照使用插件的顺序从头到尾执行,如果是异步的,会等待之前处理完毕,在执行下一个插件。
-
parallel: 同上,不过如果某个插件是异步的,其后的插件不会等待,而是并行执行。
一个完整的插件例子
插件就是一个实现了各个hook的对象。按照hook使用顺序如下排列:
const myPlugin = {
name: 'my-plugin', // 必须的插件标志
apply: 'build' | 'serve', // 表明此插件应用在何种模式
enforce: 'post' | 'pre', // 插件排序
// rollup通用插件,ctx是一个plugins集合的上下文
options(ctx,pluginOptions) {
// 返回plugin opthons ,类型:async, sequential
return somePluginOptions
},
buildStart(ctx,pluginOptions) {
// 在服务启动前开始执行,类型:async, parallel
// ...do something
},
resolveId(ctx,srouce, importer, pluginOptions) {
// srouce为资源的路径,importer为引入此资源的文件
// 如果有返回值,则将替换掉importer中引入的路径,同时将返回值传递给其他hook
// 类型 async, first
// ...do something
return srouce
},
load(ctx,id, srr) {
// id为resolveId返回的值
// 加载资源并返回 类型 async, first
// ...do something
return code
},
transform(ctx,code, id, ssr) {
// code为load返回的值,id为resolveId返回的值
// 转译code并返回转译结果 类型 async, first,
// ...do something
return transformCode
},
buildEnd(err) {
// 构建结束的回调,可以捕获错误。类型 async, parallel
},
closeBundle() {
// 构建结束的最终回调,类型 async, parallel
},
// vite 独有插件
config(config, env) {
// 返回一个配置对象,merge到最终config中
// 类型 sync, sequential
return config
},
configResolved(config) {
// 解析 Vite 配置后调用 类型 sync, parallel
},
configureServer(server) {
// 服务器配置完后的hook 类型 sync, paralle
},
transformIndexHtml() {
// 转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文
// 类型 async, sequential
},
handleHotUpdate(HmrContext) {
// 触发热更新时的hook,可以更加精确的控制hmr
// 类型
}
}
源码分析
解析插件入口
插件的解析步骤发生在resolveConfig过程中,在我的第一篇vite源码分析里有提过,现在着重讲解plugin的解析:
async function resolveConfig(inlineConfig, ...args) {
let config = inlineConfig; // 配置参数对象
// ...伪代码
// 首先扁平化plugins数组,所以你的plugin配置这样写也是可以的😂:[[pulginA,pulginB],pulginC]
// 筛选应用apply设置应用场景(serve|build)的插件
const rawUserPlugins = (config.plugins || []).flat().filter((p) => {
return p && (!p.apply || p.apply === command);
});
// sortUserPlugins函数根据enforce字段对插件进行排序,不再讲解
// 源码:https://github.com/vitejs/vite/blob/5745a2e8072cb92d647662dc387e7f12b2841cab/packages/vite/src/node/config.ts#L652
const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins);
// 执行plugin.config hook,可再次设置配置参数
// mergeConfig函数合并参数,不再讲解,源码:https://github.com/vitejs/vite/blob/5745a2e8072cb92d647662dc387e7f12b2841cab/packages/vite/src/node/config.ts#L573
const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins];
for (const p of userPlugins) {
if (p.config) {
const res = await p.config(config, configEnv);
if (res) {
config = mergeConfig(config, res);
}
}
}
// 利用createPluginContainer函数创建一个用于特殊场景的内部解析器,比如解析css @import ,预构建依赖等
// createPluginContainer往下会讲解,源码:https://github.com/vitejs/vite/blob/5745a2e8072cb92d647662dc387e7f12b2841cab/packages/vite/src/node/server/pluginContainer.ts#L121
const createResolver = (options) => {
return async (id, importer, aliasOnly) => {
let container = await createPluginContainer(...args);
return (await container.resolveId(id, importer))?.id;
};
};
// 最终参数配置对象
const resolved = {
// ...其他配置
plugins: userPlugins,
createResolver,
};
// resolvePlugins函数添加vite内部插件,使完成各功能开箱即用。
// resolvePlugins函数往下会讲解,源码:https://github.com/vitejs/vite/blob/5745a2e8072cb92d647662dc387e7f12b2841cab/packages/vite/src/node/plugins/index.ts#L18
resolved.plugins = await resolvePlugins(resolved, prePlugins, normalPlugins, postPlugins);
// 执行plugin.configResolved hook
await Promise.all(userPlugins.map((p) => p.configResolved?.(resolved)));
// ...伪代码
}
resolvePlugins
加入的内置插件基本对应官网所提供的功能
async function resolvePlugins(config, prePlugins, normalPlugins, postPlugins) {
const isBuild = config.command === 'build';
// 加载build相关插件
const buildPlugins = isBuild
? (await import('../build')).resolveBuildPlugins(config)
: { pre: [], post: [] };
return [
// 在我的第二篇源码分析提到过此插件,作用是拦截已经预构建后的依赖名称,并返回修改后的名称,详情可看我的第二篇文章。
isBuild ? null : preAliasPlugin(),
aliasPlugin({ entries: config.resolve.alias }),// 别名解析
...prePlugins,// plugin.enforce=pre的用户插件
dynamicImportPolyfillPlugin(config),// 插入动态导入polyfill
resolvePlugin(...args),// 解析各资源的路径
htmlInlineScriptProxyPlugin(),//
cssPlugin(config),// 解析css
config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,// 用户配置的esbuild插件
jsonPlugin(...args),// 解析json
wasmPlugin(config),// 解析Web Assembly
webWorkerPlugin(config),// 解析web worker
assetPlugin(config),// 解析静态资源
...normalPlugins,// 用户插件
definePlugin(config),// 解析全局常量
cssPostPlugin(config),// 解析css post
...buildPlugins.pre,//
...postPlugins,
...buildPlugins.post,
// 热更新客户端资源插入和import 分析,如import.meta.glob,import.meta.hot等
...(isBuild ? [] : [clientInjectionsPlugin(config), importAnalysisPlugin(config)]),
].filter(Boolean)
}
createPluginContainer
此函数就是将所有的插件的hook函数集中起来统一执行处理,并根据hook类型决定如何返回值。
export async function createPluginContainer({ plugins, rollupOptions }: ResolvedConfig) {
// ...伪代码
// plugins的上下文
class Context implements PluginContext {}
// transform hooks的上下文
class TransformContext extends Context {}
const container = {
// 注意,options是一个立即调用函数,你的插件的options hook可以直接是配置对象,也可以是返回配置对象的函数
options: await (async () => {
for (const plugin of plugins) {
if (!plugin.options) continue;
options = (await plugin.options(...args)) || options;
}
})(),
async buildStart() {
await Promise.all(
plugins.map((plugin) => {
if (plugin.buildStart) {
return plugin.buildStart.call(new Context(plugin), ...args);
}
}),
);
},
async resolveId(...args) {
const ctx = new Context();
for (const plugin of plugins) {
if (!plugin.resolveId) continue;
const result = await plugin.resolveId.call(ctx, ...args);
if (!result) continue;
}
},
async load(...args) {
for (const plugin of plugins) {
if (!plugin.load) continue;
const result = await plugin.load.call(ctx, ...args);
}
},
async transform(...args) {
const ctx = new TransformContext(...args);
for (const plugin of plugins) {
result = await plugin.transform.call(ctx, ...args);
}
},
watchChange(...args) {
const ctx = new Context();
for (const plugin of plugins) {
if (!plugin.watchChange) continue;
plugin.watchChange.call(ctx,...args);
}
},
async close() {
if (closed) return;
const ctx = new Context();
await Promise.all(plugins.map((p) => p.buildEnd && p.buildEnd.call(ctx)));
await Promise.all(plugins.map((p) => p.closeBundle && p.closeBundle.call(ctx)));
closed = true;
},
};
return container;
}
其余vite 独有hook执行地点
configureServer
configureServer hook在配置server参数结束后执行。
async function resolveConfig(...args){
// ...伪代码
// ...配置server
const postHooks: ((() => void) | void)[] = []
for (const plugin of plugins) {
if (plugin.configureServer) {
postHooks.push(await plugin.configureServer(server))
}
}
}
tansformIndexHtml
tansformIndexHtml hook在转换index.html时使用,这里只给出源码位置。
handleHotUpdate
热更新时触发的hook,可以更精确地控制hmr行为。触发点在监听文件变动时。
async function createServer(...args){
// ...伪代码
// ...配置server
// watcher,文件监听对象
watcher.on('change', async (file) => {
// ...伪代码
if (serverConfig.hmr !== false) {
await handleHMRUpdate(file, server)
}
})
}
插件使用流程
客户端请求服务器资源后,在返回资源过程中,将资源的名称、内容等信息传入由createPluginContainer创建的plugins集合中,执行相关hook,再返回给客户端。