淺析微前端qiankun的2種應用間通信方式(actions通信及shared通信、各自通信原理及實例代碼)


一、應該如何划分子應用

  在開始介紹 qiankun 的應用通信之前,我們需要先了解微前端架構如何划分子應用。在微前端架構中,我們應該按業務划分出對應的子應用,而不是通過功能模塊划分子應用。這么做的原因有兩個:

  1. 在微前端架構中,子應用並不是一個模塊,而是一個獨立的應用,我們將子應用按業務划分可以擁有更好的可維護性和解耦性。
  2. 子應用應該具備獨立運行的能力,應用間頻繁的通信會增加應用的復雜度和耦合度。

  綜上所述,我們應該從業務的角度出發划分各個子應用,盡可能減少應用間的通信,從而簡化整個應用,使得我們的微前端架構可以更加靈活可控。

  補充一下,上面是理想狀態,但是理想狀態我們很難達到。微前端最核心的點,是可以做到技術棧無關。比如如果你為了做到技術棧無關,想體驗不同的技術,那就無所謂了,直接按模塊划分即可,因為單獨獨立的應用,很多公司遇到這種需求也蠻少。我就是為了實踐不同技術棧,所以上了微前端。

  本次將介紹兩種通信方式:

  1. 第一種是 qiankun 官方提供的通信方式 - Actions 通信,適合業務划分清晰,比較簡單的微前端應用,一般來說使用第一種方案就可以滿足大部分的應用場景需求。
  2. 第二種是基於 redux 實現的通信方式 - Shared 通信,適合需要跟蹤通信狀態,子應用具備獨立運行能力,較為復雜的微前端應用。

二、Actions 通信

  我們先介紹官方提供的應用間通信方式 - Actions 通信,這種通信方式比較適合業務划分清晰,應用間通信較少的微前端應用場景。

1、通信原理

  qiankun 內部提供了 initGlobalState 方法用於注冊 MicroAppStateActions 實例用於通信,該實例有三個方法,分別是:

  • setGlobalState:設置 globalState - 設置新的值時,內部將執行 淺檢查,如果檢查到 globalState 發生改變則觸發通知,通知到所有的 觀察者 函數。
  • onGlobalStateChange:注冊 觀察者 函數 - 響應 globalState 變化,在 globalState 發生改變時觸發該 觀察者 函數。
  • offGlobalStateChange:取消 觀察者 函數 - 該實例不再響應 globalState 變化。

  我們來畫一張圖來幫助大家理解(見下圖)

  我們從上圖可以看出,我們可以先注冊 觀察者 到觀察者池中,然后通過修改 globalState 可以觸發所有的 觀察者 函數,從而達到組件間通信的效果。

2、通信實例

(1)主應用需要做的工作

// micro-app-main/src/shared/actions.ts
import { initGlobalState, MicroAppStateActions } from "qiankun"; const initialState = { status: 'login' }; // 全局狀態池給了個默認值
const shareActions: MicroAppStateActions = initGlobalState(initialState); export default shareActions;

(2)組件里注冊觀察者函數,在使用時觸發全局狀態池更新

  在注冊 MicroAppStateActions 實例后,我們在需要通信的組件中使用該實例,並注冊 觀察者 函數,我們這里以登錄功能為例,實現如下:

// 1、先引入 actions
import actions from "@/shared/actions"; @Component export default class Login extends Vue { mounted() { // 2、注冊一個觀察者函數
    actions.onGlobalStateChange((state, prevState) => { console.log("主應用觀察者:token 改變前的值為 ", prevState.token); console.log("主應用觀察者:登錄狀態發生改變,改變后的 token 的值為 ", state.token); }); } async login() { // 3、觸發狀態池更新:登錄成功后,設置 token
 actions.setGlobalState({ token }); } }

(3)子應用需要做的工作

  我們首先來改造我們的 Vue 子應用,首先我們設置一個 Actions 實例,代碼實現如下:

// micro-app-vue/src/shared/actions.js
function emptyAction() { // 警告:提示當前使用的是空 Action
  console.warn("Current execute action is empty!"); } class Actions { // 默認值為空 Action
  actions = { onGlobalStateChange: emptyAction, setGlobalState: emptyAction }; // 設置 actions setActions(actions) { this.actions = actions; } // 映射 onGlobalStateChange(...args) { return this.actions.onGlobalStateChange(...args); } // 映射 setGlobalState(...args) { return this.actions.setGlobalState(...args); } } const actions = new Actions(); export default actions;
  我們創建 actions 實例后,我們需要為其注入真實 Actions。我們在入口文件 main.jsrender 函數中注入,代碼實現如下:
// micro-app-vue/src/main.js //...

/** * 渲染函數 * 主應用生命周期鈎子中運行/子應用單獨啟動時運行 */ function render(props) { if (props) { // 注入 actions 實例
 actions.setActions(props); } ...... }

  從上面的代碼可以看出,掛載子應用時將會調用 render 方法,我們在 render 方法中將主應用的 actions 實例注入即可。

  最后我們在子應用的 通訊頁 獲取 globalState 中的 token,使用 token 來獲取用戶信息,最后在頁面中顯示用戶信息。流程偽代碼實現如下:

// 引入 actions 實例
import actions from "@/shared/actions"; export default { ...... mounted() { // 注冊觀察者函數:onGlobalStateChange 第二個參數為 true,表示立即執行一次觀察者函數
    actions.onGlobalStateChange(state => { const { token } = state; // 未登錄 - 返回主頁
      if (!token) { this.$message.error("未檢測到登錄信息!"); return this.$router.push("/"); } // 獲取用戶信息
      this.getUserInfo(token); }, true); }, ...... };

  從上面的代碼可以看到,我們在組件掛載時注冊了一個 觀察者 函數並立即執行,從 globalState/state 中獲取 token,然后使用 token 獲取用戶信息,最終渲染在頁面中。

3、總結

  到這里,qiankun 基礎通信 就完成了!我們在主應用中實現了登錄功能,登錄拿到 token 后存入 globalState 狀態池中。在進入子應用時,我們使用 actions 獲取 token,再使用 token 獲取到用戶信息,完成頁面數據渲染!

  最后我們畫一張圖幫助大家理解這個流程(見下圖):

4、在項目里實戰:在 Nuxt.js 里使用的問題

  由於在 nuxtjs 里服務器不支持瀏覽器相關的 api,如 actions 的 initGlobalState 里卻使用了很多瀏覽器 api,所以會報錯。如何解決呢?

  使用 插件 plugins 配合 ssr: false,使插件在客戶端時才加載,這樣就可以了。

(1)主應用需要做的

// ssr: false 插件里引入
import shareActions from "@/utils/shareActions"
// 注冊一個觀察者函數
shareActions.onGlobalStateChange((state, prevState) => { console.log("主應用觀察者:變更前-", state, ",變更后-", prevState) }) Vue.prototype.$actions = shareActions
// 在需要使用的地方直接這樣操作即可
this.$actions.setGlobalState({ logStatus: 'logout' })

(2)子應用對應業務

// 子應用里直接注冊監聽函數,然后做對應業務即可
export async function mount(props: any) { // console.log('[vue] props from main framework', props)
 render() props?.onGlobalStateChange((state: any) => { state?.logStatus === 'logout' && store.commit('clearToken') }, true) }

三、Shared 通信

  官方提供的 Actions 通信方案是通過全局狀態池和觀察者函數進行應用間通信,該通信方式適合大部分的場景。Actions 通信方案也存在一些優缺點,優點如下:

  1. 使用簡單;
  2. 官方支持性高;
  3. 適合通信較少的業務場景;

  缺點如下:

  1. 子應用獨立運行時,需要額外配置無 Actions 時的邏輯;
  2. 子應用需要先了解狀態池的細節,再進行通信;
  3. 由於狀態池無法跟蹤,通信場景較多時,容易出現狀態混亂、維護困難等問題;

  如果你的應用通信場景較多,希望子應用具備完全獨立運行能力,希望主應用能夠更好的管理子應用,那么可以考慮 Shared 通信方案。

1、通信原理

  Shared 通信方案的原理就是,主應用基於 redux 維護一個狀態池,通過 shared 實例暴露一些方法給子應用使用。同時,子應用需要單獨維護一份 shared 實例,在獨立運行時使用自身的 shared 實例,在嵌入主應用時使用主應用的 shared 實例,這樣就可以保證在使用和表現上的一致性。

  Shared 通信方案需要自行維護狀態池,這樣會增加項目的復雜度。好處是可以使用市面上比較成熟的狀態管理工具,如 reduxmobx,可以有更好的狀態管理追蹤和一些工具集。

  Shared 通信方案要求父子應用都各自維護一份屬於自己的 shared 實例,同樣會增加項目的復雜度。好處是子應用可以完全獨立於父應用運行(不依賴狀態池),子應用也能以最小的改動被嵌入到其他 第三方應用 中。

  Shared 通信方案也可以幫助主應用更好的管控子應用。子應用只可以通過 shared 實例來操作狀態池,可以避免子應用對狀態池隨意操作引發的一系列問題。主應用的 Shared 相對於子應用來說是一個黑箱,子應用只需要了解 Shared 所暴露的 API 而無需關心實現細節

  個人總結一下:

  (1)比如我們主應用使用現有vuex狀態管理,然后再提供一個 shared 實例暴露一些 api 來操作其 vuex,再把 shared 實例通過 props 傳給子應用;

  (2)子應用使用現有vuex狀態管理,子應用也實現自己的shared;然后在入口文件處如果主應用有傳 shared 實例,則使用主應用實例,沒有(即獨立運行時)則使用子應用自己的 shared實例。

2、通信實例

(1)主應用的工作

   首先我們需要在主應用中創建 store 用於管理全局狀態池,這里我們使用 redux 來實現

// micro-app-main/src/shared/store.ts
import { createStore } from "redux"; export type State = { token?: string; }; type Action = { type: string; payload: any; }; const reducer = (state: State = {}, action: Action): State => { switch (action.type) { default: return state; // 設置 Token
    case "SET_TOKEN": return { ...state, token: action.payload, }; } }; const store = createStore<State, Action, unknown, unknown>(reducer); export default store;

  從上面可以看出,我們使用 redux 創建了一個全局狀態池,並設置了一個 reducer 用於修改 token 的值。

  接下來我們需要實現主應用的 shared 實例,代碼實現如下:

// micro-app-main/src/shared/index.ts
import store from "./store"; class Shared { // 獲取 Token
  public getToken(): string { const state = store.getState(); return state.token || ""; } // 設置 Token
  public setToken(token: string): void { // 將 token 的值記錄在 store 中
 store.dispatch({ type: "SET_TOKEN", payload: token }); } } const shared = new Shared(); export default shared;
  從上面實現可以看出,我們的 shared 實現非常簡單,shared 實例包括兩個方法 getTokensetToken 分別用於獲取 token 和設置 token。接下來我們還需要對我們的 登錄組件 進行改造,將 login 方法修改一下,修改如下:
// micro-app-main/src/pages/login/index.vue
async login() { // 使用 shared 的 setToken 方法記錄 token
 shared.setToken(token); this.$router.push("/"); }

  從上面可以看出,在登錄成功后,我們將通過 shared.setToken 方法進行業務處理token 記錄在 store 中。

  最后,我們需要shared 實例通過 props 傳遞給子應用,代碼實現如下:

// micro-app-main/src/micro/apps.ts
import shared from "@/shared"; const apps = [ { name: "ReactMicroApp", entry: "//localhost:10100", container: "#frame", activeRule: "/react", // 通過 props 將 shared 傳遞給子應用
 props: { shared }, }, { name: "VueMicroApp", entry: "//localhost:10200", container: "#frame", activeRule: "/vue", // 通過 props 將 shared 傳遞給子應用
 props: { shared }, }, ]; export default apps;

(2)子應用的工作

  現在,我們來處理子應用需要做的工作。我們剛才提到,我們希望子應用有獨立運行的能力,所以子應用也應該實現 shared,以便在獨立運行時可以擁有兼容處理能力。代碼實現如下:

// micro-app-vue/src/shared/index.js
class Shared { // 獲取 Token getToken() { // 子應用獨立運行時,在 localStorage 中獲取 token
    return localStorage.getItem("token") || ""; } // 設置 Token setToken(token) { // 子應用獨立運行時,在 localStorage 中設置 token
    localStorage.setItem("token", token); } } class SharedModule { static shared = new Shared(); // 重載 shared
  static overloadShared(shared) { SharedModule.shared = shared; } // 獲取 shared 實例
  static getShared() { return SharedModule.shared; } } export default SharedModule;

  從上面我們可以看到兩個類,我們來分析一下其用處:

  • Shared:子應用自身的 shared,子應用獨立運行時將使用該 shared,子應用的 shared 使用 localStorage 來操作 token
  • SharedModule:用於管理 shared,例如重載 shared 實例、獲取 shared 實例等等;

  我們實現了子應用的 shared 后,我們需要在入口文件處注入 shared,代碼實現如下:

// micro-app-vue/src/main.js // 渲染函數:主應用生命周期鈎子中運行/子應用單獨啟動時運行 function render(props = {}) { // 當傳入的 shared 為空時,使用子應用自身的 shared // 當傳入的 shared 不為空時,主應用傳入的 shared 將會重載子應用的 shared
  const { shared = SharedModule.getShared() } = props; SharedModule.overloadShared(shared); ...... }

  從上面可以看出,我們在 propsshared 字段不為空時,將會使用傳入的 shared 重載子應用自身的 shared。這樣做的話,主應用的 shared 和子應用的 shared 在使用時的表現是一致的。

  然后我們修改子應用的 通訊頁,使用 shared 實例獲取 token,代碼實現如下:

// 引入 SharedModule
import SharedModule from "@/shared"; export default { mounted() { const shared = SharedModule.getShared(); // 使用 shared 獲取 token
    const token = shared.getToken(); } };

3、總結

  我們從上面的案例也可以看出 Shared 通信方案的優缺點,這里也做一些簡單的分析,優點有這些:

  • 可以自由選擇狀態管理庫,更好的開發體驗。 - 比如 redux 有專門配套的開發工具可以跟蹤狀態的變化。
  • 子應用無需了解主應用的狀態池實現細節,只需要了解 shared 的函數抽象,實現一套自身的 shared 甚至空 shared 即可,可以更好的規范子應用開發。
  • 子應用無法隨意污染主應用的狀態池,只能通過主應用暴露的 shared 實例的特定方法操作狀態池,從而避免狀態池污染產生的問題。
  • 子應用將具備獨立運行的能力,Shared 通信使得父子應用有了更好的解耦性。

  缺點也有兩個:

  • 主應用需要單獨維護一套狀態池,會增加維護成本和項目復雜度;
  • 子應用需要單獨維護一份 shared 實例,會增加維護成本;

  Shared 通信方式也是有利有弊,更高的維護成本帶來的是應用的健壯性和可維護性。

  最后我們來畫一張圖對 shared 通信的原理和流程進行解析(見下圖):

  個人總結一下實踐應用的幾個步驟:

(1)主應用:自身 store(vuex)狀態管理   ——  創建 shared 實例 對 store 進行部分 api 管理  ——  在使用的時候引入 shared 實例觸發對 vuex 狀態的 api 管理  ——  將 shared 實例通過 props 傳給子應用。

(2)子應用:自身 shared 實例、自身 sharedModule 管理shared實例  ——  在入口處,主應用有傳 shared 實例則使用主應用 shared 實例覆載自身的實例;若未傳(即獨立運行)則使用自身的 shared 實例   ——   在使用的時候引入 shared 實例觸發 api 管理

(3)這樣的話子應用也可以有自己的 store 狀態管理,若想保證自己的狀態管理和主應用的狀態管理一直,則再觸發 vuex 更新的時候,調用 shared 實例觸發主應用 vuex 更新即可。

  到這里,兩種 qiankun 應用間通信方案就分享完啦!兩種通信方案都有合適的使用場景,大家可以結合自己的需要選擇即可。

來自於這篇文章的學習及在項目中的實戰使用:《基於 qiankun 的微前端最佳實踐 - 應用間通信篇 - https://juejin.cn/post/6844904151231496200》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM