跳到主要内容

插件指南

Garfish 框架引入了插件化机制,目的是为了让开发者能够通过编写插件的方式扩展更多功能,或为自身业务定制个性化功能;同时框架的基础能力也都是通过插件机制来实现,确保框架核心足够精简和稳定。

插件能做什么

插件的功能范围没有严格的限制——一般有下面两种:

  1. 添加全局方法或增加默认参数
  2. 在应用的生命周期中自定义功能(例如:Garfish routerGarfish sandbox

编写插件

Garfish Router 增加了全局方法和应用的自动渲染和销毁能力,下面让我们来以 Garfish router 为例,如何编写一个插件,来实现路由的能力。

当插件被注册到 Garfish 框架时,将会调用插件函数并将 GarfishInstance 作为参数传递,函数的返回值中包括插件的基本信息:nameversion,除了基本信息外最重要的则是包括 hookGarfish 框架将在应用的整个生命周期中触发 hook 的调用,可以在 hook 中对信息进行二次处理或执行特定的功能。

让我们从编写插件函数开始,建议在单独的文件中创建它并将其导出,如下所示,以保持插件逻辑的整洁和分离,在实际开发过程中我们建议将实际插件的内容放置一个函数中返回,以便插件在实际调用时可接收参数

// plugins/router.ts
import type { interfaces } from 'garfish';
// function return plugin
export function GarfishRouter(_args?: any) {
// Plugin code goes here
return function (GarfishInstance: interfaces.Garfish): interfaces.Plugin {
return {
name: 'garfish-router',
version: '1.2.1',
// ...
};
};
}

Garfish Router 的这个 plugin 期望达到的目标是通过提供的 Router MapGarfish 框架能够自动的完成微前端应用的渲染和销毁调度,从而降低典型中台中管理应用销毁和渲染的工作,提升开发效率。那么要实现这个需求我们需要依次实现以下功能:

  • 扩展类型
    • appInfo 的类型,让 appInfo 类型提示支持 activeWhenbasename 等配置
    • Garfish 增加 router 类型
  • Garfish 实例扩展 router 方法,用于实现路由跳转和路由监听等能力
  • 监听 bootstrap hook(该 hook 会在主应用触发 Garfish.run 后调用),触发 bootstrap
    • 劫持路由变化:改写 history.pushhistory.replace,监听 popstate 浏览器后退事件
    • 当路由发生变化时通过 appInfoactiveWhen 进行规则判断,对应用进行渲染和销毁
  • 监听 registerApp hook(该 hook 会在注册子应用时触发)
    • 当有新注册应用时对新应用进行检验是否如何渲染条件,进行销毁
提示

Garfish Router 就是通过 Garfish 的 Plugin 机制实现,以下案例精简了大部分逻辑,主要介绍如何编写插件来扩展 Garfish 的整体功能,若想了解实现,请参考 Garfish Router plugin

import type { interfaces } from 'garfish';
declare module 'garfish' {
// 为 GarfishInstance 添加 router 方法
export default interface Garfish {
router: {
push: (info: {
path: string;
query?: { [key: string]: string };
basename?: string;
}) => void;
replace: (info: {
path: string;
query?: { [key: string]: string };
basename?: string;
}) => void;
};
}

export namespace interfaces {
// 为全局配置增加 autoRefreshApp、onNotMatchRouter 参数类型
export interface Config {
onNotMatchRouter?: (path: string) => Promise<void> | void;
}

export interface AppInfo {
// 手动加载,可不填写路由
activeWhen?: string | ((path: string) => boolean);
basename?: string;
}
}
}

// 这里仅做伪代码的演示,功能无法正常运行
export function GarfishRouter(_args?: { autoRefreshApp?: boolean }) {
return function (Garfish: interfaces.Garfish): interfaces.Plugin {
// 为 Garfish 实例添加 router 方法
Garfish.router = {
push: ({ path }) => history.push(null, '', path),
replace: ({ path }) => history.replace(null, '', path),
};

return {
name: 'router',
version: '1.0.0',
// 在触发 Garfish.run 后启动路由监听,自动渲染和销毁应用
bootstrap(options: interfaces.Options) {
let activeApp = null;
const unmounts: Record<string, Function> = {};
const { basename } = options;

const apps = Object.values(Garfish.appInfos);

// 该函数会劫持 history 变化,当某个 appInfo 的 activeWhen 符合触发条件后会触发 active 回调
// 提供 appInfo 信息,这个时候通过 Garfish.loadApp 加载该应用并进行销毁
// 当某个 appInfo 处于已经渲染状态,并且在路由发生变化后处于销毁状态将会触发 deactive 回调
// 通过 appInfo,触发缓存的 app 实例的销毁函数
listenRouterAndReDirect({
basename,
active: async (appInfo: interfaces.AppInfo, rootPath: string) => {
const { name, cache = true, active } = appInfo;

// 当前应用处于激活状态后触发
const app = await Garfish.loadApp(appInfo.name, {
basename: rootPath,
entry: appInfo.entry,
cache: true,
domGetter: appInfo.domGetter,
});

if (app) {
const isDes = cache && app.mounted;
isDes ? await app.show() : await app.mount();

unmounts[name] = () => {
const isDes = cache && app.mounted;
isDes ? await app.show() : await app.mount();
};
}
},
deactive: async (appInfo: interfaces.AppInfo, rootPath: string) => {
const { name, deactive } = appInfo;
const unmount = unmounts[name];
unmount && unmount();
},
autoRefreshApp,
notMatch: onNotMatchRouter,
apps,
listening: true,
});
},

registerApp(appInfos) {
// 将新注册的应用信息注入到路由中
const appList = Object.values(appInfos);
router.registerRouter(appList.filter((app) => !!app.activeWhen));
// 触发路由的重定向,检测当前应用是否需要触发渲染
initRedirect();
},
};
};
}

插件编写总结

  • 若要为 Garfish 实例扩展方法,通过 declare module 直接扩展 Garfishinterfaces,然后通过插件函数获取 Garfish 的实例直接添加方法,用于扩展 Garfish 的能力
  • 可通过 namespace interfaces 直接扩展 Garfish configAppInfo 配置
  • 在对应用用的生命周期中进行能力的扩展

插件公约

  • 插件应该包括清晰的名称
  • 如果插件单独封装至 npm 包,在 package.json 中添加 garfish-plugin 关键词
  • 插件应该包括完备的测试
  • 插件应该具备完整的使用文档
  • 如果你觉得你的插件足够通用,请联系:zhouxiao.shaw@bytedance.com,评估后是否是和加入推荐列表

使用插件

通过调用 Garfish.usePlugin 方法将插件添加到你的应用程序中。

我们将使用在 如何编写插件 部分中创建的 routerPlugin 插件进行演示。

usePlugin() 方法第一个参数接收要安装的插件,在这种情况下为 routerPlugin 的返回值。

它还会自动阻止你多次使用同一插件,因此在同一插件上多次调用只会安装一次该插件,Garfish 内部通过插件执行后返回的 name 作为唯一标识来进行区分,在进行插件命名时,请确保不会和其他插件之间发生冲突。

import Garfish from 'garfish';
import routerPlugin from './plugins/router';

Garfish.usePlugin(routerPlugin());

usePlugin

通过 Garfish.usePlugin 可以注册插件

Garfish.usePlugin(plugin: (GarfishInstance: interfaces.Garfish)=> interfaces.Plugin)

plugin

name

  • Type: string
  • 插件的名称,作为插件的唯一标识和便于调试

version?

  • Type: string
  • 插件的版本号,用于观测线上环境使用使用的插件版本

beforeBootstrap?

  • Type: (options: interfaces.Options) => void
    • hook 的第一个参数为 Garfish.run 提供的配置信息
  • Kind: sync, sequential
  • Trigger:
    • Garfish.run 调用后触发
    • 触发该 hook 时配置未注册到全局

bootstrap?

  • Type: (options: interfaces.Options) => void
    • hook 的第一个参数为 Garfish.run 提供的配置信息
  • Kind: sync, sequential
  • Trigger:
    • Garfish.run 调用后触发
    • 触发该 hook 时配置已经注册到全局
  • Previous Hook: beforeBootstrap

beforeRegisterApp?

  • Type: (appInfo: interfaces.AppInfo | Array<interfaces.AppInfo>) => void
    • hook 的第一个参数为需要注册的应用信息
  • Kind: sync, sequential
  • Trigger:

registerApp?

  • Type: (appInfo: interfaces.AppInfo | Array<interfaces.AppInfo>) => void
    • hook 的第一个参数为需要注册的应用信息
  • Kind: sync, sequential
  • Trigger:

beforeLoad

  • Type: async (appInfo: AppInfo, appInstance: App) => false | undefined

    • hook 的参数分别为:应用信息、应用实例;
    • 当返回 false 时将中断子应用的加载及后续流程;
  • Kind: async, sequential

  • Trigger:

    • 在调用 Garfish.load 时触发该 hook
    • 子应用加载前触发,此时还未开始加载子应用资源;
  • 示例

Garfish.run({
...,
beforeLoad(appInfo) {
console.log('子应用开始加载', appInfo.name);
}

afterLoad

  • Type: async (appInfo: AppInfo, appInstance: interfaces.App) => void

  • hook 的参数分别为:应用信息、应用实例;

  • Kind: async, sequential

  • Trigger:

    • 在调用 Garfish.load 后并且子应用加载完成时触发该 hook
  • 示例

Garfish.run({
...,
afterLoad(appInfo) {
console.log('子应用加载完成', appInfo.name);
}
})

errorLoadApp

  • Type: (error: Error, appInfo: AppInfo, appInstance: interfaces.App) => void

    • hook 的参数分别为:error 实例、 appInfo 信息、appInstance 应用实例
    • 一旦设置该 hook,子应用加载错误不会 throw 到文档流中,全局错误监听将无法捕获到;
  • Kind: sync, sequential

  • Trigger:

    • 在调用 Garfish.load 过程中,并且加载失败时触发该 hook
  • 示例

Garfish.run({
...,
errorLoadApp(error, appInfo) {
console.log('子应用加载异常', appInfo.name);
console.error(error);
}
})

beforeMount

  • Type: (appInfo: AppInfo, appInstance: interfaces.App, cacheMode: boolean) => void

    • hook 的参数分别为:appInfo 信息、appInstance 应用实例、是否为 缓存模式 渲染和销毁
  • Kind: sync, sequential

  • Previous Hook: beforeEvalafterEval

  • Trigger:

    • 此时子应用资源准备完成,运行时环境初始化完成,准备开始渲染子应用 DOM 树;
    • 在调用 app.mountapp.show 触发该 hook,用户除了手动调用这两个方法外,Garfish Router 托管模式还会自动触发
      • 在使用 app.mount 渲染应用是 cacheModefalse
      • 在使用 app.show 渲染应用是 cacheModetrue
  • 示例

Garfish.run({
...,
beforeMount(appInfo) {
console.log('子应用开始渲染', appInfo.name);
}
})

afterMount

  • Type: (appInfo: AppInfo, appInstance: interfaces.App, cacheMode: boolean) => void

    • hook 的参数分别为:appInfo 信息、appInstance 应用实例、是否为 缓存模式 渲染和销毁
  • Kind: sync, sequential

  • Previous Hook: beforeLoadafterLoadbeforeMount

  • Trigger:

    • 此时子应用 DOM 树已渲染完成,garfish 实例 activeApps 中已添加当前子应用 app 实例;
    • 在挂载过程中,会调用应用生命周期中的 render 函数,用户可在挂载前定义相关操作;
    • 若挂载过程中出现异常,会触发 errorMountApp,同时会清除已创建的 app 渲染容器 appContainer
  • 示例

Garfish.run({
...,
afterMount(appInfo) {
console.log('子应用渲染结束', appInfo.name);
}
})

beforeEval

  • Type: (appInfo: AppInfo, code: string, env: Record<string, any>, url: string, options) => void

    • hook 的参数分别为:appInfo 信息、code 执行的代码、env 要注入的环境变量,url 代码的资源地址、options 参数选项(例如 async 是否异步执行、noEntry 是否是 noEntry 模式);
  • Kind: sync, sequential

  • Previous Hook: beforeMount

  • Trigger:

    • 在子应用挂载过程中、实际执行代码前触发该 hook;
    • 应用 html 内的 script 和动态创建的脚本执行时都会触发该 hook
    • 此时 DOM 树已添加至文档流中,子应用代码准备执行;
    • 若代码执行过程中抛出异常,则将触发 errorMountApp,否则触发 beforeEval
  • 示例

Garfish.run({
...,
beforeEval(appInfo) {
console.log('子应用代码开始执行', appInfo.name);
}
})

afterEval

  • Type: (appInfo: AppInfo, code: string, env: Record<string, any>, url: string, options) => void

    • hook 的参数分别为:appInfo 信息、code 执行的代码、env 要注入的环境变量,url 应用访问地址、options 参数选项例如 async 是否异步执行、noEntry 是否是 noEntry 模式;
  • Kind: sync, sequential

  • Previous Hook: beforeLoadafterLoad

  • Trigger:

    • 在实际执行代码后。afterMount 触发前触发;
    • 子应用 html 内的 script 和动态创建的脚本执行时都会触发该 hook
  • 示例

Garfish.run({
...,
afterEval(appInfo) {
console.log('子应用代码执行完成', appInfo.name);
}
})

errorMountApp

  • Type: (error: Error, appInfo: AppInfo, appInstance: interfaces.App) => void

    • 一旦设置该 hook,子应用加载错误不会 throw 到文档流中,全局错误监听将无法捕获到;
  • Kind: sync, sequential

  • Previous Hook: beforeLoadafterLoadbeforeMountafterMount

  • Trigger:

    • 在渲染过程中出现异常会触发该 hook,子应用同步执行的代码出现异常会触发该 hook,异步代码无法触发
  • 示例

Garfish.run({
...,
errorMountApp(error, appInfo) {
console.log('子应用渲染异常', appInfo.name);
console.error(error);
}
})

beforeUnmount

  • Type: ( appInfo: AppInfo, appInstance: interfaces.App) => void
  • Kind: sync, sequential
  • Previous Hook: beforeLoadafterLoadbeforeMountafterMount
  • Trigger:
    • 在调用 app.unmountapp.hide 触发该 hook,用户除了手动调用这两个方法外,Garfish Router 托管模式还会自动触发
      • 在使用 app.unmount 渲染应用是 cacheModefalse
      • 在使用 app.hide 渲染应用是 cacheModetrue
    • 此时子应用 DOM 元素还未卸载,副作用尚未清除;
    • 此时子应用 DOM 树已渲染完成,garfish 实例 activeApps 中已添加当前子应用 app 实例;

afterUnmount

  • Type: ( appInfo: AppInfo, appInstance: interfaces.App) => void
  • Kind: sync, sequential
  • Trigger:
    • 此时,应用在渲和运行过程中产生的副作用已清除,DOM 已卸载,沙箱副作用已清除,garfish 实例 activeApps 当前 app 已移除;
    • 在应用销毁过程中会调用应用生命周期中的 destory 函数,用户可在销毁前定义相关操作;
    • 若应用卸载过程中出现异常,会触发 errorUnmountApp

errorUnmountApp

  • Type: (error: Error, appInfo: AppInfo, appInstance: interfaces.App)=> void

    • 一旦设置该 hook,子应用销毁错误不会向上 throw 到文档流中,全局错误监听将无法捕获到;
  • Kind: sync, sequential

  • Trigger:

    • app.unmountapp.hide 销毁过程中出现异常则会触发该 hook,用户除了手动调用这两个方法外,Garfish Router 托管模式还会自动触发
  • 示例

Garfish.run({
...,
errorUnmountApp(error, appInfo) {
console.log('子应用销毁异常', appInfo.name);
console.error(error);
}
})

onNotMatchRouter

  • Type: (path: string)=> void

    • hook 的参数分别为:应用信息、应用实例;
  • Kind: sync, sequential

  • Trigger:

    • 路由发生变化当前未激活子应用且未匹配到任何子应用时触发
  • 示例

Garfish.run({
...,
onNotMatchRouter(path) {
console.log('未匹配到子应用', path);
}
})