在基于
qiankun
来进行微前端应用开发过程中,难免会碰到应用之间通信的情况,如基于某个业态下某些定制化的业务需求。这时候就涉及到某个子应用需要知道当前的"宿主"是谁(标识是哪一个私有化项目定制的业务需求),而要知道"宿主"是谁,就需要"宿主"主动告知某个子应用,这就需要涉及到主、子应用数据通信。
微前端下应用之间通信方式
- 基于
qiankun
框架自带的通信方式:通过api
中的initGlobalState
进行通信 - 基于三方库的通信方式:如
@ice/stark-data
进行通信
基于qiankun
自带的通信方式
基于@ice/stark-data
通信方式
基于@ice/stark-data
的微前端架构模式下的通信方式可以实现主、子应用之间互相的通讯方式(全双工),表现如下:
- 主->子:针对这种场景,可以使用
@/ice/stark-data
库中的store
对象的set
、get
方式实现 - 子->主:针对这种场景,可以使用
@/ice/stark-data
库中的event
对象的emit
、on
方式实现 - 子->子:针对这种场景,可以使用
@/ice/stark-data
库中的store
对象的set
、get
方式实现
@ice/stark-data
源码分析
我们知道在基于VueJs
或ReactJs
三方状态管理库vuex
或redux
等,其对数据的存储都是存储在内存中的(非持久性)。同样@ice/stark-data
在对数据进行存储的时候,是通过基于某个命名空间结合window
对象进行存储的,也是非持久性的。但@ice/startk-data
实现了简单的发布订阅机制,通过全局的 window 共享应用间的数据,一般情况下内容会比较简单
而 vuex
和 redux
都是状态管理方案在使用场景上是不同
注:当前解析的源码版本是0.1.3
,仓库地址:https://github.com/ice-lab/ic...
整个@ice/stark-data
库的源码其实比较简单,由以下几个部分组成:
utils.ts
:工具集,总共包含三个函数isObject
、isArray
、warn
,分别用于判断某个变量是否是对象、数组类型及警告信息输出的函数封装。cache.ts
:基于命名空间ICESTARK
及window
全局对象封装的用于存取的函数setCache
、getCache
,这里使用了命名空间,在一定程度上也能够避免了window
全局对象上变量的污染问题。store.ts
:主要实现了主应用与子应用、子应用与子应用单向数据通信event.ts
:主要实现了子应用与主应用单向数据通信index.ts
:将store
、event
进行按需导出
可以看出,整个库中的核心代码在store.ts
与event.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
中。在第11
到14
行代码之间,定义了一个名为IO
的接口。并在该接口中分别定义了set
与get
方法:
interface IO {
set(key: string | symbol | object, value?: any): void;
get(key?: StringSymbolUnion): void;
}
其中set
方法接收两个参数,key
的类型是一个联合类型,value
是任意类型的变量。该方法无返回值。
而get
方法接收一个参数,key
的类型是一个联合类型。该方法也是无返回值?
在第16
到20
行代码之间,定义了一个名为Hooks
的接口。并在该接口中分别定义了on
、off
、has
三个方法:
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
类,该类同时实现了IO
与Hooks
这两个接口。class Store implements IO, Hooks
在类Store
中分别定义了store
与storeEmitter
两个属性,并在构造函数中对其进行初始化操作:
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
接口中的set
与get
方法。先来看set
方法(67
行到84
行)的实现:
set(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
变量的类型,如果不是string
、symbol
、object
类型之一,则之间进行return
。反之,先判断key
变量如果是一个object
类型,则获取到key
"对象"中的属于"键",并进行遍历。在遍历的过程中获取k
对应的v
。在调用实例对象的内部方法_setValue
来存储数据(值);如果key
不是对象类型,之间调用实例对象的内部方法_setValue
来存储数据(值)。
而get
方法是通过key
来获取对应存储的数据(值),先判断参数key
的类型如果不是string
、symbol
之一,则返回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
:参数key
是string
与symbol
的联合类型callback
:参数callback
是一个回调函数force
:参数force
是一个可选参数,类型是一个boolean
类型
从源码实现可以看出,on
方法的主要作用是基于key
与callback
参数来对storeEmitter
进行元素存储的过程。如果参数force
为true
,则通过参数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
:参数key
是string
与symbol
的联合类型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;
小总结:
- 当我们调用
store.set
方法通过键值对(key
、value
)的形式设置某个值的时候,内部先判断key
的类型,如果key
是对象类型。那么通过Object.keys
方法获取该对象上的所有属性,并针对属性集进行遍历取出属性(k
)对应的值(v
),然后调用store
实例内部的_setValue
方法,将对应属性(k
)的值(v
)赋值给实例store
属性。然后调用内部方法_emit
方法,根据属性k
从实例属性storeEmitter
、及调用内部方法_getValue
中获取对应的值(keyEmitter
、value
)。最后遍历数组keyEmitter
中的每一项元素(回调函数),并以value
作为参数执行其回调函数。 - 该库的实现过程中,对于命名空间的使用,目的在于隔离(直接)造成
window
对象属性的污染。 - 当我们调用
store.get
方法通过键|属性(key
)来获取对应的值的时候,内部会先判断key
是否存在,不存在则之间返回实例对象的store
属性;如果key
存在,但其类型不是string
/symbol
之一,则返回null
。反之,调用实例内部的_getValue
方法通过属性key
从实例属性store
中获取到对应的值。 - 这里
Store
类中的两个属性store
及storeEmitter
在对其进行定义及值存取操作中,涉及到了队列的操作。两个属性的类型都是"对象"类型,但也可以定义为数组类型(从实现的角色来看)或者通过WeakMap
处理会更好。
event.ts
源码解析
在基于@ice/stark-data
处理从子应用传递数据到主应用时,做法一般如下:
子应用:
import { event } from '@ice/stark-data' event.emit('refreshToken', 'cdacavfsasxxs')
主应用
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
,并在其内部定义了四个方法,分别是:
emit
:emit(key: StringSymbolUnion, value: any): void;
该方法在实现阶段的作用在于针对的订阅"事件",从队列中遍历出所以的"事件",并执行对应的回调。on
:on(key: StringSymbolUnion, callback: (value: any) => void): void;
该方法在实现阶的作用在于将回调函数callback
存储在队列中off
:off(key: StringSymbolUnion, callback?: (value: any) => void): void;
该方法在实现阶的作用在于从队列中找到不属于callback
的元素进行移除(过滤)操作has
:has(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
代码中类Store
中on
方法的实现几乎是一样的。这里就不在详细说了。
至于另外三个方法的实现,也与之前提到的类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.ts
与event.ts
中分别定义的Hooks
接口,没有达到复用的效果(各自都定义了一次,没必要)。另外store.ts
与event.ts
中分别针对类Store
、Event
定义中部分方法代码的实现是一样的,没必要两边都各自实现一次,可以写一个基类(父类),然后从其进行继承extends
。针对需要重写的方法再进行重写会好很多;- 源码中对于代码健壮性的处理还是不错的,比如类
Event
中实现的四个方法中,对于key
的判断处理。