你們好~前端
本文是基於 qiankun
的微前端最佳實踐系列文章之 應用間通訊篇
,本文將分享在 qiankun
中如何進行應用間通訊。vue
在開始介紹 qiankun
的應用通訊以前,咱們須要先了解微前端架構如何劃分子應用。react
在微前端架構中,咱們應該按業務劃分出對應的子應用,而不是經過功能模塊劃分子應用。這麼作的緣由有兩個:git
- 在微前端架構中,子應用並非一個模塊,而是一個獨立的應用,咱們將子應用按業務劃分能夠擁有更好的可維護性和解耦性。
- 子應用應該具有獨立運行的能力,應用間頻繁的通訊會增長應用的複雜度和耦合度。
綜上所述,咱們應該從業務的角度出發劃分各個子應用,儘量減小應用間的通訊,從而簡化整個應用,使得咱們的微前端架構能夠更加靈活可控。github
咱們本次教程將介紹兩種通訊方式,redux
- 第一種是
qiankun
官方提供的通訊方式 -Actions
通訊,適合業務劃分清晰,比較簡單的微前端應用,通常來講使用第一種方案就能夠知足大部分的應用場景需求。 - 第二種是基於
redux
實現的通訊方式 -Shared
通訊,適合須要跟蹤通訊狀態,子應用具有獨立運行能力,較爲複雜的微前端應用。
Actions
通訊
咱們先介紹官方提供的應用間通訊方式 - Actions
通訊,這種通訊方式比較適合業務劃分清晰,應用間通訊較少的微前端應用場景。api
通訊原理
qiankun
內部提供了 initGlobalState
方法用於註冊 MicroAppStateActions
實例用於通訊,該實例有三個方法,分別是:緩存
setGlobalState
:設置globalState
- 設置新的值時,內部將執行淺檢查
,若是檢查到globalState
發生改變則觸發通知,通知到全部的觀察者
函數。onGlobalStateChange
:註冊觀察者
函數 - 響應globalState
變化,在globalState
發生改變時觸發該觀察者
函數。offGlobalStateChange
:取消觀察者
函數 - 該實例再也不響應globalState
變化。
咱們來畫一張圖來幫助你們理解(見下圖)性能優化
咱們從上圖能夠看出,咱們能夠先註冊 觀察者
到觀察者池中,而後經過修改 globalState
能夠觸發全部的 觀察者
函數,從而達到組件間通訊的效果。架構
實戰教程
咱們以 實戰案例 - feature-communication 分支 (案例是以 Vue
爲基座的主應用,接入 React
和 Vue
兩個子應用) 爲例,來介紹一下如何使用 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
方法綁定在下圖的按鈕中(見下圖)。
此時咱們點擊 2
次按鈕,將觸發咱們在主應用設置的 觀察者
函數(以下圖)
從上圖中咱們能夠看出:
- 第一次點擊:原
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.js
的 render
函數中注入,代碼實現以下:
// 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 通信頁
,就能夠看到效果啦!(見下圖)
React
子應用的實現也是相似的,實現代碼能夠參照 完整 Demo - feature-communication 分支,實現效果以下(見下圖)
小結
到這裏,qiankun 基礎通訊
就完成了!
咱們在主應用中實現了登陸功能,登陸拿到 token
後存入 globalState
狀態池中。在進入子應用時,咱們使用 actions
獲取 token
,再使用 token
獲取到用戶信息,完成頁面數據渲染!
最後咱們畫一張圖幫助你們理解這個流程(見下圖)。
Shared
通訊
因爲
Shared
方案實現起來會較爲複雜,因此當Actions
通訊方案知足需求時,使用Actions
通訊方案能夠獲得更好的官方支持。
官方提供的 Actions
通訊方案是經過全局狀態池和觀察者函數進行應用間通訊,該通訊方式適合大部分的場景。
Actions
通訊方案也存在一些優缺點,優勢以下:
- 使用簡單;
- 官方支持性高;
- 適合通訊較少的業務場景;
缺點以下:
- 子應用獨立運行時,須要額外配置無
Actions
時的邏輯; - 子應用須要先了解狀態池的細節,再進行通訊;
- 因爲狀態池沒法跟蹤,通訊場景較多時,容易出現狀態混亂、維護困難等問題;
若是你的應用通訊場景較多,但願子應用具有徹底獨立運行能力,但願主應用可以更好的管理子應用,那麼能夠考慮 Shared
通訊方案。
通訊原理
Shared
通訊方案的原理就是,主應用基於 redux
維護一個狀態池,經過 shared
實例暴露一些方法給子應用使用。同時,子應用須要單獨維護一份 shared
實例,在獨立運行時使用自身的 shared
實例,在嵌入主應用時使用主應用的 shared
實例,這樣就能夠保證在使用和表現上的一致性。
Shared
通訊方案須要自行維護狀態池,這樣會增長項目的複雜度。好處是可使用市面上比較成熟的狀態管理工具,如 redux
、mobx
,能夠有更好的狀態管理追蹤和一些工具集。
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
實例包括兩個方法 getToken
和 setToken
分別用於獲取 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"); } 複製代碼
從上面能夠看出,咱們在 props
的 shared
字段不爲空時,將會使用傳入的 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; } } }; 複製代碼
最後咱們打開頁面,看看在主應用中運行和獨立運行時的表現吧!(見下圖)
從 上圖 1
能夠看出,咱們在主應用中運行子應用時,shared
實例被主應用重載,登陸後能夠在狀態池中獲取到 token
,而且使用 token
成功獲取了用戶信息。
從 上圖 2
能夠看出,在咱們獨立運行子應用時,shared
實例是子應用自身的 shared
,在 localStorage
中沒法獲取到 token
,被攔截返回到主頁。
這樣一來,咱們就完成了 Shared
通訊啦!
小結
咱們從上面的案例也能夠看出 Shared
通訊方案的優缺點,這裏也作一些簡單的分析:
優勢有這些:
- 能夠自由選擇狀態管理庫,更好的開發體驗。 - 好比
redux
有專門配套的開發工具能夠跟蹤狀態的變化。 - 子應用無需瞭解主應用的狀態池實現細節,只須要了解
shared
的函數抽象,實現一套自身的shared
甚至空shared
便可,能夠更好的規範子應用開發。 - 子應用沒法隨意污染主應用的狀態池,只能經過主應用暴露的
shared
實例的特定方法操做狀態池,從而避免狀態池污染產生的問題。 - 子應用將具有獨立運行的能力,
Shared
通訊使得父子應用有了更好的解耦性。
缺點也有兩個:
- 主應用須要單獨維護一套狀態池,會增長維護成本和項目複雜度;
- 子應用須要單獨維護一份
shared
實例,會增長維護成本;
Shared
通訊方式也是有利有弊,更高的維護成本帶來的是應用的健壯性和可維護性。
最後咱們來畫一張圖對 shared
通訊的原理和流程進行解析(見下圖)
總結
到這裏,兩種 qiankun
應用間通訊方案就分享完啦!
兩種通訊方案都有合適的使用場景,你們能夠結合本身的須要選擇便可。
最後一件事
若是您已經看到這裏了,但願您仍是點個 贊
再走吧~
您的 點贊
是對做者的最大鼓勵,也可讓更多人看到本篇文章!
本系列其餘文章計劃一到兩個月內完成,計劃以下:
- 不一樣技術棧(如 React、Vue、Angular)子應用接入篇;
- 生命週期篇;
- IE 兼容篇;
- 生產環境部署篇;
- 性能優化、緩存方案篇;
- 從 0 到 1 篇;
若是感興趣的話,請關注 博客 或者關注做者便可獲取最新動態!
微前端技術交流羣,感興趣的能夠進羣一塊兒交流呀~