在基於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<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
變數的型別,如果不是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
的判斷處理。