Qiankun

Qiankun is a solution to achieve a micro front end (based on single-SPA). Today we mainly look at the use of the communication and source code analysis of Qiankun can make us understand more deeply

Use of communication mode in Qiankun

  • Usage defines the global state and returns the communication method, which is recommended for primary applications. Microapplications obtain the communication method from props.

  • Examples (from official website) main application:

    import { initGlobalState, MicroAppStateActions } from 'qiankun';
    // Initialize state
    const actions: MicroAppStateActions = initGlobalState(state);
    
    actions.onGlobalStateChange((state, prev) = > {
      // state: state after the change; Prev Status before change
      console.log(state, prev);
    });
    actions.setGlobalState(state);
    actions.offGlobalStateChange();
    Copy the code

    Application:

    // Get the communication method from the lifecycle mount. Use the same method as master
    export function mount(props) {
    
      props.onGlobalStateChange((state, prev) = > {
        // state: state after the change; Prev Status before change
        console.log(state, prev);
      });
    
      props.setGlobalState(state);
    }
    Copy the code

Source code analysis of Communication in Qiankun

  • The communication mode of Qiankun is a typical subscription publishing design mode
  import { cloneDeep } from 'lodash';
  
  let globalState: Record<string.any> = {};
  const deps: Record<string, OnGlobalStateChangeCallback> = {};

  function initGlobalState(state: Record<string.any> = {}) {
       if (state === globalState) {
           console.warn([Qiankun] State has not changed! ');
       } else {
           const prevGlobalState = cloneDeep(globalState);
           globalState = cloneDeep(state);
           emitGlobal(globalState, prevGlobalState);
       }
       return getMicroAppStateActions(`global-The ${+new Date()}`.true);
   }
Copy the code

Initialization globalState initialization is an empty object. Deps initialization is an empty object to hold the subscriber. We pass the initialization object (state) to initGlobalState using LoDash to clone the parameters we passed here Create a new ID based on the timestamp and we return the result of the getMicroAppStateActions function

  export function getMicroAppStateActions(id: string, isMaster? :boolean) :MicroAppStateActions {
    return {
        onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately? :boolean) {
            if(! (callbackinstanceof Function)) {
                console.error('[qiankun] callback must be function! ');
                return;
            }
            if (deps[id]) {
                console.warn(`[qiankun] '${id}' global listener already exists before this, new listener will overwrite it.`);
            }
            deps[id] = callback;
            const cloneState = cloneDeep(globalState);
            if(fireImmediately) { callback(cloneState, cloneState); }},/** * setGlobalState updates store data ** 1. Validates the layer 1 properties of the input state. Only the bucket properties declared at initialization will be changed * 2. Modify store and trigger global listening * *@param state* /
        setGlobalState(state: Record<string.any> = {}) {
            if (state === globalState) {
                console.warn([Qiankun] State has not changed! ');
                return false;
            }

            const changeKeys: string[] = [];
            const prevGlobalState = cloneDeep(globalState);
            globalState = cloneDeep(
                Object.keys(state).reduce((_globalState, changeKey) = > {
                    if (isMaster || _globalState.hasOwnProperty(changeKey)) {
                        changeKeys.push(changeKey);
                        return Object.assign(_globalState, { [changeKey]: state[changeKey] });
                    }
                    console.warn(`[qiankun] '${changeKey}' not declared when init state!`);
                    return _globalState;
                }, globalState),
            );
            if (changeKeys.length === 0) {
                console.warn([Qiankun] State has not changed! ');
                return false;
            }
            emitGlobal(globalState, prevGlobalState);
            return true;
        },

        // Unregister the application dependencies
        offGlobalStateChange() {
        delete deps[id];
        return true; }}; }Copy the code

OnGlobalStateChange two arguments one is the callback callback and one is fireImmediately whether or not to execute the callback directly and we determine if it’s a function and if it’s an existing subscriber and we put the subscriber in deps if there’s a new subscriber Subscribers with the same ID will be overwritten

The setGlobalState argument is state

  1. The first level of validation is done for state and only properties that are initialized are allowed to be modified
 setGlobalState(state: Record<string.any> = {}) {
            if (state === globalState) {
                console.warn([Qiankun] State has not changed! ');
                return false;
            }

            const changeKeys: string[] = [];
            const prevGlobalState = cloneDeep(globalState);
            globalState = cloneDeep(
                Object.keys(state).reduce((_globalState, changeKey) = > {
                    if (isMaster || _globalState.hasOwnProperty(changeKey)) {
                        changeKeys.push(changeKey);
                        return Object.assign(_globalState, { [changeKey]: state[changeKey] });
                    }
                    console.warn(`[qiankun] '${changeKey}' not declared when init state!`);
                    return _globalState;
                }, globalState),
            );
            if (changeKeys.length === 0) {
                console.warn([Qiankun] State has not changed! ');
                return false;
            }
            emitGlobal(globalState, prevGlobalState);
            return true;
        },
Copy the code

We made a judgment it is the main application of isMaster (we wear the second parameter) when initGlobalState _globalState. HasOwnProperty (changeKey) to judge the incoming parameters If the initialization statement attributes If it wasn’t for the console warning, And it doesn’t say, ok, push it into changeKeys and change _globalState we determine the length of the changeKeys if the length is greater than 1 we go emitGlobal(globalState, prevGlobalState); And returns true

emitGlobal

function emitGlobal(state: Record<string.any>, prevState: Record<string.any>) {
  Object.keys(deps).forEach((id: string) = > {
    if (deps[id] instanceof Function) { deps[id](cloneDeep(state), cloneDeep(prevState)); }}); }Copy the code

EmitGlobal we fire the subscriber one by one based on the state and preState that’s passed in

offGlobalStateChange() {
    delete deps[id];
    return true;
},
Copy the code

OffGlobalStateChange Unloads the subscriber’s hook

How did we pass props to the sub-application in Qiankun

// unique id in the universe
const appInstanceId = `${appName}_The ${+new Date()}_The ${Math.floor(Math.random() * 1000)}`;

   Call getMicroAppStateActions to return onGlobalStateChange setGlobalState offGlobalStateChange
  const {
    onGlobalStateChange,
    setGlobalState,
    offGlobalStateChange,
  }: Record<string.Function> = getMicroAppStateActions(appInstanceId);

   // Pass the Qiankun generation lifecycle to Single-SPA
  asyncprops => mount({ ... props,container: containerGetter(), setGlobalState, onGlobalStateChange }),

   // In single-spa, use getProps to get the custom parameter customProps passed to our child application
  app.loadApp(getProps(app));
  const result = assign({}, customProps, {
    name,
    mountParcel: mountParcel.bind(appOrParcel),
    singleSpa,
  });
Copy the code

At the end

Qiankun is an excellent JS library, which is relatively easy for developers to access. Its JS sandbox and CSS sandbox are cleverly designed. We will read it next time