theme: smartblue highlight: a11y-dark
引言
对于微前端来说,应用间通信(主要为主应用-微应用)往往是架构设计之初就要考虑的核心需求,并且这种通信需求往往不是一个简单的传参就能满足的(如果是的话,路由
、localstorage
就可以满足),因此这就要求我们在进行技术选型,调研微前端解决方案时,通信方案也要一并考虑。
背景
在调研了众多微前端解决方案后,我们初步选择了qiankun
框架,但是qiankun自身在通信方面并不提供完整的解决方案,更多的是提供api,通过这些api可以快捷地实现通讯功能,但是对于更丰富、未来可能更复杂地业务需求,依照qiankun文档中的示例,恐怕难以满足。因此在横向对比了众多方案后(其中一篇文章给了我很大的启发-基于 qiankun 的微前端最佳实践(图文并茂) - 应用间通信篇)
最终选择使用redux
作为微前端的通信解决方案。
特点
该方案以redux
为核心,采用发布-订阅模式
进行封装,实现应用间通信数据上的响应式
,并在代码结构上实现模块化
,api方面仿照vuex,降低上手难度, 并可适用多框架
(如vue、react).
实现
设计思路
-
Shared
实例基于BaseShared
基类生成-
BaseShared基类接收两个构造参数:
Pool
、Action
-
Pool由
redux
的createStore
生成,所需参数为所有module的reducer, 也就是说Pool是所有reducer的合集 -
Action负责管理所有module的action
-
-
每个Module均包含两个部分,
reducer
与action
reducer
即为redux中的reducer类型,可实现对状态树的操作,并最终导出给Pool模块交由redux
的createStore
进行生成, 对reducer
有疑问的,可以参考redux文档action
类似vuex的action, 用于提交mutation
(即交由reducer来更改状态),同时action中的api也将是暴露给使用者的接口(也就是使用过程中是无法直接操作reducer的,只能调用action)
注意: Pool即为Redux文档中的store, 只是一般项目中的状态模块均命名为store, 因此为了避免混淆, 取名为Pool
实际代码
注意: 本次演示我创建了两个项目, 一个叫plat,一个叫micro, 顾名思义, plat项目即为主应用, micro为微应用
目录
主应用项目代码里的shared目录 @/shared
shared
├── action.ts
├── base.ts
├── index.ts
├── pool.ts
└── modules
├── locale
│ ├── action.ts
│ └── reducer.ts
└── user
├── action.ts
└── reducer.ts
开发流程
- 以开发user模块开始
// @/shared/modules/user/reducer.ts
interface UserInfo {
username: string,
}
interface State {
userinfo?: UserInfo | Record<string, never>
}
const state:State = {
userinfo: {},
};
type Mutation = {
type: string;
payload: any;
}
const reducer = (userState: State = state, mutation: Mutation): State => {
switch (mutation.type) {
case 'SET_USERINFO': return {
...userState,
userinfo: mutation.payload,
}; break;
default: return userState;
}
};
export default reducer;
// @/shared/modules/user/action.ts
import pool from '../../pool';
interface UserInfo {
username: string,
}
export const userAction = {
name: 'user',// 模块名称,shared将根据名称区分不同模块的action
getUserinfo: (): UserInfo | Record<string, never> => {
const state = pool.getState();
return state.user.userinfo || {};
},
setUserinfo: (userinfo: UserInfo): void => {
pool.dispatch({
type: 'SET_USERINFO',
payload: userinfo,
});
},
};
- 将user模块的reducer 和 action 分别导入到pool模块和action模块
// @/shared/pool.ts
import { combineReducers, createStore } from 'redux';
import userReducer from './modules/user/reducer';
import localeReducer from './modules/locale/reducer';
const staticReducers = combineReducers({
user: userReducer,
locale: localeReducer,
});
const pool = createStore(staticReducers);
export default pool;
// @/shared/action.ts
import { localeAction } from './modules/locale/action';
import { userAction } from './modules/user/action';
const actionList = [
localeAction,
userAction,
];
const actions = new Map();
actionList.forEach((obj: any) => {
const { name } = obj;
Object.keys(obj).forEach((key) => {
if (key !== 'name') actions.set(`${name}/${key}`, obj[key]);
});
});
export default actions;
- 将pool模块和action模块导入到index.ts中,由BaseShared基类构造为shared实例
// @/shared/index.ts
import BaseShared from './base';
import pool from './pool';
import actions from './action';
const shared = new BaseShared(pool as any, actions);
export default shared;
// @/shared/base.ts
import { Store } from 'redux';
export default class BaseShared {
static pool: Store;
static actions = new Map();
constructor(Pool: Store, action = new Map()) {
BaseShared.pool = Pool;
BaseShared.actions = action;
}
public init(listener: any): void {
BaseShared.pool.subscribe(listener);
}
public dispatch(target: string, param: any = ''):any {
const res:any = BaseShared.actions.get(target)(param);
return res;
}
}
BaseShared基类是整个shared模块的核心,实现action的分发(dispatch), redux订阅事件的初始化(init)
到这里为止,shared核心内容已经完成,接下来要做的,就是将shared对接qiankun,并在子应用中接收该实例了
- 在主应用项目中,进行qiankun的微应用注册的地方
import { registerMicroApps, start } from 'qiankun';
import shared from '@/shared';
registerMicroApps([
{
name: 'micro',
entry: '//localhost:8888',
container: '#nav',
activeRule: '/micro',
props: {
shared
},
},
]);
start();
- 在微应用中,接收shared实例
// @/main.ts 已隐藏无关代码
import SharedModule from '@/shared';
function render(props: any = {}) {
const { container, shared = SharedModule.getShared() } = props;
SharedModule.overloadShared(shared);
}
SharedModule是什么? 是微应用用于管理shared实例的模块
微应用中的SharedModule目录如下
shared
├── index.ts
└── shared.ts // 当微应用独立运行时(即不存在主应用的传参), 替代主应用的shared
// @/shared/index.ts
import { Shared } from './shared';// 若不需要微应用独立运行,那么此处可以忽视
class SharedModule {
static shared = new Shared();// shared实例
static listener: Array<any> = [];// 监听事件列表
/**
* 重载 shared
*/
static overloadShared(shared) {
SharedModule.shared = shared;
shared.init(() => {
SharedModule.listener.forEach((fn) => {
fn();
});
});
}
/**
* 初始化监听事件列表
*/
static subscribe(fn: any) {
if (!fn) throw Error('缺少参数');
if (fn.length) {
SharedModule.listener.push(...fn);
} else {
SharedModule.listener.push(fn);
}
}
/**
* 获取 shared 实例
*/
static getShared() {
return SharedModule.shared;
}
}
export default SharedModule;
- 在微应用的store中,使用shared实例
// @/store/index.ts
import Vue from 'vue';
import Vuex from 'vuex';
import SharedModule from '@/shared';
Vue.use(Vuex);
let shared:any = null;
export interface UserInfo {
username: string,
}
interface State {
locale: string,
userinfo: UserInfo | Record<string, never>,
}
export default new Vuex.Store({
state: {
locale: '',
userinfo: {},
},
mutations: {
SET_LOCALE: (state: State, locale: string) => {
state.locale = locale;
},
SET_USERINFO: (state: State, userinfo: UserInfo) => {
state.userinfo = userinfo;
},
},
actions: {
initShared() {
shared = SharedModule.getShared();
// 通过 SharedModule.subscribe 传入回调函数进行订阅, 可以数组形式批量传入
// 当pool内数据有变化时(监听到redux提供的set方法执行了),会通过回调函数统一发布
this.dispatch('setLocale');
this.dispatch('setUserinfo');
SharedModule.subscribe([
() => {
this.dispatch('setLocale');
},
() => {
this.dispatch('setUserinfo');
},
]);
},
setLocale({ commit }) {
const locale = shared.dispatch('locale/getLocale');
commit('SET_LOCALE', locale);
},
setUserinfo({ commit }) {
const userinfo = shared.dispatch('user/getUserinfo');
commit('SET_USERINFO', userinfo);
},
},
getters: {
locale: (state: State) => state.locale,
userinfo: (state: State) => state.userinfo,
},
modules: {
},
});
至此已通过shared实例,完成主、微应用之间的通信
- 在页面中实际使用
// 主应用的App.vue
<template>
<div id="app">
<h2>plat</h2>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
<p>
<span>username:</span>
<input v-model="username"/>
<button @click="handleSubmit">submit</button>
</p>
<p>
userinfo: {{ userinfo }}
</p>
<p>
language: {{ locale }}
<button @click="handleChange">change</button>
</p>
<div id="nav">
<router-view/>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return {
username: '',
};
},
computed: {
...mapGetters(['userinfo', 'locale']),
},
methods: {
handleSubmit() {
const userinfo = {
username: this.username,
};
this.$store.dispatch('getInfo', userinfo);
},
handleChange() {
let locale = ''
if (this.locale === 'zh') {
locale = 'en';
} else {
locale = 'zh';
}
this.$store.dispatch('setLocale', locale);
}
},
};
</script>
// 微应用的HelloWorld.vue
<template>
<div class="hello">
<h2>micro</h2>
<p>userinfo: {{ userinfo }}</p>
<p>language: {{ locale }}</p>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters(['userinfo', 'locale']),
},
};
</script>
api介绍
api | 说明 |
---|---|
SharedModule.overloadShared | 用于重载SharedModule内的shared实例 |
SharedModule.getShared() | 获取SharedModule内部的shared实例 |
SharedModule.subscribe() | 注册订阅事件(可传数组),在state发生改变后会触发此处订阅的事件 |
shared.dispatch() | 类似vuex的store.dispatch,用于调用不同模块的action |
api的设计思路:
该shared通信模块,依据redux本身的(state + action => reducer)结合vuex的(state + mutation + action), 最终设计结构为(state + mutation => reducer + action)。
即:只有mutation能够操作state, 而mutation需要action调用,微应用只能使用主应用暴露出来的action,而主应用可自行决定要暴露的action。
总结&Todo
- 这个设计还有个问题,由于该响应式是通过发布-订阅模式实现的, 而该模式依赖于redux提供的subscribe这个api, 但仅凭这个api,我们无法精确地得知究竟哪个属性发生改变, 也就是说只要是state任何属性发生改变, 都会触发subscribe, 当state变得庞大时,该通信模块地性能将不可避免地下降(当然,要非常庞大),因此按模块发布,将会是后续改良的重点
- 目前在使用或者说上手难度上来说,还有点复杂, 后续看看能不能更高度的抽象, 暴露更简洁的api
- 目前演示demo已经上传至gitee: 主应用、微应用
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 圊妖 原文链接:https://juejin.im/post/6989132702775525407