你们好~前端
本文是基于 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
应用间通讯方案就分享完啦!
两种通讯方案都有合适的使用场景,你们能够结合本身的须要选择便可。
若是您已经看到这里了,但愿您仍是点个 赞
再走吧~
您的 点赞
是对做者的最大鼓励,也可让更多人看到本篇文章!
本系列其余文章计划一到两个月内完成,计划以下:
若是感兴趣的话,请关注 博客 或者关注做者便可获取最新动态!
微前端技术交流群,感兴趣的能够进群一块儿交流呀~