Garfish 源码解析 —— 一个微应用是如何被挂载的

背景

Garfish 是字节跳动 web infra 团队推出的一款微前端框架

包含构建微前端系统时所需要的基本能力,任意前端框架均可使用。接入简单,可轻松将多个前端应用组合成内聚的单个产品

Garfish 源码解析 —— 一个微应用是如何被挂载的_第1张图片

因为当前对 Garfish 的解读极少,而微前端又是现代前端领域相当重要的一环,因此写下本文,同时也是对学习源码的一个总结

本文基于 garfish#0d4cc0c82269bce8422b0e9105b7fe88c2efe42a 进行解读

学习源码

git clone https://github.com/modern-js-dev/garfish.git
cd garfish
pnpm install
pnpm build
pnpm dev

然后打开https://localhost:8090/ 即可看到演示项目

基本使用

主应用

export const GarfishInit = async () => {
   
  try {
   
    Garfish.run(Config);
  } catch (error) {
   
    console.log('garfish init error', error);
  }
};

其中关键点是 Config 参数, 其所有参数都是可选的,一般比较重要的几个参数为:

  • basename 子应用的基础路径,默认值为 /,整个微前端应用的 basename。设置后该值为所有子应用的默认值,若子应用 AppInfo 中也提供了该值会替换全局的 basename 值
  • domGetter 子应用挂载点。如'#submodule'
  • apps 需要主要参数如 name, entry, activeWhen(路由地址)

此函数运行之后,Garfish会自动进行路由劫持功能。根据路由变化

子应用

以react17为例:

import {
    reactBridge, AppInfo } from '@garfish/bridge-react';

export const provider = reactBridge({
   
  el: '#root', // 此处的root是子应用自己声明的root
  // a promise that resolves with the react component. Wait for it to resolve before mounting
  loadRootComponent: (appInfo: AppInfo) => {
   
    return Promise.resolve(() => <RootComponent {
   ...appInfo} />);
  },
  errorBoundary: (e: any) => <ErrorBoundary />,
});

其中:

  • RootComponent 是子应用的主要逻辑
  • reactBridge 是garfish导出的一个封装函数。大概的逻辑就是把react的一些特有写法映射到garfish的通用生命周期,包含renderdestroy

源码解读

那么简单了解了一些garfish的基本使用方案,我们就来看看garfish在此过程中到底做了什么。

Garfish.run开始:

garfish/packages/core/src/garfish.ts

run(options: interfaces.Options = {
   }) {
   
  if (this.running) {
   
    /**
     * 重复运行检测
     */
    if (__DEV__) {
   
      warn('Garfish is already running now, Cannot run Garfish repeatedly.');
    }
    return this;
  }

  /**
   * 全局化配置
   */
  this.setOptions(options);

  /**
   * 载入插件
   */
  // Register plugins
  options.plugins?.forEach((plugin) => this.usePlugin(plugin));

  // Put the lifecycle plugin at the end, so that you can get the changes of other plugins
  this.usePlugin(GarfishOptionsLife(this.options, 'global-lifecycle'));

  // Emit hooks and register apps
  this.hooks.lifecycle.beforeBootstrap.emit(this.options); // 生命周期事件beforeBootstrap
  this.registerApp(this.options.apps || []); // 注册子应用
  this.running = true;
  this.hooks.lifecycle.bootstrap.emit(this.options); // bootstrap
  return this;
}

其中移除插件等内容,最重要的是registerApp调用,用于将配置注册到实例中

接下来的代码会移除无关紧要的代码,仅保留核心逻辑

registerApp(list: interfaces.AppInfo | Array<interfaces.AppInfo>) {
   
  if (!Array.isArray(list)) list = [list];

  for (const appInfo of list) {
   
    if (!this.appInfos[appInfo.name]) {
   
      this.appInfos[appInfo.name] = appInfo;
    }
  }

  return this;
}

看上去仅仅是一些配置设定,那么所谓的路由绑定是从哪里发生的呢?这一切其实早就暗中进行了处理。

export type {
    interfaces } from '@garfish/core';
export {
    default as Garfish } from '@garfish/core';
export {
    GarfishInstance as default } from './instance';
export {
    defineCustomElements } from './customElement';

当调用 import Garfish from 'garfish';时, 使用的是默认创建好的全局Garfish实例。该逻辑简化版大概如下:

import {
    GarfishRouter } from '@garfish/router';
import {
    GarfishBrowserVm } from '@garfish/browser-vm';
import {
    GarfishBrowserSnapshot } from '@garfish/browser-snapshot';

// Initialize the Garfish, currently existing environment to allow only one instance (export to is for test)
function createContext(): Garfish {
   
  // Existing garfish instance, direct return
  if (inBrowser() && window['__GARFISH__'] && window['Garfish']) {
   
    return window['Garfish'];
  }

  const GarfishInstance = new Garfish({
   
    plugins: [GarfishRouter(), GarfishBrowserVm(), GarfishBrowserSnapshot()],
  });

  type globalValue = boolean | Garfish | Record<string, unknown>;
  const set = (namespace: string, val: globalValue = GarfishInstance) => {
   
    // NOTE: 这里有一部分状态判定的逻辑,以及确保只读,这里是精简后的逻辑
    window[namespace] = val;
  };

  if (inBrowser()) {
   
    // Global flag
    set('Garfish');
    Object.defineProperty(window, '__GARFISH__', {
   
      get: () => true,
      configurable: __DEV__ ? true : false,
    });
  }

  return GarfishInstance;
}

export const GarfishInstance = createContext();

其中核心逻辑为:

  • 如果本地已经有Garfish实例,则直接从本地拿。(浏览器环境用于子应用,也可以从这边看出garfish并不支持其他的js环境
  • 创建Garfish实例,并安装插件:
    • GarfishRouter 路由劫持能力
    • GarfishBrowserVm js运行时沙盒隔离
    • GarfishBrowserSnapshot 浏览器状态快照
  • 在window上设置全局Garfish对象并标记__GARFISH__, 注意该变量为只读

其中安全和样式隔离的逻辑我们暂且不看,先看其核心插件 GarfishRouter 的实现

插件系统

Garfish 自己实现了一套插件协议,其本质是pubsub模型的变种(部分生命周期的emit阶段增加了异步操作的等待逻辑)。

我们以Garfish最核心的插件 @garfish/router 为学习例子,该代码的位置在: garfish/packages/router/src/index.ts

export function GarfishRouter(_args?: Options) {
   
  return function (Garfish: interfaces.Garfish): interfaces.Plugin {
   
    Garfish.apps = {
   };
    Garfish.router = router;

    return {
   
      name: 'router',
      version: __VERSION__,

      bootstrap(options: interfaces.Options) {
   
        let activeApp: null | string = null;
        const unmounts: Record<string, Function> = {
   };
        const {
    basename } = options;
        const {
    autoRefreshApp = true, onNotMatchRouter = () => null } =
          Garfish.options;

        async function active(
          appInfo: interfaces.AppInfo,
          rootPath: string = '/',
        ) {
   
          routerLog(`${
     appInfo.name} active`, {
   
            appInfo,
            rootPath,
            listening: RouterConfig.listening,
          });

          // In the listening state, trigger the rendering of the application
          if (!RouterConfig.listening) return;

          const {
    name, cache = true, active } = appInfo;
          if (active) return active(appInfo, rootPath);
          appInfo.rootPath = rootPath;

          const currentApp = (activeApp = createKey());
          const app = await Garfish.loadApp(appInfo.name, {
   
            basename: rootPath,
            entry: appInfo.entry,
            cache: true,
            domGetter: appInfo.domGetter,
          });

          if (app) {
   
            app.appInfo.basename = rootPath;

            const call = async (app: interfaces.App, isRender: boolean) => {
   
              if (!app) return;
              const isDes = cache && app.mounted;
              if (isRender) {
   
                return await app[isDes ? 'show' : 'mount']();
              } else {
   
                return app[isDes ? 'hide' : 'unmount']();
              }
            };

            Garfish.apps[name] = app;
            unmounts[name] = () => {
   
              // Destroy the application during rendering and discard the application instance
              if (app.mounting) {
   
                delete Garfish.cacheApps[name];
              }
              call(app, false);
            };

            if (currentApp === activeApp) {
   
              await call(app, true);
            }
          }
        }

        async function deactive(appInfo: interfaces.AppInfo, rootPath: string) {
   
          routerLog(`${
     appInfo.name} deactive`, {
   
            appInfo,
            rootPath,
          });

          activeApp = null;
          const {
    name, deactive } = appInfo;
          if (deactive) return deactive(appInfo, rootPath);

          const unmount = unmounts[name];
          unmount && unmount();
          delete Garfish.apps[name];

          // Nested scene to remove the current application of nested data
          // To avoid the main application prior to application
          const needToDeleteApps = router.routerConfig.apps.filter((app) => {
   
            if (appInfo.rootPath === app.basename) return true;
          }<

你可能感兴趣的:(前端,工程化,javascript,前端,前端框架)