logologo
指南
API
博客
常见问题
指南
API
博客
常见问题
logologo

快速上手

介绍
快速开始
环境变量

接入指南

概述
react 子应用
vue 子应用
vite 子应用
angular 子应用

核心能力

bridge
缓存机制
生命周期
路由机制
构建配置
沙箱机制

进阶

插件指南
Previous Page沙箱机制

#插件指南

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

#插件能做什么

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

  1. 添加全局方法或增加默认参数
  2. 在应用的生命周期中自定义功能(例如: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 会在注册子应用时触发)
    • 当有新注册应用时对新应用进行检验是否如何渲染条件,进行销毁
TIP

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
  • 示例

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

#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);
  }
})