⚡qiankun微前端中的应用通信(四)-上手micro-shared及原理解析|8月更文挑战

1,168 阅读8分钟

引言

micro-shared以redux为核心,采用发布-订阅模式进行封装,实现应用间通信数据上的响应式,并在代码结构上实现模块化,使用上编写方式及api类似vuex,上手难度低, 并可适用多框架(如vue、react)

image.png

是的,前文中的通信模块已经正式命名为micro-shared并打包发布到npm中,这个包也算是对前三版的思路做了个总结,在结构上、具体代码上都做了优化,通过npm包的形式,可以在使用上难度更低,代码清晰。

若还未看过前三版的童鞋,可以戳这里:

⚡qiankun微前端中的应用通信-不受框架限制的响应式数据管理

⚡qiankun微前端中的应用通信(二)-可订阅指定state|8月更文挑战

⚡qiankun微前端中的应用通信(三)-结构模块化,用法简单化|8月更文挑战 (juejin.cn)

如何使用

安装

yarn add micro-shared

在项目中使用

  1. 首先在主应用里创建pool文件夹 @/pool
pool
 ├── index.ts
 └── modules
     ├── locale.ts
     └── user.ts
  1. 然后开始编写user.ts
import { Mutation } from 'micro-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由三部分组成statereduceraction

同时对外暴露namereduceraction

其中name为模块名,action是微应用唯一能直接访问的api, 而state只能由reducer改变

graph TD
action --> reducer --> state
  1. 将user模块导入,同时生成shared
// pool/index.ts
import Shared from 'micro-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传给微应用即可。

  1. 在主应用项目中,进行qiankun的微应用注册的地方
import { registerMicroApps, start } from 'qiankun';
import shared from '@/pool';

registerMicroApps([
  {
    name: 'micro',
    entry: '//localhost:8888',
    container: '#nav',
    activeRule: '/micro',
    props: {
        shared
    },
  },
]);

start();
  1. 在微应用中,接收shared实例
// @/main.ts 已隐藏无关代码
import SharedModule from '@/pool';

function render(props: any = {}) {
    const { container, shared = SharedModule.getShared() } = props;
    SharedModule.overloadShared(shared);
}
  1. 在微应用中创建 @/pool目录 (也是上一步里, import SharedModule的来源)
pool
 ├── index.ts

@/pool/index.ts也十分简单,相当于只需做模块注册即可

import SharedModule from 'micro-shared/sharedmodule';

import { Shared } from './shared';// 本地模块

SharedModule.initShared(Shared);// 加载本地模块

export default SharedModule;

第二行和第三行是用于加载本地shared实例(确保微应用独立运行时不会缺失shared),如果不考虑独立运行的话,这两行代码也可以删去。

  1. 现在你就可以在微应用自身的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: {
    },
});

  1. SharedModule.subscribe 用于注册订阅事件。通过传入回调函数进行订阅, 可以数组形式批量传入,当pool内数据有变化时(监听到redux提供的set方法执行了),会通过回调函数统一发布。
  2. 注册的订阅事件可以接收一个参数 stateName,该参数会返回当前发生改变的state, 例如此次demo的state有 user 和 locale, 当user里的userinfo发生改变时, 每个订阅事件都会获得stateName参数,告诉你user这个state发生了改变,这可以更好的帮助你决定更新哪些模块的状态
  3. 由于2实现的核心是浅比较,因此当stateName为空字符串时,可以判断出是嵌套较深的state发生了改变,这在一定程度上也可以知道到底是哪个state改变了

原理解析

在看完了如何使用后, 是不是对micro-shared这个黑盒子里到底装了什么起了兴趣?

建议结合前三版食用,可以完整体验从0到最终npm包的思路演变,相信可以对你有些许帮助:

⚡qiankun微前端中的应用通信-不受框架限制的响应式数据管理

⚡qiankun微前端中的应用通信(二)-可订阅指定state|8月更文挑战

⚡qiankun微前端中的应用通信(三)-结构模块化,用法简单化|8月更文挑战 (juejin.cn)

设计思路

image.png

  1. Shared实例基于BaseShared基类生成

    • BaseShared基类接收两个构造参数: PoolAction

    • Pool由reduxcreateStore生成,所需参数为所有module的reducer, 也就是说Pool是所有reducer的合集

    • Action负责管理所有module的action

    • BaseShared接收的PoolAction由flatModule导出

  2. flatModule负责管理所有的Module(即主应用中使用中所编写的user、locale等)

    • flatModule会将所有的Module统一拆分成reduceraction(正如你编写那些模块时暴露的reducer和action)然后分别对reducer、action进行处理,最终向外暴露为pool、actions

    • reducer 即为redux中的reducer类型,可实现对状态树的操作,并在flatModule中交由Pool模块处理(核心是由reduxcreateStore进行生成), 对reducer有疑问的,可以参考redux文档

    • action 类似vuex的action, 用于提交mutation(即交由reducer来更改状态),同时action中的api也将是暴露给使用者的接口(也就是使用过程中是无法直接操作reducer的,只能调用action),在flatModule中交由action模块进行处理

目录结构

micro-shared
 ├── base.ts
 ├── index.ts
 ├── types.ts
 ├── utils.ts
 ├── modules
 │   ├── action.ts
 │   ├── index.ts
 │   └── pool.ts
 └── sharedmodule
     └── index.ts

实际代码

  1. 我们先来看看入口文件有哪些内容
import BaseShared from './base';
import { flatModule } from './modules';

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需要的两个参数poolactions则是flatModule处理option后得来

  1. 那么接下来我们就来看看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要实现得功能是:

  1. init:订阅事件,并在state发生改变时触发事件。
  2. dispatch: 分发事件,使用户可以调用所需的action方法
// modules/index.ts
import createPool from './pool';
import createAction from './action';

export function flatModule(option: any) {
    const { modules } = option;
    const reducers: any = {}
    const actionList: Array<any> = [];
    Object.values(modules).forEach((obj: any) => {
        const { reducer, action, name } = obj;
        reducers[name] = reducer;
        actionList.push({
            name,
            actionObj: action,
        })
    })

    const pool = createPool(reducers);
    const actions = createAction(actionList, pool);
    
    return {
        pool,
        actions,
    }
}

flatModule从option中取出导入得modules,再将modules拆分为两类,reducer和action, 分别交由createPool和createAction处理并得到最终的pool,actions

  1. 说到这里,我们再来看看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

对了, combineReducerscreateStore 都来自于 redux

// action.ts
import { Pool } from '../types';

function createAction(actionList: Array<any>, pool: Pool): Map<string, unknown> {
    let actions = new Map();
    actionList.forEach((v) => {
        const { actionObj, name }= v;
        const actionHandler = installAction(actionObj, pool, name);
        Object.keys(actionHandler).forEach((key) => {
            actions.set(`${name}/${key}`, actionObj[key]);
        });
    })
    return actions;
}


export function installAction(action: Record<string, Function>, pool: Pool, moduleName: string) {
    Object.keys(action).map((key) => {
        action[key] = registerAction(pool, action[key], moduleName);
    });
    return action
}

function registerAction(pool: Pool, 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可以获得所需的上下文环境(部分)

  1. 最后再来看一下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;

export * from '../types';

SharedModule提供的功能也很简单,载入实例、获取实例、注册订阅事件。

剩下的utils.ts、types.ts就不说了,不影响整个模块的思路,仅仅只是做一些抽离而已,有兴趣可以自行查阅源码

总结

  1. 目前源码已上传至gitee(micro-shared: 一个用于微前端的通信模块 (gitee.com)),并发布到npm(micro-shared - npm (npmjs.com)
  2. 而实际使用的演示demo已经上传至gitee: 主应用微应用,关于本次的内容,两个仓库均请切换到v4分支。
  3. 觉得还不错的话,能否给个三键三连?(点赞、收藏、关注), 如果文章有问题的话,恳请评论回复我,每一次指导都是进步✨