插件指南
Garfish 框架引入了插件化机制,目的是为了让开发者能够通过编写插件的方式扩展更多功能,或为自身业务定制个性化功能;同时框架的基础能力也都是通过插件机制来实现,确保框架核心足够精简和稳定。
插件能做什么
插件的功能范围没有严格的限制——一般有下面两种:
- 添加全局方法或增加默认参数
- 在应用的生命周期中自定义功能(例如:
Garfish router
、Garfish sandbox
)
编写插件
Garfish Router
增加了全局方法和应用的自动渲染和销毁能力,下面让我们来以 Garfish router
为例,如何编写一个插件,来实现路由的能力。
当插件被注册到 Garfish
框架时,将会调用插件函数并将 GarfishInstance
作为参数传递,函数的返回值中包括插件的基本信息:name
、version
,除了基本信息外最重要的则是包括 hook
,Garfish
框架将在应用的整个生命周期中触发 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 Map
后 Garfish
框架能够自动的完成微前端应用的渲染和销毁调度,从而降低典型中台中管理应用销毁和渲染的工作,提升开发效率。那么要实现这个需求我们需要依次实现以下功能:
- 扩展类型
appInfo
的类型,让appInfo
类型提示支持activeWhen
、basename
等配置Garfish
增加router
类型
- 为
Garfish
实例扩展router
方法,用于实现路由跳转和路由监听等能力 - 监听
bootstrap hook
(该 hook 会在主应用触发Garfish.run
后调用),触发bootstrap
后- 劫持路由变化:改写
history.push
、history.replace
,监听popstate
浏览器后退事件 - 当路由发生变化时通过
appInfo
的activeWhen
进行规则判断,对应用进行渲染和销毁
- 劫持路由变化:改写
- 监听
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
直接扩展Garfish
的interfaces
,然后通过插件函数获取Garfish
的实例直接添加方法,用于扩展Garfish
的能力 - 可通过
namespace interfaces
直接扩展Garfish config
和AppInfo
配置 - 在对应用用的生命周期中进行能力的扩展
插件公约
- 插件应该包括清晰的名称
- 如果插件单独封装至
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:
- 调用
Garfish.run
且,提供了apps
参数时触发 - 直接通过
Garfish.registerApp
调用时 - 触发该
hook
是子应用信息尚未注册成功
- 调用
registerApp?
- Type:
(appInfo: interfaces.AppInfo | Array<interfaces.AppInfo>) => void
- 该
hook
的第一个参数为需要注册的应用信息
- 该
- Kind:
sync
,sequential
- Trigger:
- 调用
Garfish.run
且,提供了apps
参数时触发 - 直接通过
Garfish.registerApp
调用时 - 触发该
hook
是子应用信息注册成功
- 调用
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:
beforeEval
、afterEval
Trigger:
- 此时子应用资源准备完成,运行时环境初始化完成,准备开始渲染子应用 DOM 树;
- 在调用
app.mount
或app.show
触发该hook
,用户除了手动调用这两个方法外,Garfish Router
托管模式还会自动触发- 在使用
app.mount
渲染应用是cacheMode
为false
; - 在使用
app.show
渲染应用是cacheMode
为true
;
- 在使用
示例
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:
beforeLoad
、afterLoad
、beforeMount
Trigger:
- 此时子应用 DOM 树已渲染完成,garfish 实例
activeApps
中已添加当前子应用 app 实例; - 在挂载过程中,会调用应用生命周期中的
render
函数,用户可在挂载前定义相关操作; - 若挂载过程中出现异常,会触发 errorMountApp,同时会清除已创建的 app 渲染容器 appContainer
- 此时子应用 DOM 树已渲染完成,garfish 实例
示例
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:
beforeLoad
、afterLoad
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:
beforeLoad
、afterLoad
、beforeMount
、afterMount
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:
beforeLoad
、afterLoad
、beforeMount
、afterMount
- Trigger:
- 在调用
app.unmount
或app.hide
触发该hook
,用户除了手动调用这两个方法外,Garfish Router
托管模式还会自动触发- 在使用
app.unmount
渲染应用是cacheMode
为false
; - 在使用
app.hide
渲染应用是cacheMode
为true
;
- 在使用
- 此时子应用 DOM 元素还未卸载,副作用尚未清除;
- 此时子应用 DOM 树已渲染完成,garfish 实例
activeApps
中已添加当前子应用 app 实例;
- 在调用
afterUnmount
- Type: ( appInfo: AppInfo, appInstance: interfaces.App) => void
- Kind:
sync
,sequential
- Trigger:
- 此时,应用在渲和运行过程中产生的副作用已清除,DOM 已卸载,沙箱副作用已清除,garfish 实例
activeApps
当前 app 已移除; - 在应用销毁过程中会调用应用生命周期中的
destory
函数,用户可在销毁前定义相关操作; - 若应用卸载过程中出现异常,会触发 errorUnmountApp
- 此时,应用在渲和运行过程中产生的副作用已清除,DOM 已卸载,沙箱副作用已清除,garfish 实例
errorUnmountApp
Type: (error: Error, appInfo: AppInfo, appInstance: interfaces.App)=> void
- 一旦设置该 hook,子应用销毁错误不会向上 throw 到文档流中,全局错误监听将无法捕获到;
Kind:
sync
,sequential
Trigger:
- 在
app.unmount
或app.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);
}
})