@ice/stark-data原始碼解析

前端掃地僧發表於2021-11-20
在基於qiankun來進行微前端應用開發過程中,難免會碰到應用之間通訊的情況,如基於某個業態下某些定製化的業務需求。這時候就涉及到某個子應用需要知道當前的"宿主"是誰(標識是哪一個私有化專案定製的業務需求),而要知道"宿主"是誰,就需要"宿主"主動告知某個子應用,這就需要涉及到主、子應用資料通訊。

微前端下應用之間通訊方式

  • 基於qiankun框架自帶的通訊方式:通過api中的initGlobalState進行通訊
  • 基於三方庫的通訊方式:如@ice/stark-data進行通訊

基於qiankun自帶的通訊方式

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

基於@ice/stark-data通訊方式

https://i.iter01.com/images/fcbfcf836eb6bd0884983f23ceb0fdffcc88f14eefd95af2c07c81d42787958c.png

基於@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://i.iter01.com/images/c224b5b2751f5b1f87525c95cf7325e19ca5c7dfcd77b06487b055c63f913896.png

  • 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的判斷處理。

相關文章