2
在基于qiankun来进行微前端应用开发过程中,难免会碰到应用之间通信的情况,如基于某个业态下某些定制化的业务需求。这时候就涉及到某个子应用需要知道当前的"宿主"是谁(标识是哪一个私有化项目定制的业务需求),而要知道"宿主"是谁,就需要"宿主"主动告知某个子应用,这就需要涉及到主、子应用数据通信。

微前端下应用之间通信方式

  • 基于qiankun框架自带的通信方式:通过api中的initGlobalState进行通信
  • 基于三方库的通信方式:如@ice/stark-data进行通信

基于qiankun自带的通信方式

https://img2020.cnblogs.com/blog/1080099/202103/1080099-20210305151502519-325252408.png

基于@ice/stark-data通信方式

https://segmentfault.com/img/bVcV9Gt

基于@ice/stark-data的微前端架构模式下的通信方式可以实现主、子应用之间互相的通讯方式(全双工),表现如下:

  • 主->子:针对这种场景,可以使用@/ice/stark-data库中的store对象的setget方式实现
  • 子->主:针对这种场景,可以使用@/ice/stark-data库中的event对象的emiton方式实现
  • 子->子:针对这种场景,可以使用@/ice/stark-data库中的store对象的setget方式实现

@ice/stark-data源码分析

我们知道在基于VueJsReactJs三方状态管理库vuexredux等,其对数据的存储都是存储在内存中的(非持久性)。同样@ice/stark-data在对数据进行存储的时候,是通过基于某个命名空间结合window对象进行存储的,也是非持久性的。但@ice/startk-data 实现了简单的发布订阅机制,通过全局的 window 共享应用间的数据,一般情况下内容会比较简单
vuexredux 都是状态管理方案在使用场景上是不同

注:当前解析的源码版本是0.1.3,仓库地址:https://github.com/ice-lab/ic...

整个@ice/stark-data库的源码其实比较简单,由以下几个部分组成:

https://segmentfault.com/img/bVcV9D4

  • utils.ts:工具集,总共包含三个函数isObjectisArraywarn,分别用于判断某个变量是否是对象、数组类型及警告信息输出的函数封装。
  • cache.ts:基于命名空间ICESTARKwindow全局对象封装的用于存取的函数setCachegetCache,这里使用了命名空间,在一定程度上也能够避免了window全局对象上变量的污染问题。
  • store.ts:主要实现了主应用与子应用、子应用与子应用单向数据通信
  • event.ts:主要实现了子应用与主应用单向数据通信
  • index.tsstoreevent进行按需导出

可以看出,整个库中的核心代码在store.tsevent.ts文件中,接下来就专门针对这两个文件中的代码进行解析。

store.ts源码解析

当我们需要从主应用中传递数据给子应用时,基于@ice/stark-data的一般做法如下:

  • 主应用设置数据

    import { store } from '@ice/stark-data'
    store.set('someData', 'darkCode')
  • 子应用接收数据

    import { store } from '@ice/stark-data'
    const data: any = store.get('someData')

store.ts中。在第1114行代码之间,定义了一个名为IO的接口。并在该接口中分别定义了setget方法:

interface IO {
  set(key: string | symbol | object, value?: any): void;
  get(key?: StringSymbolUnion): void;
}

其中set方法接收两个参数,key的类型是一个联合类型,value是任意类型的变量。该方法无返回值。

get方法接收一个参数,key的类型是一个联合类型。该方法也是无返回值?

在第1620行代码之间,定义了一个名为Hooks的接口。并在该接口中分别定义了onoffhas三个方法:

interface Hooks {
  on(key: StringSymbolUnion, callback: (value: any) => void, force?: boolean): void;
  off(key: StringSymbolUnion, callback?: (value: any) => void): void;
  has(key: StringSymbolUnion): boolean;
}

接口Hooks的主要作用是用来针对数据进行订阅发布处理及针对对应的"事件"进行"销毁"处理。

在代码的第22行,定义了Store类,该类同时实现了IOHooks这两个接口。class Store implements IO, Hooks

在类Store中分别定义了storestoreEmitter两个属性,并在构造函数中对其进行初始化操作:

  store: object;
  storeEmitter: object;
  constructor() {
    this.store = {};
    this.storeEmitter = {};
  }

接下来是定义了两个"私有"方法,_setValue_getValue分别对"数据"进行"写入"及"输出"。

  _getValue(key: StringSymbolUnion) {
    return this.store[key];
  }

  _setValue(key: StringSymbolUnion, value: any) {
    this.store[key] = value;
    this._emit(key);
  }

_setValue的实现中,先对实例对象属性store对象挂载key属性,并设置其值为value。同时将该key通过调用_emit方法从_getValue中取出对应的值,并从属性storeEmitter中取出对应的"触发器"(keyEmitter),然后对其遍历执行对应的回调。


接下来是重写实现IO接口中的setget方法。先来看set方法(67行到84行)的实现:

  set<T>(key: string | symbol | object, value?: T) {
    if (typeof key !== 'string'
      && typeof key !== 'symbol'
      && !isObject(key)) {
      warn('store.set: key should be string / symbol / object');
      return;
    }

    if (isObject(key)) {
      Object.keys(key).forEach(k => {
        const v = key[k];

        this._setValue(k, v);
      });
    } else {
      this._setValue(key as StringSymbolUnion, value);
    }
  }

内部首先判断参数key变量的类型,如果不是stringsymbolobject类型之一,则之间进行return。反之,先判断key变量如果是一个object类型,则获取到key"对象"中的属于"键",并进行遍历。在遍历的过程中获取k对应的v。在调用实例对象的内部方法_setValue来存储数据(值);如果key不是对象类型,之间调用实例对象的内部方法_setValue来存储数据(值)。

get方法是通过key来获取对应存储的数据(值),先判断参数key的类型如果不是stringsymbol之一,则返回null。反之调用内部方法_getValue来获取值


接下来是对实现接口Hooks中的三个方法进行重写。先来看第一个方法on(在86行到106行):

  on(key: StringSymbolUnion, callback: (value: any) => void, force?: boolean) {
    if (typeof key !== 'string' && typeof key !== 'symbol') {
      warn('store.on: key should be string / symbol');
      return;
    }

    if (callback === undefined || typeof callback !== 'function') {
      warn('store.on: callback is required, should be function');
      return;
    }

    if (!this.storeEmitter[key]) {
      this.storeEmitter[key] = [];
    }

    this.storeEmitter[key].push(callback);

    if (force) {
      callback(this._getValue(key));
    }
  }

on方法接收三个参数:

  • key:参数keystringsymbol的联合类型
  • callback:参数callback是一个回调函数
  • force:参数force是一个可选参数,类型是一个boolean类型

从源码实现可以看出,on方法的主要作用是基于keycallback参数来对storeEmitter进行元素存储的过程。如果参数forcetrue,则通过参数key从方法_getValue中获取对应的值作为回调函数callback的参数,并执行回调函数callback

对于第二个方法off(在108行到125行),实现如下:

  off(key: StringSymbolUnion, callback?: (value: any) => void) {
    if (typeof key !== 'string' && typeof key !== 'symbol') {
      warn('store.off: key should be string / symbol');
      return;
    }

    if (!isArray(this.storeEmitter[key])) {
      warn(`store.off: ${String(key)} has no callback`);
      return;
    }

    if (callback === undefined) {
      this.storeEmitter[key] = undefined;
      return;
    }

    this.storeEmitter[key] = this.storeEmitter[key].filter(cb => cb !== callback);
  }

off方法接收两个参数:

  • key:参数keystringsymbol的联合类型
  • callback:参数callback是一个回调函数

从源码实现可以看出,off方法的主要作用是基于实例对象的storeEmitter属性结合key来过滤掉callback(类似从数组中删除某个元素。)


紧接着,通过调用函数getCache来创建变量store,如果变量store没有值,则调用类Store创建一个实例对象赋值给store变量,并将该变量挂载到以storeNameSpace为命名空间的window对象上。最后将该store变量导出。

let store = getCache(storeNameSpace);
if (!store) {
  store = new Store();
  setCache(storeNameSpace, store);
}

export default store;

小总结:

  1. 当我们调用store.set方法通过键值对(keyvalue)的形式设置某个值的时候,内部先判断key的类型,如果key是对象类型。那么通过Object.keys方法获取该对象上的所有属性,并针对属性集进行遍历取出属性(k)对应的值(v),然后调用store实例内部的_setValue方法,将对应属性(k)的值(v)赋值给实例store属性。然后调用内部方法_emit方法,根据属性k从实例属性storeEmitter、及调用内部方法_getValue中获取对应的值(keyEmittervalue)。最后遍历数组keyEmitter中的每一项元素(回调函数),并以value作为参数执行其回调函数。
  2. 该库的实现过程中,对于命名空间的使用,目的在于隔离(直接)造成window对象属性的污染。
  3. 当我们调用store.get方法通过键|属性(key)来获取对应的值的时候,内部会先判断key是否存在,不存在则之间返回实例对象的store属性;如果key存在,但其类型不是string/symbol之一,则返回null。反之,调用实例内部的_getValue方法通过属性key从实例属性store中获取到对应的值。
  4. 这里Store类中的两个属性storestoreEmitter在对其进行定义及值存取操作中,涉及到了队列的操作。两个属性的类型都是"对象"类型,但也可以定义为数组类型(从实现的角色来看)或者通过WeakMap处理会更好。

event.ts源码解析

在基于@ice/stark-data处理从子应用传递数据到主应用时,做法一般如下:

  1. 子应用:

    import { event } from '@ice/stark-data'
    event.emit('refreshToken', 'cdacavfsasxxs')
  2. 主应用

    import { event } from '@ice/stark-data'
    event.on('refreshToken', (val: any) => { 
      console.log('the value from subApp is:', val)
    })

这种实现是借助发布订阅模式来实现,类似于VueJs中子组件与父组件之间通信的情况。接下来从源码的角度去理解它们内部的实现细节。

store.ts代码中,第6行代码定义了一个常量eventNameSpace的命名空间const eventNameSpace = 'event';,在第8行代码定义了一个联合类型StringSymbolUnion(类型的定义),type StringSymbolUnion = string | symbol;

接着在第10行到15行定义了接口Hooks,并在其内部定义了四个方法,分别是:

  • emitemit(key: StringSymbolUnion, value: any): void;该方法在实现阶段的作用在于针对的订阅"事件",从队列中遍历出所以的"事件",并执行对应的回调。
  • onon(key: StringSymbolUnion, callback: (value: any) => void): void;该方法在实现阶的作用在于将回调函数callback存储在队列中
  • offoff(key: StringSymbolUnion, callback?: (value: any) => void): void;该方法在实现阶的作用在于从队列中找到不属于callback的元素进行移除(过滤)操作
  • hashas(key: StringSymbolUnion): boolean;该方法在实现阶的作用在于基于某个key判断队列中对应的"值"集合是否存在,并返回一个布尔类型的值。

event.ts文件中的第17行定义了类Event并实现了Hooks接口class Event implements Hooks,该类Event中定义了属性eventEmitter,并在构造函数中对其进行初始化操作。

  eventEmitter: object;

  constructor() {
    this.eventEmitter = {};
  }

接下来就是对接口Hooks中定义的四个方法分别进行了重写实现。先来看下on方法的实现:

  on(key: StringSymbolUnion, callback: (value: any) => void) {
    if (typeof key !== 'string' && typeof key !== 'symbol') {
      warn('event.on: key should be string / symbol');
      return;
    }
    if (callback === undefined || typeof callback !== 'function') {
      warn('event.on: callback is required, should be function');
      return;
    }

    if (!this.eventEmitter[key]) {
      this.eventEmitter[key] = [];
    }

    this.eventEmitter[key].push(callback);
  }

从源码可以看出on方法的实现其实也很简单,与store.ts代码中类Storeon方法的实现几乎是一样的。这里就不在详细说了。

至于另外三个方法的实现,也与之前提到的类Store中对应的这三个方法的实现几乎一样,也不细说。

最后通过调用函数getCache来创建一个对象类型的变量event,然后event不存在(值为null、undefined等)。则调用类Event通过new关键词创建出来的对象实例赋值给变量event,同时调用setCache函数将变量event基于命名空间常量eventNameSpace挂载到window对象上。然后导出该变量event(对象类型)。

总结

  • 基于@ice/stark-data在微前端框架qiankun中实现的主、子应用之间全双工通信的实现很简单,核心是基于发布订阅者模式去实现,以不同的命名空间变量作为区分,将对应属性(key)的值(value)挂载在全局window对象上,这样对于在同一个"应用"里,只要知道了对应的命名空间,就能够访问到其对应的值;
  • @ice/stark-data源码的封装存在一些不足的地方,比如在store.tsevent.ts中分别定义的Hooks接口,没有达到复用的效果(各自都定义了一次,没必要)。另外store.tsevent.ts中分别针对类StoreEvent定义中部分方法代码的实现是一样的,没必要两边都各自实现一次,可以写一个基类(父类),然后从其进行继承extends。针对需要重写的方法再进行重写会好很多;
  • 源码中对于代码健壮性的处理还是不错的,比如类Event中实现的四个方法中,对于key的判断处理。

前端扫地僧
2.5k 声望1.2k 粉丝