The introduction
The solution takes Redux as the core and adopts published-subscribe mode for encapsulation to realize the responsiveness of inter-application communication data. It also realizes modularization in code structure, imitates VUEX in API to reduce the difficulty of getting started, and can be applied to multiple frameworks (such as VUE and React).
This revision will be based on the previous two versions of the communication module completed on the structure of the revision, the implementation of the communication module highly abstract, the actual development of business code and communication module code highly decouple, more concise API, clearer development process, lower difficulty to get started.
If you haven’t seen the first two versions of children’s shoes, you can poke them here:
⚡ Application Communication in Microfront-end – Responsive data management without framework constraints
⚡ Qiankun Micro Front End in Application communications (II)- subscribe to specified State | August More text Challenge
How to use
Yes, this time we don’t talk about design, implementation and use, but show you how to use it.
Because this version of the core, not to achieve each specific functional code, but the restructuring of the structure, optimize the use.
This time we abstract the entire shared module (communication module) and place it in the @/utils/shared directory (for both main and micro apps), so the next code presentation will probably have a lot of import XXX from ‘@/utils/shared’. Hopefully this won’t bother you.
- First create the Pool folder in the main application
@/pool
├── all exercises, ├─ all exercises, all exercises, all exercisesCopy the code
- Then start writing user.ts
import { Mutation } from '@/utils/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
It can be seen that the whole user consists of state, Reducer and action
Meanwhile, name, Reducer and Action were exposed externally
Where, name is the module name, action is the only API that can be directly accessed by micro-application, and state can only be changed by Reducer
graph TD
action --> reducer --> state
- Import the user module and generate 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();
Copy the code
Shared was now written and passed to the micro application via the Props of qiankun.
Do you feel that this time, the writing is very concise, the code structure is clear?
Because although the core is redux, I wrote it like vuex🤣
- In the main application project, the place for qiankun’s microapplication registration
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
- In microapplications, shared instances are received
// @/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
- Create the @/pool directory in the microapplication (also in the previous step,
import SharedModule
The source of the)
The pool ├ ─ ─ index. TsCopy the code
@/pool/index.ts is also very simple, equivalent to a module registration
import SharedModule from '@/utils/shared/sharedmodule';
import { Shared } from './shared';// Local module
SharedModule.initShared(Shared);// Local module
export default SharedModule;
Copy the code
‘@/utils/shared/sharedmodule’; Like the main application, the shared module’s code is abstracted
The second and third lines are used to load the local shared instance (to ensure that the shared is not missing when the microapplication runs independently), and can be deleted if running independently is not considered.
- Now you can use it in the microapp’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
SharedModule.subscribe
Used to register subscription events. Subscribe by passing in a callback function, which can be passed in as an array in bulk. When the pool data changes (when redux’s set method executes), it will be published through the callback.- A registered subscription event can receive a parameter stateName, which will return the current changed state. For example, the demo state has user and locale. When userInfo in user changes, each subscription event will receive the parameter
stateName
Parameters, tell youuser
The state has been changed to help you decide which modules to update - Since the core of the 2 implementation is
Shallow comparison
So that whenstateName
When the string is empty, it can be determined that the deeply nested state has changed, which can also be known to some extent which state has changed
As you can see, this release abstracts the shared module significantly, making it much simpler to use.
implementation
After seeing how it works, are you interested in what’s in the @/utils/shared black box?
We’re going to unveil it now
Design ideas
-
Shared instances are generated based on the BaseShared base class
-
The BaseShared base class takes two construction parameters: Pool and Action
-
The Pool is generated by redux’s createStore. The required parameters are the reducer of all modules, that is, the Pool is a set of all reducer
-
Action manages the actions for all modules
-
The Pool and Action received by BaseShared are exported by the flatModule
-
-
FlatModule is responsible for managing all modules (that is, the users, locales, and so on that are written for use in the main application)
-
The flatModule will divide all modules into reducer and Actions (just as you exposed reducer and actions when you compile those modules) and then deal with reducer and actions respectively, and finally expose them to pool and actions
-
Reducer is the Reducer type in REdux, which can operate the state tree and submit the reducer to the Pool module in the flatModule (the core is generated by The createStore of Redux). If you have any doubts about reducer, please refer to the Redux document
-
Actions are similar to vuEX actions and are used to submit mutation(that is, the state is changed by reducer). Meanwhile, the apis in the actions will also be exposed to the user (that is, the reducer cannot be directly operated during use, only actions can be called). This is handled by the Action module in the flatModule
-
The directory structure
@/utils/shared
Shared ├─ Action.ts To handle the action and return actions to the flatModule ├─ BaseShared class. ├─ index. Ts import file ├─ module.ts - flatModule to deal with the core of the shared module ├─ class ├─ class ├─ classes, exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercisesCopy the code
The actual code
- Let’s take a look at the contents of the entry file
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() {
returnShared.shared; }}export default Shared;
export * from './types';
Copy the code
As you can see, the main entry is to export the Shared class
What the Shared class does when constructed is generate a Shared instance from the BaseShared base class
The BaseShared parameters pool and Actions are derived from the flatModule’s options
- So let’s see what BaseShared is and what flatModule does
// 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 does:
init
: subscribes to events and fires when state changes.dispatch
: distributes the event so that the user can invoke the desired action method
// 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(a); actionList.forEach((v) = > {
const k = createAction(installAction(v.action, pool, v.name), v.name);
actions = new Map([...actions, ...k]);
})
return {
pool,
actions,
}
}
Copy the code
The flatModule extracted the imported modules from option and split the modules into two classes (Reducer and Action), which were processed by createPool and createAction respectively to obtain the final pool and actions
- 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 createPool needs to do is to merge all the reducer in the Reducer set into the combineReducers and then send the merged result to createStore to generate a pool
By the way, combineReducers and createStore are both from Redux
// action.ts
import { Store } from './types';
function createAction(action: Record<string.Function>, name: string) :Map<string.unknown> {
const actions = new Map(a);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();
returnstate[moduleName]; }) (),commit: pool.dispatch, }, ... param)return res
}
}
export default createAction;
Copy the code
CreateAction handles the action as a Map and the key names as module/method names
RegisterAction provides a wrapper around each action so that the action can obtain the required context (part)
- Finally, take a look at the SharedModule
// sharedmodule/index.ts
class SharedModule {
static shared: any;
static listener: Array<Function> = [];
static initShared(shared: any) {
SharedModule.shared = shared;
}
/** * reload 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 arguments');
if(fn.length) { SharedModule.listener.push(... fn); }else{ SharedModule.listener.push(fn); }}/** * Get the shared instance */
static getShared() {
returnSharedModule.shared; }}export default SharedModule;
Copy the code
SharedModule also provides simple functions such as loading instances, fetching instances, and registering subscribed events.
Utils. Ts and types. Ts are not mentioned
Summary & Todo
- Now that it’s fairly straightforward to use, the next thing to do is to continue to optimize the internal implementation code and consider whether to upload to NPM
- At present, demo has been uploaded to Gitee: Main application and Micro application. For this content, please switch to V3 branch for both warehouses.
- If you think it’s good, can you give me a triple button? (like, favorites, follow), if there is a problem with the article, please comment back to me, every guidance is progress ✨