微前端icestark源码解读-微应用间数据通信详解(四)
快速链接
icestark源码解读(一):控制微应用加载与卸载的核心原理
前言
-
前面三个章节,我们了解到控制子应用加载与卸载的原理,以及子应用加载与卸载的完整过程。在实际业务开发的过程中,我们难免会遇到应用间通信的问题。例如官网说的中英文切换场景,切换按钮在主应用,监听事件在微应用,微应用要能够知道主应用点击了切换按钮。接下来,我们看
icestark
源码中是如何解决这个问题的。 -
源码位置是在
icestark-data
目录下。和redux
有着相似的概念,其拥有一个store
, 专门用于储存需要应用间共享的state
。
Store类
Store
是一个类,应用间需要共享的数据其实都是通过这个类的实例来去实现的。现在我们从new Store
以及文档介绍的使用方法两个角度去看一下,Store类里面隐藏着哪些方法。
new Store
去创建一个store
的实例的时候,首先是先去执行Store
类里面的构造函数constructor
, 该函数内部就做了两件事:
- 创建一个类型为
object
的store
变量,用于储存state
. - 创建一个类型为
object
的storeEmitter
变量,用于储存state
值变化触发的回调函数.
constructor 函数
constructor() {
this.store = {}; // 储存state
this.storeEmitter = {}; // 储存 state值变化事件
}
上面代码执行之后,我们就有一个store
实例。此时我们不知道store身上有哪些方法。我们看下图官网介绍的store身上的api。
好,那么我们就从设置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值变化所有回调中指定的回调。
注意这里比较是否为同一回调,采用的是内存 堆中的引用地址进行比较的。所以在使用on
和 off
的时候,传入的回调函数是引用地址完全相同的回调函数。
有些时候我们想知道程序中是否对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
也是一个类,其实例身上拥有emit
、on
、off
、has
方法,供我们实现一个简单的发布订阅。与Store
一样,其最终是创建一个Event
实例event,然后将event挂载到window对象上,以便不同应用之间可以共享这个event。
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是如何创建沙箱的。
转载自:https://juejin.cn/post/7158643584550305800