引言
该方案以redux
为核心,采用发布-订阅模式
进行封装,实现应用间通信数据上的响应式
,并在代码结构上实现模块化
,api方面仿照vuex,降低上手难度, 并可适用多框架
(如vue、react).
这次将针对前两版所完成的通信模块进行结构上的改版,此次改版实现了通信模块高度抽象化
、实际开发中实现业务代码与通信模块代码高度解耦
、更简洁的api
、更清晰的开发流程、更低的上手难度。
若还未看过前两版的童鞋,可以戳这里:
⚡qiankun微前端中的应用通信-不受框架限制的响应式数据管理
⚡qiankun微前端中的应用通信(二)-可订阅指定state|8月更文挑战
如何使用
是的,这一次我们不再从设计
、实现
、使用
来讲解, 而是先为大家展示如何使用
。
因为这一版最核心的,不是实现每一个具体的功能代码,而是重构了结构
,优化了使用
。
这次我们把整个shared模块
(通信模块)抽象并放置在@/utils/shared目录下(主应用、微应用均如此),因此后面的代码展示可能会频繁出现
import xxx from '@/utils/shared'
, 希望不会对你造成困扰。
- 首先在主应用里创建pool文件夹
@/pool
pool
├── index.ts
└── modules
├── locale.ts
└── user.ts
- 然后开始编写user.ts
import { Mutation } from '@/utils/shared';
interface UserInfo {
username: string,
}
interface State {
userinfo?: UserInfo | Record<string, never>
}
const state:State = {
userinfo: {},
};
const reducer = (userState: State = state, mutation: Mutation): State => {
switch (mutation.type) {
case 'SET_USERINFO': return {
...userState,
userinfo: mutation.payload,
}; break;
default: return userState;
}
};
const action = {
getUserinfo: ( { state }: any ): UserInfo | Record<string, never> => {
return state.userinfo || {};
},
setUserinfo: ({ commit }: any, userinfo: UserInfo): void => {
commit({
type: 'SET_USERINFO',
payload: userinfo,
});
},
};
export default {
name: 'user',
reducer,
action
};
可以看出整个user由三部分组成state
、reducer
、action
同时对外暴露name
、reducer
、action
其中name为模块名,action是微应用唯一能直接访问的api, 而state只能由reducer改变
graph TD
action --> reducer --> state
- 将user模块导入,同时生成shared
import Shared from '@/utils/shared';
import User from './modules/user';
import Locale from './modules/locale';
const shared = new Shared({
modules: {
User,
Locale,
},
})
export default shared.getShared();
至此shared已编写完毕,接下来通过qiankun的props传给微应用即可。
有没有感觉到,这次的编写非常简洁,代码结构也清晰?
因为虽然核心是用的redux
,但是写法上我模仿了vuex🤣
- 在主应用项目中,进行qiankun的微应用注册的地方
import { registerMicroApps, start } from 'qiankun';
import shared from '@/pool';
registerMicroApps([
{
name: 'micro',
entry: '//localhost:8888',
container: '#nav',
activeRule: '/micro',
props: {
shared
},
},
]);
start();
- 在微应用中,接收shared实例
// @/main.ts 已隐藏无关代码
import SharedModule from '@/pool';
function render(props: any = {}) {
const { container, shared = SharedModule.getShared() } = props;
SharedModule.overloadShared(shared);
}
- 在微应用中创建 @/pool目录 (也是上一步里,
import SharedModule
的来源)
pool
├── index.ts
而@/pool/index.ts
也十分简单,相当于只需做模块注册即可
import SharedModule from '@/utils/shared/sharedmodule';
import { Shared } from './shared';// 本地模块
SharedModule.initShared(Shared);// 本地模块
export default SharedModule;
'@/utils/shared/sharedmodule';
与主应用一样,存放的是抽象出来的shared模块的代码
而第二行和第三行是用于加载本地shared实例(确保微应用独立运行时不会缺失shared),如果不考虑独立运行的话,这两行代码也可以删去。
- 现在你就可以在微应用自身的store里使用了
import Vue from 'vue';
import Vuex from 'vuex';
import SharedModule from '@/pool';// 从第6步创建的pool目录里引入
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: {
/*
初始化shared模块
建议在main.ts中,在执行完SharedModule.overloadShared(shared)后就执行该初始化
*/
initShared() {
shared = SharedModule.getShared();
this.dispatch('setLocale');
this.dispatch('setUserinfo');
SharedModule.subscribe([
(stateName: string) => {
if(stateName === 'locale') this.dispatch('setLocale');
},
(stateName: string) => {
if(stateName === 'user') 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: {
},
});
SharedModule.subscribe
用于注册订阅事件。通过传入回调函数进行订阅, 可以数组形式批量传入,当pool内数据有变化时(监听到redux提供的set方法执行了),会通过回调函数统一发布。- 注册的订阅事件可以接收一个参数 stateName,该参数会返回当前发生改变的state, 例如此次demo的state有 user 和 locale, 当user里的userinfo发生改变时, 每个订阅事件都会获得
stateName
参数,告诉你user
这个state发生了改变,这可以更好的帮助你决定更新哪些模块的状态 - 由于2实现的核心是
浅比较
,因此当stateName
为空字符串时,可以判断出是嵌套较深的state发生了改变,这在一定程度上也可以知道到底是哪个state改变了
可以看到,这次的版本极大地抽象出了shared模块,也因此在使用上做到更加地简洁明了。
实现
在看完了如何使用后, 是不是对@/utils/shared
这个黑盒子里到底装了什么起了兴趣?
我们现在就来揭开它的面纱
设计思路
-
Shared
实例基于BaseShared
基类生成-
BaseShared基类接收两个构造参数:
Pool
、Action
-
Pool由
redux
的createStore
生成,所需参数为所有module的reducer, 也就是说Pool是所有reducer的合集 -
Action负责管理所有module的action
-
BaseShared接收的
Pool
、Action
由flatModule导出
-
-
flatModule负责管理所有的Module(即主应用中使用中所编写的user、locale等)
-
flatModule会将所有的Module统一拆分成
reducer
和action
(正如你编写那些模块时暴露的reducer和action)然后分别对reducer、action进行处理,最终向外暴露为pool、actions -
reducer
即为redux中的reducer类型,可实现对状态树的操作,并在flatModule中交由Pool模块处理(核心是由redux
的createStore
进行生成), 对reducer
有疑问的,可以参考redux文档 -
action
类似vuex的action, 用于提交mutation
(即交由reducer来更改状态),同时action中的api也将是暴露给使用者的接口(也就是使用过程中是无法直接操作reducer的,只能调用action),在flatModule中交由action模块进行处理
-
目录结构
@/utils/shared
shared
├── action.ts 负责处理action,并返回actions给flatModule
├── base.ts BaseShared类,是shared模块的核心
├── index.ts 入口文件
├── module.ts 即flatModule 负责处理开发者所编写、导入的模块
├── pool.ts 负责处理reducer,并返回pool给flatModule
├── utils.ts 工具函数
├── types.ts 项目中的类型,并向外暴露
└── sharedmodule 给微应用接收shared实例并使用的工具类
└── index.ts
实际代码
- 我们先来看看入口文件有哪些内容
import BaseShared from './base';
import { flatModule } from './module';
class Shared {
static shared:BaseShared;
constructor(option = {}) {
const { pool, actions } = flatModule(option);
Shared.shared = new BaseShared(pool, actions);
}
public getShared() {
return Shared.shared;
}
}
export default Shared;
export * from './types';
可以看到入口最主要的就是导出Shared类
而Shared类在构造时要做的就是通过BaseShared基类生成shared实例
而BaseShared需要的两个参数pool
、actions
则是flatModule处理option后得来
- 那么接下来我们就来看看BaseShared是什么,以及flatModule又做了什么处理
// base.ts
import { Store } from 'redux';
import { update } from './utils'
export default class BaseShared {
static pool: Store;
static actions = new Map();
static cacheState: any = null;
constructor(Pool: Store, action = new Map()) {
BaseShared.pool = Pool;
BaseShared.actions = action;
}
public init(listener: Function): void {
BaseShared.cacheState = BaseShared.pool.getState();
BaseShared.pool.subscribe(() => {
const newState: any = BaseShared.pool.getState();
const stateName = update(BaseShared.cacheState, newState);
BaseShared.cacheState = newState;
return listener(stateName);
});
}
public dispatch(target: string, param?: any):any {
const res:any = BaseShared.actions.get(target)(param ? param : null);
return res;
}
}
可以看到BaseShared
要实现得功能是:
init
:订阅事件,并在state发生改变时触发事件。dispatch
: 分发事件,使用户可以调用所需得action方法
// module.ts
import createPool from './pool';
import createAction, { installAction } from './action';
export function flatModule(option: any) {
const { modules } = option;
const reducers: any = {}
let actionList: Array<any> = [];
Object.values(modules).forEach((obj: any) => {
const { reducer, action, name } = obj;
reducers[name] = reducer;
actionList.push({
name,
action,
})
})
const pool = createPool(reducers);
let actions = new Map();
actionList.forEach((v) => {
const k = createAction(installAction(v.action, pool, v.name), v.name);
actions = new Map([...actions, ...k]);
})
return {
pool,
actions,
}
}
flatModule从option中取出导入得modules,再将modules拆分为两类,reducer和action, 分别交由createPool和createAction处理并得到最终的pool,actions
- 说到这里,我们再来看看createPool和createAction内部是怎么样的吧
// pool.ts
import { combineReducers, createStore } from 'redux';
function createPool(reducers = {}) {
const staticReducers = combineReducers(reducers);
return createStore(staticReducers);
}
export default createPool;
原来createPool要做的是将reducer集合中的所有reducer交由combineReducers进行合并, 合并后的结果再交由createStore生成pool
对了, combineReducers
和 createStore
都来自于 redux
噢
// action.ts
import { Store } from './types';
function createAction(action: Record<string, Function>, name: string): Map<string, unknown> {
const actions = new Map();
Object.keys(action).forEach((key) => {
actions.set(`${name}/${key}`, action[key]);
});
return actions;
}
export function installAction(action: Record<string, Function>, pool: Store, moduleName: string) {
Object.keys(action).map((key) => {
action[key] = registerAction(pool, action[key], moduleName);
});
return action
}
function registerAction(pool: Store, actionFn: Function, moduleName: string) {
return function wrappedActionHandler (...param: any) {
let res = actionFn({
state: (() => {
pool.getState()
let state = pool.getState();
return state[moduleName];
})(),
commit: pool.dispatch,
}, ...param)
return res
}
}
export default createAction;
createAction
主要将action处理为Map集合,键名则以模块名/方法名
的格式处理
registerAction
则为每一个action提供一层包装,是action可以获得所需的上下文环境(部分)
- 最后再来看一下SharedModule
// sharedmodule/index.ts
class SharedModule {
static shared: any;
static listener: Array<Function> = [];
static initShared(shared: any) {
SharedModule.shared = shared;
}
/**
* 重载 shared
*/
static overloadShared(shared: any) {
SharedModule.shared = shared;
shared.init((stateName: string) => {
SharedModule.listener.forEach((fn) => {
fn(stateName);
});
});
}
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;
SharedModule提供的功能也很简单,载入实例、获取实例、注册订阅事件。
剩下的utils.ts、types.ts就不说了,不影响整个模块的思路,仅仅只是做一些抽离而已