前言
上一节,我们结合 vite 源码分析了实现插件化所需的核心要素
本节我们将以此作为理论基础在 svite 中引入插件系统
好文推荐
源码获取
更新进度
公众号:更新至第20
节
博客:更新至第9
节
公众号
代码实现
- 约定插件注册机制和 plugin 基本格式
我们选择在 svite.config.ts 配置文件中配置 plugins 来作为外部插件的注册入口,它通过数组接收多个 plugin
如下,暂时我们不与 vite 保持一致,因为 vite 中部分 hook,比如 transformIndexHtml 和 handleHotUpdate 等暂时还用不到,等开发此部分功能时再进行补充,目前的 TypeScript 定义如下
ts
// packages\vite\src\node\plugin.ts
type LoadResult = null | string;
type TransformResult = null | string;
type PluginContext = any;
type TransformPluginContext = any;
export interface Plugin {
// 指定在所有插件中的运行阶段
enforce?: "pre" | "post";
// 服务器启动时
buildStart?: (this: PluginContext) => void;
// 路径解析
resolveId?: (
this: PluginContext,
source: string,
importer: string | undefined
) => Promise<string> | string;
// 模块加载
load?: (this: PluginContext, id: string) => Promise<LoadResult> | LoadResult;
// 模块转换
transform?: (
this: TransformPluginContext,
code: string,
id: string
) => Promise<TransformResult> | TransformResult;
}
- 根据插件的 enforce 属性进行归类
在配置文件一节中,已经定义了 resolveConfig 来对用户侧的配置项做处理,因此找到该函数,它在 packages\vite\src\node\config.ts 中
如下,定义 resolvePlugins 来单独处理
ts
export async function resolveConfig(userConf: UserConfig) {
const internalConf = {};
const conf = {
...userConf,
...internalConf,
};
const userConfig = await parseConfigFile(conf);
const resolved: ResolvedConfig = {
...conf,
...userConfig,
plugins: await resolvePlugins(conf.plugins || []),
};
return resolved;
}
进入 resolvePlugins 函数
ts
async function resolvePlugins(userPlugins:readonly Plugin[]){
...
}
第一步,我们需要兼容下用户侧意外的使用了异步函数来生成 plugin 配置项的情况,如下,根据 PromiseA+规范,递归的去检查是否具有.then 属性,有就使用 await 对其进行求值以得到配置项
ts
export async function asyncFlatten<T>(arr: T[]): Promise<T[]> {
do {
arr = (await Promise.all(arr)).flat(Infinity) as any;
} while (arr.some((v: any) => v?.then));
return arr;
}
接下来,我们需要按照 enforce 属性对 plugins 进行排序,将其分组成三个数组:pre、normal、post
ts
export function sortUserPlugins(
plugins: (Plugin | Plugin[])[] | undefined
): [Plugin[], Plugin[], Plugin[]] {
const prePlugins: Plugin[] = [];
const postPlugins: Plugin[] = [];
const normalPlugins: Plugin[] = [];
if (plugins) {
plugins.flat().forEach((p) => {
if (p.enforce === "pre") prePlugins.push(p);
else if (p.enforce === "post") postPlugins.push(p);
else normalPlugins.push(p);
});
}
return [prePlugins, normalPlugins, postPlugins];
}
- 与内置 plugin 合并
在实现了插件化的主体应用程序中,某个特定功能的实现一般都会以 plugin 的形式提供,主程序只负责调度就好了。因此我们在 packages\vite\src\node 文件夹下新建 plugins 文件夹,后续 svite 的所有的内置插件都定义在这里
接着,在 plugins 下新建 index.ts 文件,用以作为内置插件的入口,它将导入并导出内置插件并提供一些插件相关的公共接口。如下,我们通过 resolvePlugins 得到一个合并排序后的插件列表。后续当我们增加特定的功能(比如 alisa 别名)时就可以在该接口的对应位置中合并进去
ts
export async function resolvePlugins(
prePlugins: Plugin[],
normalPlugins: Plugin[],
postPlugins: Plugin[]
): Promise<Plugin[]> {
return [...prePlugins, ...normalPlugins, ...postPlugins];
}
- 插件上下文
上下文在应用程序设计中扮演着十分重要的角色:状态的管理与传递、数据的共享、事件的跨模块调用等,同时也有效促进了代码的可维护性和可扩展性。因此,我们在 packages\vite\src\node\server 文件夹下新建 pluginContainer.ts 文件来处理上下文的创建
目前来说,它只有如下四个 hook 供跨模块调用
ts
export interface PluginContainer {
buildStart(): Promise<void>;
resolveId(id: string, importer?: string): Promise<NullValue>;
transform(code: string, id: string): Promise<{ code: string }>;
load(id: string): Promise<string | NullValue>;
}
由于我们的 plugin 会有 n 多个,每一个插件都是一个完整的生命周期,因此我们没法统一将状态的管理设计到 PluginContainer 上,就像 vue 中的 data 被设计成函数来避免状态污染一样,我们也需要单独对状态进行维护和管理
ts
class Context {
_activePlugin: Plugin | null = null;
_activeId: string = "";
_activeCode:string="";
constructor(initialPlugin?: Plugin) {
this._activePlugin = initialPlugin || null;
}
}
现在我们的上下文创建函数长这样
ts
export async function createPluginContainer(
plugins: Plugin[]
): Promise<PluginContainer> {
class Context {
_activePlugin: Plugin | null = null;
_activeId: string = "";
_activeCode:string="";
}
const container: PluginContainer = {
async buildStart() {},
async resolveId() {},
async transform() {
return {
code: "",
};
},
async load() {},
};
return container;
}
最后我们将其挂载到 server 上,它在 packages\vite\src\node\server\index.ts
ts
async function _createServer(userConfig:UserConfig){
...
const container = await createPluginContainer(config.plugins)
const server:ViteDevServer = {
...
pluginContainer:container,
...
}
return server
}
- 辅助函数
在上一节我们提到过,vite 会收集所有插件的指定 hook 然后按照对应的 hook 类型进行调用,因此我们在具体实现 hook 之前,还需要将这一部分代码提取为公共函数
找到 packages\vite\src\node\plugins\index.ts 文件,它应该导出两个函数:getSortedPlugins 用于收集包含指定 hook 的插件,getSortedPluginHooks 用于获取指定 hook 组成的列表
ts
export function createPluginHookUtils(
plugins: readonly Plugin[]
): PluginHookUtils {
function getSortedPlugins() {}
function getSortedPluginHooks() {}
return {
getSortedPlugins,
getSortedPluginHooks,
};
}
现在来看 getSortedPlugins,由于 createPluginHookUtils 生成的闭包会导致作用域被保留,故我们使用 sortedPluginsCache 来实现缓存以提高效率
ts
const sortedPluginsCache = new Map<keyof Plugin, Plugin[]>();
function getSortedPlugins(hookName: keyof Plugin): Plugin[] {
if (sortedPluginsCache.has(hookName))
return sortedPluginsCache.get(hookName)!;
const sorted = getSortedPluginsByHook(hookName, plugins);
// 推入缓存
sortedPluginsCache.set(hookName, sorted);
return sorted;
}
而 getSortedPluginsByHook 即拿到定义了指定 hook 的 plugin 后,按照 enforce 属性定义的值对插件进行分组
ts
export function getSortedPluginsByHook(
hookName: keyof Plugin,
plugins: readonly Plugin[]
): Plugin[] {
const pre: Plugin[] = [];
const normal: Plugin[] = [];
const post: Plugin[] = [];
for (const plugin of plugins) {
const enforce = plugin.enforce;
const hook = plugin[hookName];
// plugin中定义了指定的hook
if (hook) {
if (enforce === "pre") {
pre.push(plugin);
continue;
}
if (enforce === "post") {
post.push(plugin);
continue;
}
normal.push(plugin);
}
}
return [...pre, ...normal, ...post];
}
最后我们来看 getSortedPluginHooks,它对 getSortedPlugins 返回的符合指定 hook 的插件列表做 map,最终返回的是由 hook 组成的列表而非插件列表
ts
function getSortedPluginHooks<K extends keyof Plugin>(
hookName: K
): NonNullable<Plugin[K]>[] {
// 拿到定义了指定hookName的所有plugin
const plugins = getSortedPlugins(hookName);
return plugins
.map((p) => {
const hook = p[hookName]!;
return hook;
})
.filter(Boolean);
}
最后在创建上下文阶段将这两个辅助函数暴露出来
ts
export async function createPluginContainer(
plugins: readonly Plugin[]
): Promise<PluginContainer> {
const { getSortedPlugins,getSortedPluginHooks } = createPluginHookUtils(plugins);
...
}
- hooks 的设计与实现
现在,我们只需要在需要的时候通过 server.pluginContainer 即可去调用对应 hook,不过,目前我们的 hook 还都是空函数
- buildStart
按照我们在"插件化方案设计"中对 hook 的分类,buildStart 属于 Parallel 类型,它应该跳出 JavaScript 单线程的限制,如下伪代码,当不是 Promise 时,我们使用 Promise.then 进行下包装
ts
if (hook instanceof Promise) {
hook.then();
} else {
Promise.then(hook);
}
在 vite 中,buildStart 还支持指定 sequential 来强制性逐个执行,我们 svite 暂时不考虑支持该特性,但是对这部分逻辑我们要先处理下
如下,我们使用 getSortedPlugins 将指定了 buildStart 钩子的插件挑选出来进行遍历,然后 push 到 parallelPromises,最后借助 Promise.all 并发执行
ts
async function hookParallel<H extends keyof PluginHooks>(
hookName: H,
context: (plugin: Plugin) => ThisType<Context>,
args: (plugin: Plugin) => Parameters<PluginHooks[H]>
): Promise<void> {
const parallelPromises: any[] = [];
for (const plugin of getSortedPlugins(hookName)) {
const hook = plugin[hookName];
if (typeof hook !== "function") continue;
// 指定了sequential后取一个run一个
if (plugin.sequential) {
await Promise.all(parallelPromises);
parallelPromises.length = 0;
// 使用await等待
await hook!.apply(context(plugin), args(plugin));
} else {
parallelPromises.push(hook!.apply(context(plugin), args(plugin)));
}
}
await Promise.all(parallelPromises);
}
但需要注意的是,尽管插件之间的 buildStart 是并发的,但是我们仍然要保证整个插件的生命周期是有先后顺序的,因此,我们还需要定义 handleHookPromise 来进行下处理
ts
const processesing = new Set<Promise<any>>();
function handleHookPromise<T>(maybePromise: undefined | T | Promise<T>) {
if (!(maybePromise as any)?.then) {
return maybePromise;
}
const promise = maybePromise as Promise<T>;
processesing.add(promise);
// parallelPromises全部处理完毕
return promise.finally(() => processesing.delete(promise));
}
最后,我们在 buildStart 中进行调用即可
ts
const container: PluginContainer = {
async buildStart() {
await handleHookPromise(
hookParallel(
"buildStart",
(plugin) => new Context(plugin),
() => []
)
);
},
...
};
- resolveId
按照我们在"插件化方案设计"中对 hook 的分类,resolveId 属于 First 类型,它必须串行调用,并且当遇到第一个有返回值的 hook 时就需要终止后续钩子的执行
ts
const container: PluginContainer = {
async resolveId(rawId, importer) {
const ctx = new Context();
let overrideId:string|undefined
for (const plugin of getSortedPlugins("resolveId")) {
if (!plugin.resolveId) continue;
ctx._activePlugin = plugin;
const hook = plugin.resolveId!;
const result = await handleHookPromise(
hook.call(ctx as any, rawId, importer)
);
// 如果当前hook未返回结果,则依次调用下一个hook
if (!result) continue;
overrideId = result
break;
}
return overrideId || undefined
},
...
};
- transform
按照我们在"插件化方案设计"中对 hook 的分类,transform 属于 Sequential 类型,它必须串行调用,并且各个 plugin 之间相互影响,前一个 hook 的返回值将被作为后一个 hook 的输入值
ts
const container: PluginContainer = {
async transform(code, id) {
const ctx = new Context();
for (const plugin of getSortedPlugins("transform")) {
if (!plugin.transform) continue;
ctx._activePlugin = plugin;
ctx._activeId = id;
ctx._activeCode = code;
let result: string | undefined | null;
const hook = plugin.transform!;
// 将上一次的code继续传递给下一个hook
result = await handleHookPromise(hook.call(ctx as any, code, id));
if (!result) continue;
// 记录上一个hook的结果
code = result;
}
return {
code
};
},
...
};
- load
按照我们在"插件化方案设计"中对 hook 的分类,load 属于 First 类型,它必须串行调用
ts
const container: PluginContainer = {
async load(id) {
const ctx = new Context();
for (const plugin of getSortedPlugins("load")) {
if (!plugin.load) continue;
ctx._activePlugin = plugin;
const hook = plugin.load!;
const result = await handleHookPromise(hook.call(ctx as any, id));
if (result != null) {
return result;
}
}
return null;
},
...
};
总结
本节,我们为svite引入了插件机制,后续在进行开发过程中,我们将以plugin的形式对功能进行分类和组织