《vite技术揭秘、还原与实战》第9节--在svite中引入插件系统

前言

上一节,我们结合 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的形式对功能进行分类和组织

相关推荐
red润2 天前
Vue3DevTools7是如何在vscode定位指定文件位置的?
前端·vue.js·vite
小许_.7 天前
vite+vue3快速构建项目+router、vuex、scss安装
前端·css·vue3·vite·scss
唯之为之9 天前
vue3项目部署到Github
vue·github·pnpm·vue3·vite
williamdsy13 天前
【vite-plugin-vuetify】自动导入 vuetify 组件和指令
vite·plugin·vuetify·自动导入
whyfail17 天前
在 Vite 项目中自动为每个 Vue 文件导入 base.less
前端·less·vite
har01d19 天前
vue3,扫雷
开发语言·javascript·vue·ecmascript·vite
Vgbire1 个月前
我把前端部署用户提醒做成了一个npm包
前端·webpack·性能优化·npm·vite·打包优化·webpack优化
威哥爱编程1 个月前
为什么用Vite框架?来看它的核心组件案例详解
前端·javascript·vite
站在顶峰看星星1 个月前
React + Vite项目别名配置
前端·webpack·typescript·vue·react·vite·angular
奶昔不会射手1 个月前
nuxt3学习
前端·vite·nuxt3