The introduction

Micro-shared takes Redux as the core, adopts the published-subscribe mode for encapsulation, realizes the response of communication data between applications, and realizes the modularity of code structure. The writing method and API are similar to VUEX, which makes it easy to get started and can be applied to multiple frameworks (such as VUE and React).

Yes, the communication module mentioned above has been officially named micro-shared and packaged and released to NPM. This package is also a summary of the ideas of the first three versions. The structure and specific code have been optimized.

If you haven’t seen the first three versions of children’s shoes, you can poke them here:

⚡ Qiankun Application communication in micro front end – responsive data management without framework limitations

⚡ Qiankun Micro front-end application communication (ii)- can subscribe to specified state | August more article challenges

⚡ Qiankun Micro front-end application communication (3)- Modular structure, simple usage | August more text challenge (juejin.cn)

How to use

The installation

yarn add micro-shared
Copy the code

Used in projects

  1. First create the pool folder in the main application@/pool
Pool ├── index. Ts ├── modules ├── loce.ts ├─ user.tsCopy the code
  1. Then start writing user.ts
import { Mutation } from 'micro-shared';
interface UserInfo {
    username: string,}interfaceState { 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: returnuserState; }};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
};
Copy the code

You can see that the entire user consists of state, Reducer, and Action

The name, Reducer, and action are also exposed

Where name is the module name, action is the only API that can be directly accessed by micro applications, and state can only be changed by reducer

graph TD
action --> reducer --> state
  1. The user module is imported and shared is generated
// 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();
Copy the code

Now that shared has been written, you can send it to the micro app via The Props of Qiankun.

  1. In the main app project, the place to register qiankun’s micro app
import { registerMicroApps, start } from 'qiankun';
import shared from '@/pool';

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

start();
Copy the code
  1. In microapplications, receive shared instances
// @/main.ts has hidden irrelevant code
import SharedModule from '@/pool';

function render(props: any = {}) {
    const { container, shared = SharedModule.getShared() } = props;
    SharedModule.overloadShared(shared);
}
Copy the code
  1. Create the @/pool directory in the microapplication (also in the previous step,import SharedModuleThe source of the)
The pool ├ ─ ─ index. TsCopy the code

At @/pool/index.ts, it’s as simple as registering the module

import SharedModule from 'micro-shared/sharedmodule';

import { Shared } from './shared';// Local module

SharedModule.initShared(Shared);// Load the local module

export default SharedModule;
Copy the code

The second and third lines are used to load the local shared instance (to ensure that the microapplication does not lose shared when running independently) and can be removed if running independently is not considered.

  1. Now you can use it in the App’s own store
import Vue from 'vue';
import Vuex from 'vuex';
import SharedModule from '@/pool';// From the pool directory created in step 6

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: {
        / * initialize the Shared module proposals in the main. Ts, the execution of the SharedModule. OverloadShared (Shared) is executed after the initialization * /
        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: {},});Copy the code
  1. SharedModule.subscribeUsed to register subscription events. When the data in the pool changes (the set method provided by redux is executed), the data will be published through the callback function.
  2. Registered subscription events can receive a parameter stateName, which will return the current changed state. For example, the state of the demo can be user and locale. When the userInfo in the User is changed, each subscription event will receive the parameterstateNameParameter, tell youuserThis state has been changed to better help you decide which modules to update
  3. Because the core of the 2 implementation isShallow comparisonSo that whenstateNameWhen the string is empty, you can tell which deeply nested state has changed, and to some extent which state has changed

The principle of analytic

After seeing how it works, are you interested in what’s in the black box of micro-shared?

It is recommended to combine the first three versions to experience the evolution of the idea from 0 to the final NPM package. I believe it will be of some help to you:

⚡ Qiankun Application communication in micro front end – responsive data management without framework limitations

⚡ Qiankun Micro front-end application communication (ii)- can subscribe to specified state | August more article challenges

⚡ Qiankun Micro front-end application communication (3)- Modular structure, simple usage | August more text challenge (juejin.cn)

Design ideas

  1. The Shared instance is generated based on the BaseShared base class

    • The BaseShared base class receives two construction parameters: Pool and Action

    • The Pool is generated by the redux createStore. The required parameters are the reducer of all modules, that is, the Pool is the collection of all reducers

    • Action is responsible for managing actions for all modules

    • The Pool and Action received by BaseShared are exported from the flatModule

  2. The flatModule manages all the modules (users, locales, etc., written in the main application)

    • The flatModule divides all the modules into the reducer and action (just as the reducer and action were exposed when you wrote those modules) and then processes the reducer and action separately, exposing them to the pool and actions

    • The reducer is the reducer type in Redux. It can operate the state tree and is transferred to the Pool module in the flatModule (the core is generated by the Redux createStore). If you have any questions about the reducer, refer to the Redux documentation

    • Action is similar to vuex’s action, which is used to commit mutations (that is, to change state by delegating to the Reducer), and the API in the action is also the interface exposed to the user (that is, action cannot directly operate the Reducer during use, only action can be called). This is handled by the Action module in the flatModule

The directory structure

├── libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts │ ├─ libr.ts └ ─ ─ sharedmodule └ ─ ─ index. The tsCopy the code

The actual code

  1. Let’s first take a look at what the entry file contains
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() {
        returnShared.shared; }}export default Shared;

export * from './types';
Copy the code

As you can see, the main entry is to export the Shared class

The Shared class is constructed by generating Shared instances from the BaseShared base class

The two parameters that BaseShared requires, pool and actions, are taken after the flatModule handles the option

  1. So let’s look at what BaseShared is and what does the flatModule do
// base.ts
import { Store } from 'redux';
import { update } from './utils'


export default class BaseShared {
    static pool: Store;

    static actions = new Map(a);static cacheState: any = null;

    constructor(Pool: Store, action = new Map(a)) {
        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);
        returnres; }}Copy the code

You can see what BaseShared is supposed to do:

  1. init: Subscribs to events and fires them when state changes.
  2. dispatch: Distributes events that allow the user to invoke the desired action method
// 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,
    }
}

Copy the code

The flatModule takes imported modules from option and divides modules into two categories, reducer and Action, which are respectively handled by createPool and createAction to get the final pool,actions

  1. With that said, let’s take a look inside createPool and createAction
// pool.ts
import { combineReducers, createStore } from 'redux';

function createPool(reducers = {}) {
    const staticReducers = combineReducers(reducers);
    return createStore(staticReducers);
}

export default createPool;

Copy the code

What the createPool does is to combine all reducers in the reducer set with combineReducers, and then send the combined results to the createStore to generate the pool

By the way, combineReducers and createStore are both from Redux

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

function createAction(actionList: Array<any>, pool: Pool) :Map<string.unknown> {
    let actions = new Map(a); 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();
                returnstate[moduleName]; }) (),commit: pool.dispatch, }, ... param)return res
    }
}

export default createAction;
Copy the code

CreateAction mainly processes the action as a Map set and the key name in the module/method name format

RegisterAction provides a wrapper around each action so that the action can get the context it needs (in part)

  1. Finally, take a look at SharedModule
// sharedmodule/index.ts
class SharedModule {
    static shared: any;

    static listener: Array<Function> = [];

    static initShared(shared: any) {
        SharedModule.shared = shared;
    }

    /** * Overloads 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('Missing parameter');
        if(fn.length) { SharedModule.listener.push(... fn); }else{ SharedModule.listener.push(fn); }}/** * Get shared instance */
    static getShared() {
        returnSharedModule.shared; }}export default SharedModule;

export * from '.. /types';
Copy the code

The functions provided by the SharedModule are also simple: load an instance, get an instance, and register to subscribe to events.

The rest of the utils. Ts, types. Ts will not be mentioned

conclusion

  1. The source code has been uploaded to Gitee (micro-shared: a communication module for the micro front end (gitee.com)) and published to NPM (micro-shared – NPM (npmjs.com)).
  2. The actual demo has been uploaded to Gitee: Main application and Micro application. For this time, please switch to v4 branch for both warehouses.
  3. If you think that’s good, can you give me a triple bond? (like, collect, follow), if there is any problem with the article, please comment and reply to me, every guidance is progress ✨