基於 qiankun 的微前端最佳實踐(圖文並茂) - 應用間通信篇

micro-app

大家好~

本文是基於 qiankun 的微前端最佳實踐系列文章之 應用間通信篇,本文將分享在 qiankun 中如何進行應用間通信。

在開始介紹 qiankun 的應用通信之前,我們需要先了解微前端架構如何劃分子應用。

在微前端架構中,我們應該按業務劃分出對應的子應用,而不是通過功能模塊劃分子應用。這麼做的原因有兩個:

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

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

我們本次教程將介紹兩種通信方式,

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

Actions 通信

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

通信原理

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

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

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

micro-app

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

實戰教程

我們以 實戰案例 - feature-communication 分支 (案例是以 Vue 爲基座的主應用,接入 ReactVue 兩個子應用) 爲例,來介紹一下如何使用 qiankun 完成應用間的通信功能。

建議 clone 實戰案例 - feature-communication 分支 分支代碼到本地,運行項目查看實際效果。

主應用的工作

首先,我們在主應用中註冊一個 MicroAppStateActions 實例並導出,代碼實現如下:

// micro-app-main/src/shared/actions.ts
import { initGlobalState, MicroAppStateActions } from "qiankun";

const initialState = {};
const actions: MicroAppStateActions = initGlobalState(initialState);

export default actions;

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

// micro-app-main/src/pages/login/index.vue
import actions from "@/shared/actions";
import { ApiLoginQuickly } from "@/apis";

@Component
export default class Login extends Vue {
  $router!: VueRouter;

  // `mounted` 是 Vue 的生命週期鉤子函數,在組件掛載時執行
  mounted() {
    // 註冊一個觀察者函數
    actions.onGlobalStateChange((state, prevState) => {
      // state: 變更後的狀態; prevState: 變更前的狀態
      console.log("主應用觀察者:token 改變前的值爲 ", prevState.token);
      console.log("主應用觀察者:登錄狀態發生改變,改變後的 token 的值爲 ", state.token);
    });
  }
  
  async login() {
    // ApiLoginQuickly 是一個遠程登錄函數,用於獲取 token,詳見 Demo
    const result = await ApiLoginQuickly();
    const { token } = result.data.loginQuickly;

    // 登錄成功後,設置 token
    actions.setGlobalState({ token });
  }
}

在上面的代碼中,我們在 Vue 組件mounted 生命週期鉤子函數中註冊了一個 觀察者 函數,然後定義了一個 login 方法,最後將 login 方法綁定在下圖的按鈕中(見下圖)。

micro-app

此時我們點擊 2 次按鈕,將觸發我們在主應用設置的 觀察者 函數(如下圖)

micro-app

從上圖中我們可以看出:

  • 第一次點擊:原 token 值爲 undefined,新 token 值爲我們最新設置的值;
  • 第二次點擊時:原 token 的值是我們上一次設置的值,新 token 值爲我們最新設置的值;

從上面可以看出,我們的 globalState 更新成功啦!

最後,我們在 login 方法最後加上一行代碼,讓我們在登錄後跳轉到主頁,代碼實現如下:

async login() {
  //...

  this.$router.push("/");
}

子應用的工作

我們已經完成了主應用的登錄功能,將 token 信息記錄在了 globalState 中。現在,我們進入子應用,使用 token 獲取用戶信息並展示在頁面中。

我們首先來改造我們的 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);
  }

  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
    mode: "history",
    routes,
  });

  // 掛載應用
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount("#app");
}

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

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

// micro-app-vue/src/pages/communication/index.vue
// 引入 actions 實例
import actions from "@/shared/actions";
import { ApiGetUserInfo } from "@/apis";

export default {
  name: "Communication",

  data() {
    return {
      userInfo: {}
    };
  },

  mounted() {
    // 註冊觀察者函數
    // onGlobalStateChange 第二個參數爲 true,表示立即執行一次觀察者函數
    actions.onGlobalStateChange(state => {
      const { token } = state;
      // 未登錄 - 返回主頁
      if (!token) {
        this.$message.error("未檢測到登錄信息!");
        return this.$router.push("/");
      }

      // 獲取用戶信息
      this.getUserInfo(token);
    }, true);
  },

  methods: {
    async getUserInfo(token) {
      // ApiGetUserInfo 是用於獲取用戶信息的函數
      const result = await ApiGetUserInfo(token);
      this.userInfo = result.data.getUserInfo;
    }
  }
};

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

最後,我們來看看實際效果。我們從登錄頁面點擊 Login 按鈕後,通過菜單進入 Vue 通訊頁,就可以看到效果啦!(見下圖)

micro-app

React 子應用的實現也是類似的,實現代碼可以參照 完整 Demo - feature-communication 分支,實現效果如下(見下圖)

micro-app

小結

到這裏,qiankun 基礎通信 就完成了!

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

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

micro-app

Shared 通信

由於 Shared 方案實現起來會較爲複雜,所以當 Actions 通信方案滿足需求時,使用 Actions 通信方案可以得到更好的官方支持。

官方提供的 Actions 通信方案是通過全局狀態池和觀察者函數進行應用間通信,該通信方式適合大部分的場景。

Actions 通信方案也存在一些優缺點,優點如下:

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

缺點如下:

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

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

通信原理

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

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

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

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

實戰教程

我們還是以 實戰案例 - feature-communication-shared 分支登錄流程 爲例,給大家展示如何使用 Shared 進行應用間通信。

主應用的工作

首先我們需要在主應用中創建 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() {
  // ApiLoginQuickly 是一個遠程登錄函數,用於獲取 token,詳見 Demo
  const result = await ApiLoginQuickly();
  const { token } = result.data.loginQuickly;

  // 使用 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;

子應用的工作

現在,我們來處理子應用需要做的工作。我們剛纔提到,我們希望子應用有獨立運行的能力,所以子應用也應該實現 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);

  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
    mode: "history",
    routes,
  });

  // 掛載應用
  instance = new Vue({
    router,
    render: (h) => h(App),
  }).$mount("#app");
}

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

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

// micro-app-vue/src/pages/communication/index.vue
// 引入 SharedModule
import SharedModule from "@/shared";
import { ApiGetUserInfo } from "@/apis";

export default {
  name: "Communication",

  data() {
    return {
      userInfo: {}
    };
  },

  mounted() {
    const shared = SharedModule.getShared();
    // 使用 shared 獲取 token
    const token = shared.getToken();

    // 未登錄 - 返回主頁
    if (!token) {
      this.$message.error("未檢測到登錄信息!");
      return this.$router.push("/");
    }

    this.getUserInfo(token);
  },

  methods: {
    async getUserInfo(token) {
      // ApiGetUserInfo 是用於獲取用戶信息的函數
      const result = await ApiGetUserInfo(token);
      this.userInfo = result.data.getUserInfo;
    }
  }
};

最後我們打開頁面,看看在主應用中運行和獨立運行時的表現吧!(見下圖)

micro-app

micro-app

上圖 1 可以看出,我們在主應用中運行子應用時,shared 實例被主應用重載,登錄後可以在狀態池中獲取到 token,並且使用 token 成功獲取了用戶信息。

上圖 2 可以看出,在我們獨立運行子應用時,shared 實例是子應用自身的 shared,在 localStorage 中無法獲取到 token,被攔截返回到主頁。

這樣一來,我們就完成了 Shared 通信啦!

小結

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

優點有這些:

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

缺點也有兩個:

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

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

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

micro-app

總結

到這裏,兩種 qiankun 應用間通信方案就分享完啦!

兩種通信方案都有合適的使用場景,大家可以結合自己的需要選擇即可。

最後一件事

如果您已經看到這裏了,希望您還是點個 再走吧~

您的 點贊 是對作者的最大鼓勵,也可以讓更多人看到本篇文章!

本系列其他文章計劃一到兩個月內完成,計劃如下:

  • 不同技術棧(如 React、Vue、Angular)子應用接入篇;
  • 生命週期篇;
  • IE 兼容篇;
  • 生產環境部署篇;
  • 性能優化、緩存方案篇;
  • 從 0 到 1 篇;

如果感興趣的話,請關注 博客 或者關注作者即可獲取最新動態!

微前端技術交流羣,感興趣的可以進羣一起交流呀~

micro-app

github 地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章