微前端05 : 乾坤loadMicroApp方法实现以及数据通信机制分析

2022-09-27 14:19:00 浏览数 (3)

微前端01 : 乾坤的Js隔离机制(快照沙箱、两种代理沙箱)

微前端02 : 乾坤的微应用加载流程分析(从微应用的注册到loadApp方法内部实现)

微前端03 : 乾坤的沙箱容器分析(Js沙箱机制建立后的具体应用)

微前端04 : 乾坤的资源加载机制(import-html-entry的内部实现)

“在前面的文章中,我们分析了注册微应用的流程,分析了加载微应用的流程,并且深入到import-html-entry了解了乾坤获取微应用资源的具体机制。前面的这些工作,能够支持我们在路由发生变化后自动加载和挂载不同的微应用,换句话说我们的程序在某个时间点最多只能挂载一个微应用,因为这种微应用的加载和挂载是注册微应用后自动发生的,我们不能进行过多的干预。实际上乾坤也支持同一个页面,加载多个应用。那怎么实现这种机制呢?这就引出了今天的主题,手动加载微应用,也就是说我们可以在任何时间点加载任何微应用挂载到任何地方,既然这么灵活,当然就能支持同时展现多个微应用在用户眼前。手动加载微应用的功能由函数loadMicroApp提供,本文就带大家走进该函数。 ”

loadMicroApp的主逻辑

代码语言:javascript复制
// 代码片段1
export function loadMicroApp<T extends ObjectType>(
  app: LoadableApp<T>,
  configuration?: FrameworkConfiguration & { autoStart?: boolean },
  lifeCycles?: FrameworkLifeCycles<T>,
): MicroApp {
  // 此处省略许多代码...
  const memorizedLoadingFn = async (): Promise<ParcelConfigObject> => {
    // 此处省略许多代码...
    const parcelConfigObjectGetterPromise = loadApp(app, userConfiguration, lifeCycles);
    // 此处省略了许多代码...
    return (await parcelConfigObjectGetterPromise)(container);
  };
  
  // 此处省略许多代码...
  microApp = mountRootParcel(memorizedLoadingFn, { domElement: document.createElement('div'), ...props });  
  // 此处省略许多代码...
  return microApp;
}

为了方便理解,我省略了许多次要逻辑,我们来看看这个loadMicroApp主要做了什么工作。最终结果看,是调用了mountRootParcel函数,然后将其返回值作为loadMicroApp函数的返回值。说到这里相信大家会自然直觉的问mountRootParcel是什么?其实mountRootParcel函数是从single-spa中导入,其核心功能就是返回一个对象:

代码语言:javascript复制
externalRepresentation = {
    mount() {
       // 省略诸多代码...
    },
    unmount() {
       // 省略诸多代码...
    },
    getStatus() {
      // 省略诸多代码...
    },
    loadPromise: promiseWithoutReturnValue(loadPromise),
    bootstrapPromise: promiseWithoutReturnValue(bootstrapPromise),
    mountPromise: promiseWithoutReturnValue(mountPromise),
    unmountPromise: promiseWithoutReturnValue(unmountPromise),
  };

返回该对象后我们就可以操作该对象所具有的方法,挂载、卸载、获取微应用状态,执行微应用的生命周期方法。我将在后续文章深入到single-spa中讲解整个库的主要逻辑和一些细节,现在我们只需要知道调用mountRootParcel函数会返回可以操作微应用的对象就可以了。我们先看看本文代码片段1中有个函数memorizedLoadingFn,该函数的核心逻辑就是调用微应用加载函数loadApp并返回一个包含微应用暴露的生命周期方法的对象,也就是说mountRootParcel在内部是调用了这个memorizedLoadingFn获取了微应用暴露的相关生命周期方法。而memorizedLoadingFn内部的其他代码是做了一些缓存机制,这时候大家可能会觉得奇怪,前面我们分析微应用的加载时候loadApp函数会返回一个对象,该对象有微应用的生命周期函数,就已经可以控制微应用的一些行为了。为什么这里loadApp执行完后又调用了一个single-spamountRootParcel函数,而该函数返回的还是一个可以包含控制微应用行为的对象,这样做的意义何在呢?实际上微应用暴露的生命周期方法,功能比较薄弱,比如mount、unmount一般而言就是简单的把相关的DOM节点挂载到某个地方或者从该地方卸载,但虽然能控制自己渲染或者不渲染,但是整个微应用究竟该渲染到哪里去什么时机渲染就有点困难了,因为微应用可能会在任何可能的父应用下工作。而single-spa作为一个基础的框架,相当于一个控制器,比如函数loadbootstrapmountunmount等都必须循序执行,这个控制器管理者微应用的各种状态以及一些微应用具备的一些其他能力。具体single-spa是如何工作的我会在后面的文章中逐渐展开。本文先了解这些就可以了,只需要知道,获取到了mountRootParcel函数的返回值,我们就可以控制相应的微应用。

其实到了这里,结合前面的文章,我们已经可以说是对乾坤有了比较深入的了解,对其主要的API对应的实现和原理也比较清楚。本文再探讨几个乾坤中一些值得了解的点,对乾坤的分析就暂时告一段落,后面的时间主要投入到single-spa的分析中。在分析完single-spa后,我们会回过头来对乾坤整个框架的结构进行分析,洞察其中的设计思想。敬请朋友们期待。

数据通信机制

主子应用的数据传递

关于父应用给子应用传递数据,其实很简单,在注册微应用的时候,有个可选参数props

代码语言:javascript复制
export function registerMicroApps<T extends ObjectType>(
  apps: Array<RegistrableApp<T>>,
  lifeCycles?: FrameworkLifeCycles<T>,
) {
  // 省略许多代码...
  unregisteredApps.forEach((app) => {
    const { name, activeRule, loader = noop, props, ...appConfig } = app;

    registerApplication({
      name,
      app: async () => {
        // 省略许多代码...
        const { mount, ...otherMicroAppConfigs } = (
          await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
        )();
        // 省略许多代码...
      },
      activeWhen: activeRule,
      customProps: props,
    });
  });
}

从代码中可以看出,传入的props参数,在加载微应用的时候直接传入即可,事实上,这些参数可以在微应用执行生命周期方法的时候获取到,这就实现了最简单的父子应用间的东西,当然这里主要指父应用给子应用传值。

全局事件通信

在小节主子应用的数据传递中提到,父应用可以传递参数给子应用,那如果传递的是一个函数呢?没错就是原本普通的通信机制,产生了更为强大的通信机制,通过全局事件通信。刚才提到的函数主要指下面两个函数(实际上不止两个,但这两个相对重要):

代码语言:javascript复制
// 注:为了更容易理解,下面代码和源码中有点出入...
function onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
  // 该函数主要用于监听事件,将传入的callback函数进行保存
};

function setGlobalState(state: Record<string, any> = {}) {
  // 该函数主要用于更新数据,同时触发全局事件,调用函数onGlobalStateChange保存的对应callback函数
}

在函数setGlobalState提到了触发全局事件,怎么触发呢,看下面代码:

代码语言:javascript复制
function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) {
  Object.keys(deps).forEach((id: string) => {
    if (deps[id] instanceof Function) {
      deps[id](cloneDeep(state), cloneDeep(prevState));
    }
  });
}

上面代码中deps[id]就对应着onGlobalStateChange中保存的callback函数。最近受限于工作压力,晚上时间有限,没时间把更细节的东西呈现在文章中,上文提到过的或者代码出现过的缓存机制、深度克隆等等,虽然基础,但对很多基础不扎实的朋友来讲,其实是很重要有必要提出来的。希望将来有机会以书籍或者视频更详细的呈现出更多的优秀开源框架的细节。好了本文先到这里,敬请朋友们期待我接下来对single-spa进行分析的系列文章。

0 人点赞