likes
comments
collection
share

微前端icestark源码解读-微应用间数据通信详解(四)

作者站长头像
站长
· 阅读数 11

快速链接

icestark源码解读(一):控制微应用加载与卸载的核心原理

icestark源码解读(二):微应用加载详解

icestark源码解读(三):微应用卸载详解

前言

  • 前面三个章节,我们了解到控制子应用加载与卸载的原理,以及子应用加载与卸载的完整过程。在实际业务开发的过程中,我们难免会遇到应用间通信的问题。例如官网说的中英文切换场景,切换按钮在主应用,监听事件在微应用,微应用要能够知道主应用点击了切换按钮。接下来,我们看icestark源码中是如何解决这个问题的。

  • 源码位置是在icestark-data目录下。和redux有着相似的概念,其拥有一个store, 专门用于储存需要应用间共享的state

Store类

Store是一个类,应用间需要共享的数据其实都是通过这个类的实例来去实现的。现在我们从new Store以及文档介绍的使用方法两个角度去看一下,Store类里面隐藏着哪些方法。

new Store去创建一个store的实例的时候,首先是先去执行Store类里面的构造函数constructor, 该函数内部就做了两件事:

  1. 创建一个类型为objectstore变量,用于储存state.
  2. 创建一个类型为objectstoreEmitter变量,用于储存state值变化触发的回调函数.

constructor 函数

constructor() {
  this.store = {}; // 储存state
  this.storeEmitter = {}; // 储存 state值变化事件
}

上面代码执行之后,我们就有一个store实例。此时我们不知道store身上有哪些方法。我们看下图官网介绍的store身上的api。

微前端icestark源码解读-微应用间数据通信详解(四)

好,那么我们就从设置state开始,看下set方法。

set 函数

set 函数接收两个参数,第一个参数为state的key,其类型为 string | symbol | object其中之一,第二个参数为state的value,其类型为一个泛型,由开发者自行定义值类型。当state的key类型是object的时候,就证明传进来的是一组state, 会先遍历,拿到该对象中的每一个state的key以及value之后,执行内部私有方法_setValue,将state设置到this.store中。当state的key类型是string | symbol的时候,直接调用_setValue

/**
 * 设置state的方法
 * @param key state的key
 * @param value state的value
 */
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)) { // 说明是一组state,遍历拿到每一个state,然后去设置到 this.store 中
    Object.keys(key).forEach(k => {
      const v = key[k];

      this._setValue(k, v);
    });
  } 
  else { // 说明是一个state,直接调用_setValue设置到 this.store 中
    this._setValue(key as StringSymbolUnion, value);
  }
}

_setValue 函数

该函数执行的代码是真正的将state设置到store里面,然后会执行_emit方法,去触发监听该state值变化事件。

/**
 * 真正的设置state的方法
 * @param key
 * @param value
 */
_setValue(key: StringSymbolUnion, value: any) {
  this.store[key] = value; // 设置state
  this._emit(key); // 触发state值变化事件
}

_emit 函数

该函数接收state的key作为参数,通过state的key,从this.storeEmitter中找到所有监听该state的值变化回调函数,然后将state的值作为参数依次执行这些回调函数。那么this.storeEmitter中为什么会有值变化回调函数呢,这个是通过on方法并由开发者自行注册的。

/**
 * 根据state的key去触发对应值变化事件
 * @param key state的key
 */
_emit(key: StringSymbolUnion) {
  const keyEmitter = this.storeEmitter[key]; // 获取到监听该state的所有值变化回调函数

  if (!isArray(keyEmitter) || (isArray(keyEmitter) && keyEmitter.length === 0)) {
    return;
  }
  
  // 获取到state的值 循环执行监听state值变化回调函数
  const value = this._getValue(key); 
  keyEmitter.forEach(cb => {
    cb(value);
  });
}

on 函数

该函数接收三个参数,分别是state的key、回调函数、是否强制在初始化时候执行。开发者通过on方法监听state变化的回调函数都会被收集在this.storeEmitter对象中属性为state的key对应的数组中,这里为什么选用的是数组,原因是开发者可能不止一次的去监听state的变化,这里就要使用数组去储存回调函数,以便在_emit函数中遍历执行所有监听该state的回调函数。

有些场景可能需要我们在使用on注册监听事件的时候,回调函数立刻执行一次。这个时候就可以通过第三个参数force来实现。

有注册就肯定有解绑,通过off方法我们可以将注册的监听事件全部移除掉。

/**
 * 监听state值变化事件
 * @param key state的key
 * @param callback state值变化的回调函数
 * @param force 初始化注册过程中是否强制执行
 */
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)); // 将state的value作为值变化回调函数的参数
  }
}

off 函数

该函数接受state的key,以及一个可有可无的回调函数作为参数。当只传state的key时,会将所有监听该state值变化的回调函数全部移除掉。若传了第二个callback参数,则只会移除监听该state值变化所有回调中指定的回调。

注意这里比较是否为同一回调,采用的是内存 堆中的引用地址进行比较的。所以在使用onoff 的时候,传入的回调函数是引用地址完全相同的回调函数。

有些时候我们想知道程序中是否对state已经进行了值变化的监听,这个时候可以借助has函数来去实现。

/**
 * 移除监听的state值变化事件
 * @param key state的key
 * @param callback 监听的回调函数
 */
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;
  }
  // 说明要移除所有该state的回调函数
  if (callback === undefined) { 
    this.storeEmitter[key] = undefined;
    return;
  }
  // 说明只移除指定的回调函数,根据回调函数在内存堆中的引用地址来进行比较
  this.storeEmitter[key] = this.storeEmitter[key].filter(cb => cb !== callback); 
}

has 函数

该函数通过state的key去this.storeEmitter中寻找,是否有该state的值变化回调函数。其返回值是true或者 false

/**
 * 检测是否有值变化的回调函数
 * @param key state的key
 */
has(key: StringSymbolUnion) {
  const keyEmitter = this.storeEmitter[key];
  return isArray(keyEmitter) && keyEmitter.length > 0;
}

get 函数

该函数接收一个可有可无的state key,不传则会把store中所有的数据返回,否则就通过_getValue方法,获取指定state的value进行返回。

/**
 * 获取store中存储的state
 * @param key state的key
 */
get(key?: StringSymbolUnion) {
  if (key === undefined) {
    return this.store; // 将store中存储的所有state全部返回
  }

  if (typeof key !== 'string' && typeof key !== 'symbol') {
    warn('store.get: key should be string / symbol');
    return null;
  }

  return this._getValue(key);
}

_getValue 函数

通过state的key,从this.store中取出对应的value进行返回

/**
 * 获取store中指定的state
 * @param key state的key
 */
_getValue(key: StringSymbolUnion) {
  return this.store[key];
}

store 总结

上面详细介绍了store实例身上所拥有的方法,源码中在文件的末尾处,创建完Store类的实例之后,会将该实例挂在widow对象身上,由于icestark运行时主应用和子应用都是在浏览器的同一窗口下,所以可以共享window对象,这样就达到了应用间共享数据的目的。

const storeNameSpace = 'store';
let store = getCache(storeNameSpace);
if (!store) { // 如果window对象身上没有Store的实例,则需要创建
  store = new Store(); // 创建Store的实例
  setCache(storeNameSpace, store);
}

源码中把挂载到window对象的过程封装到了cache.ts文件中,我们根据名称称之为缓存。我们接下来看下cache源码。

cache.ts

该文件有一个namespace,用于标识数据要挂载到window对象指定的属性上面。其值是ICESTARK. 也就是说明共享的store实例是挂载到window.ICESTARK上面

const namespace = 'ICESTARK';

setCache 函数

该函数通过key,value的形式,将要缓存的数据挂载到window.ICESTARK身上.

/**
 * 设置缓存
 * @param key 缓存的key
 * @param value 缓存的值
 */
export const setCache = (key: string, value: any): void => {
  if (!(window as any)[namespace]) {
    (window as any)[namespace] = {};
  }
  (window as any)[namespace][key] = value; // 设置到 window.ICESTARK身上
};

getCache 函数

该函数通过key,从window.ICESTARK身上获取缓存的value

/**
 * 获取缓存
 * @param key
 */
export const getCache = (key: string): any => {
  const icestark: any = (window as any)[namespace];
  return icestark && icestark[key] ? icestark[key] : null;
};

Event 类

在实际业务场景中,有的时候我们只是单纯的想实现一个发布订阅,并不想再去store中创建一个state,然后监听state是否变化来达到发布订阅的效果。这个时候我们就需要借助icestark提供的event来做。

源码中Event也是一个类,其实例身上拥有emitonoffhas方法,供我们实现一个简单的发布订阅。与Store一样,其最终是创建一个Event实例event,然后将event挂载到window对象上,以便不同应用之间可以共享这个event。

微前端icestark源码解读-微应用间数据通信详解(四)

constructor 函数

构造函数中唯一做的就是创建一个类型为object的 this.eventEmitter去储存所有监听的事件。

constructor() {
  this.eventEmitter = {}; // 储存所有事件
}

on 函数

该函数接收事件名和事件的回调函数作为参数,开发者通过on方法监听的事件回调函数,都会被收集在this.eventEmitter对象中属性为事件名对应的数组中。

/**
 * 注册监听事件
 * @param key 事件名
 * @param callback 响应事件回调函数
 */
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);
}

off 函数

该函数接收事件名,以及一个可有可无的事件回调函数作为参数。当只传事件名时,会将所有监听该事件的回调函数全部移除掉。若传了第二个callback参数,则只会移除监听该事件所有回调中指定的回调。同样,比对是否为同一回调用的是内存 堆中的引用地址。

/**
 * 移除注册事件的回调函数
 * @param key 事件名
 * @param callback 事件的回调函数
 */
off(key: StringSymbolUnion, callback?: (value: any) => void) {
  if (typeof key !== 'string' && typeof key !== 'symbol') {
    warn('event.off: key should be string / symbol');
    return;

  }

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

  // 移除当前事件所有的回调函数
  if (callback === undefined) {
    this.eventEmitter[key] = undefined;
    return;
  }

  // 移除当前事件指定的回调函数,采用内存 堆中的引用地址是否一致作为判断条件
  this.eventEmitter[key] = this.eventEmitter[key].filter(cb => cb !== callback);
}

emit 函数

该函数接收事件名以及自定义parameter作为参数,通过事件名,从this.eventEmitter中找到所有监听该事件的回调函数,然后遍历依次执行,并将自定义parameter作为回调函数的参数。

/**
 * 根据事件名去触发事件对应的回调函数
 * @param key 事件名
 * @param args 自定义的参数
 */
emit(key: StringSymbolUnion, ...args) {
  // 从this.eventEmitter中取出所有该事件的回调函数
  const keyEmitter = this.eventEmitter[key];

  if (!isArray(keyEmitter) || (isArray(keyEmitter) && keyEmitter.length === 0)) {
    warn(`event.emit: no callback is called for ${String(key)}`);
    return;
  }

  // 执行所有的回调,并将参数传入每一个回调中
  keyEmitter.forEach(cb => {
    cb(...args);
  });
}

has 函数

该函数在官网文档中未提及,但是源码中是有的,也就是说我们是可以直接用的。其作用是根据事件名检测程序中是否已经监听了该事件。返回值是true或者 false

/**
 * 检测是否有事件的回调函数
 * @param key 事件名
 */
has(key: StringSymbolUnion) {
  const keyEmitter = this.eventEmitter[key];
  return isArray(keyEmitter) && keyEmitter.length > 0;
}

总结

从上面的源码介绍来看,无论是共享数据的store,还是共享事件的event,其本质都是借助window对象,来实现共享。创建一个类的实例,将数据赋值在类的实例身上,最后再将实例挂载到window对象上。

弄清楚应用间共享数据的原理之后,其实我们自己也可以借助window对象去实现那些不是特别复杂的数据共享场景,例如:主应用登录界面获取到的用户登录信息,我们可以直接挂载到window对象上,以便子应用均可以直接使用。官网在介绍应用间通信的时候,也说明了应用间通信的简单实现方案。

微前端icestark源码解读-微应用间数据通信详解(四)

本次应用间通信原理介绍就到这了,在下一章节我们将会看下icestark是如何创建沙箱的。