Written in the beginning

Micro front end series articles:

  • Microfront-end Best Practices based on Qiankun – from 0 to 1
  • The Qiankun Based Micro front End best Practices (illustrated) – communication between applications
  • 10,000 words long text + illustrated + comprehensive analysis of micro front end framework

The rest of the articles in this series are scheduled to be completed in a month or two.

The plan is as follows:

  • Life cycle;
  • IE compatibility;
  • Production environment deployment;
  • Performance optimization, caching scheme;

The introduction

Hello ~

This article is part of the application to application communication series based on the Micro front end of Qiankun. This article will share how to communicate between applications in Qiankun.

Before we start introducing The application communications of Qiankun, we need to understand how micro-front-end architectures can be used for molecular applications.

In a micro front-end architecture, we should divide sub-applications by business rather than molecular applications by functional modules. There are two reasons for this:

  1. In the micro front-end architecture, the sub-application is not a module, but an independent application, we divide the sub-application by business can have better maintainability and decoupling.
  2. Sub-applications should be able to run independently. Frequent communication between applications increases the complexity and coupling degree of applications.

To sum up, we should divide the sub-applications from a business perspective and minimize the communication between the applications, so as to simplify the whole application and make our micro front-end architecture more flexible and controllable.

In this tutorial, we’ll look at two ways to communicate,

  1. The first is aqiankunOfficial means of communication –ActionsCommunication, suitable for clear business division, relatively simple micro front-end applications, generally use the first solution can meet the requirements of most application scenarios.
  2. The second is based onreduxImplemented communication mode –SharedCommunication, suitable for the need to track the communication state, sub-applications with independent operation capability, more complex micro front-end applications.

Actionscommunication

We first introduce the official inter-application communication method, Actions, which is suitable for micro front-end application scenarios where services are clearly divided and there is little inter-application communication.

Communication principle

The initGlobalState method is used to register a MicroAppStateActions instance for communication. The instance has three methods:

  • setGlobalStateSet:globalState– Set new values when the internal will be executedShallow checkIf checkedglobalStateA change triggers a notification to allThe observerFunction.
  • onGlobalStateChangeRegister:The observerFunction responseglobalStateChanges inglobalStateTriggers when a change occursThe observerFunction.
  • offGlobalStateChangeCancelled:The observerFunction – This instance is no longer respondingglobalStateChange.

Let’s draw a picture to help you understand (see below)

As we can see from the figure above, we can register observers into the observer pool and then modify globalState to trigger all observer functions to achieve the effect of communication between components.

Actual combat tutorial

We take the actual case – feature-communication branch (the case is Vue as the base of the main application, access React and Vue two sub-applications) as an example, to introduce how to use Qiankun to complete the communication function between applications.

Clone practical case – feature-communication branch branch code to the local, run the project to see the actual effect.

The main application works

First, we register a MicroAppStateActions instance in the main application and export it. The code implementation is as follows:

// micro-app-main/src/shared/actions.ts
import { initGlobalState, MicroAppStateActions } from "qiankun";

const initialState = {};
const actions: MicroAppStateActions = initGlobalState(initialState);

export default actions;
Copy the code

After registering the MicroAppStateActions instance, we use it in the components we want to communicate with and register the observer function. Here, we use the login function as an example.

// micro-app-main/src/pages/login/index.vue
import actions from "@/shared/actions";
import { ApiLoginQuickly } from "@/apis";

@Component
export default class Login extendsVue { $router! : VueRouter;// 'mounted' is the Vue lifecycle hook function that executes when the component is mounted
  mounted() {
    // Register an observer function
    actions.onGlobalStateChange((state, prevState) = > {
      // state: indicates the state after the change; PrevState: Indicates the status before the change
      console.log("Main application observer: the value before token change is", prevState.token);
      console.log("Primary application Observer: The login status has changed, and the changed token value is", state.token);
    });
  }
  
  async login() {
    // ApiLoginQuickly is a remote login function used to obtain tokens. See Demo for details
    const result = await ApiLoginQuickly();
    const { token } = result.data.loginQuickly;

    // After the login, set the tokenactions.setGlobalState({ token }); }}Copy the code

In the above code, we register an observer function in the Vue component’s Mounted lifecycle hook function, then define a login method, and bind the login method to the button shown below.

At this point, we click the button twice, which will trigger the observer function we set in the main application (as shown below).

From the picture above we can see that:

  • First click: originaltokenA value ofundefinedThe newtokenThe value is the latest value we set;
  • Second click: originaltokenThe value of is the value we set last time, newtokenThe value is the latest value we set;

As you can see from the above, our globalState update succeeded!

Finally, we add a line of code at the end of the login method that lets us jump to the home page after logging in. The code is as follows:

async login() {
  / /...

  this.$router.push("/");
}
Copy the code

The work of the sub-application

We have completed the login function of the main application and recorded the token information in globalState. Now, we go to the child application and use the token to get the user information and display it in the page.

Let’s first change our Vue subapplication. First we set up an Actions instance, which is implemented as follows:

// micro-app-vue/src/shared/actions.js
function emptyAction() {
  // Warning: an empty Action is being used
  console.warn("Current execute action is empty!");
}

class Actions {
  // The default value is empty Action
  actions = {
    onGlobalStateChange: emptyAction,
    setGlobalState: emptyAction
  };
  
  /** * Set actions */
  setActions(actions) {
    this.actions = actions;
  }

  /** * Mapping */onGlobalStateChange(... args) {return this.actions.onGlobalStateChange(... args); }/** * Mapping */setGlobalState(... args) {return this.actions.setGlobalState(...args);
  }
}

const actions = new Actions();
export default actions;
Copy the code

After we create the Actions instance, we need to inject real Actions into it. We inject the render function into the entry file main.js, and the code is as follows:

// micro-app-vue/src/main.js
/ /...

/** * Render function * run in the main application lifecycle hook/run when the child application starts separately */
function render(props) {
  if (props) {
    // Inject the Actions instance
    actions.setActions(props);
  }

  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/".mode: "history",
    routes,
  });

  // Mount the application
  instance = new Vue({
    router,
    render: (h) = > h(App),
  }).$mount("#app");
}
Copy the code

As you can see from the code above, the render method is called when the child application is mounted. We can inject the actions instance of the main application into the Render method.

Finally, we obtain the globalState token in the communication page of the child application, use the token to obtain the user information, and finally display the user information in the page. Code implementation is as follows:

// micro-app-vue/src/pages/communication/index.vue
// Import the Actions instance
import actions from "@/shared/actions";
import { ApiGetUserInfo } from "@/apis";

export default {
  name: "Communication",

  data() {
    return {
      userInfo: {}}; }, mounted() {// Register the observer function
    // onGlobalStateChange the second argument is true, indicating that the observer function is executed immediately
    actions.onGlobalStateChange(state= > {
      const { token } = state;
      // Not logged in - Return to the home page
      if(! token) {this.$message.error("No login information detected!");
        return this.$router.push("/");
      }

      // Obtain user information
      this.getUserInfo(token);
    }, true);
  },

  methods: {
    async getUserInfo(token) {
      // ApiGetUserInfo is the function used to get user information
      const result = await ApiGetUserInfo(token);
      this.userInfo = result.data.getUserInfo; }}};Copy the code

As you can see from the code above, we register an observer function when the component is mounted and execute it immediately, get the token from globalState/ State, then use the token to get the user information, and finally render it in the page.

Finally, let’s look at it in action. We click the Login button from the Login page, through the menu to enter the Vue communication page, you can see the effect! (See picture below)

The React application is implemented similarly. The implementation code can refer to the full demo-feature-communication branch. The implementation effect is as follows (see the figure below).

summary

At this point, Qiankun basic communication is complete!

We have implemented the login function in the main application. After the login, the token is stored in the globalState state pool. When entering the sub-application, we use actions to get the token, and then use the token to get the user information, and complete the page data rendering!

Finally, let’s draw a picture to help you understand the process (see figure below).

Sharedcommunication

Since Shared solutions can be more complex to implement, the Actions solution can be better officially supported when the Actions solution meets the requirements.

The official Actions communication scheme uses the global state pool and observer function to communicate between applications. This communication mode is suitable for most scenarios.

The Actions communication scheme also has some advantages and disadvantages, such as:

  1. Simple to use;
  2. High official support;
  3. Suitable for business scenarios with less communication;

The disadvantages are as follows:

  1. If the sub-applications are running independently, you need to configure additional configurations. NoneActionsThe logic of time;
  2. Sub-applications need to understand the details of the state pool before communicating;
  3. Because the state pool cannot be tracked, there are many communication scenarios, which may cause problems such as state confusion and difficult maintenance.

If your application has multiple communication scenarios, and you want your subapplications to be able to run independently, and your main application to be able to manage your subapplications better, then you can consider the Shared solution.

Communication principle

The principle of the Shared communication scheme is that the primary application maintains a pool of state based on the Redux, and the Shared instance exposes some methods to the child application. At the same time, the child application needs to maintain a separate shared instance, use its own shared instance when running independently, and use the master application’s shared instance when embedded with the master application, so as to ensure consistency in usage and performance.

The Shared communication solution requires a self-maintained state pool, which adds to the complexity of the project. The benefit is that you can use the more mature state management tools on the market, such as Redux, Mobx, better state management tracking, and some toolsets.

Shared communication requires both parent and child applications to maintain their own Shared instances, which also increases project complexity. The advantage is that the child application can run completely independently of the parent application (not dependent on the state pool), and the child application can be embedded into other third-party applications with minimal change.

Shared communication solutions can also help master applications better manage sub-applications. The child application can only operate the state pool through the shared instance, which avoids a series of problems caused by the child application’s arbitrary operation of the state pool. The host application’s Shared application is a black box compared to the child application. The child application only needs to know the API exposed by the Shared application and not the implementation details.

Actual combat tutorial

Let’s use the feature-communication-shared branch login process as an example to show how shared can be used for inter-application communication.

The main application works

First we need to create a store in the main application to manage the global state pool. Here we use redux to do this. The code is as follows:

// micro-app-main/src/shared/store.ts
import { createStore } from "redux";

export typeState = { token? :string;
};

type Action = {
  type: string;
  payload: any;
};

const reducer = (state: State = {}, action: Action): State= > {
  switch (action.type) {
    default:
      return state;
    / / set the Token
    case "SET_TOKEN":
      return{... state, token: action.payload, }; }};const store = createStore<State, Action, unknown, unknown>(reducer);

export default store;
Copy the code

As you can see, we use Redux to create a global state pool and set a Reducer to modify the token value. Next we need to implement the shared instance of the main application. The code implementation is as follows:

// micro-app-main/src/shared/index.ts
import store from "./store";

class Shared {
  /** * Obtain Token */
  public getToken(): string {
    const state = store.getState();
    return state.token || "";
  }

  /** * Set Token */
  public setToken(token: string) :void {
    // Record the token value in the store
    store.dispatch({
      type: "SET_TOKEN", payload: token }); }}const shared = new Shared();
export default shared;
Copy the code

As you can see from the above implementation, our shared implementation is very simple. Our shared instance includes two methods getToken and setToken, which are used to get a token and set a token, respectively. Next, we need to modify our login component. Modify the login method as follows:

// micro-app-main/src/pages/login/index.vue
// ...
async login() {
  // ApiLoginQuickly is a remote login function used to obtain tokens. See Demo for details
  const result = await ApiLoginQuickly();
  const { token } = result.data.loginQuickly;

  // Use shared's setToken method to record the token
  shared.setToken(token);
  this.$router.push("/");
}
Copy the code

As can be seen from the above, after a successful login, we will record the token in the store through the shared.setToken method.

Finally, we need to pass the shared instance to the child application using props.

// micro-app-main/src/micro/apps.ts
import shared from "@/shared";

const apps = [
  {
    name: "ReactMicroApp",
    entry: "//localhost:10100",
    container: "#frame",
    activeRule: "/react".// Use props to pass shared to the child application
    props: { shared },
  },
  {
    name: "VueMicroApp",
    entry: "//localhost:10200",
    container: "#frame",
    activeRule: "/vue".// Use props to pass shared to the child application
    props: { shared },
  },
];

export default apps;
Copy the code

The work of the sub-application

Now, let’s deal with the work that needs to be done on the sub-application. As we mentioned earlier, we want our child applications to be able to run independently, so the child applications should also be shared so that they have compatible processing power when running independently. Code implementation is as follows:

// micro-app-vue/src/shared/index.js
class Shared {
  /** * Obtain Token */
  getToken() {
    // When the child application runs independently, it obtains the token from localStorage
    return localStorage.getItem("token") | |"";
  }

  /** * Set Token */
  setToken(token) {
    // When the child application runs independently, set the token in localStorage
    localStorage.setItem("token", token); }}class SharedModule {
  static shared = new Shared();

  /** * Overloads shared */
  static overloadShared(shared) {
    SharedModule.shared = shared;
  }

  /** * Get shared instance */
  static getShared() {
    returnSharedModule.shared; }}export default SharedModule;
Copy the code

From above we can see two classes, let’s examine their usefulness:

  • Shared: of the subapplication itselfsharedWhich will be used when the child application runs independentlyshared, subapplicationshareduselocalStorageTo operatetoken;
  • SharedModule: For managementshared, such as overloadingsharedInstance, ObtainsharedExamples and so on;

After we implement shared for the child application, we need to inject shared in the entry file as follows:

// micro-app-vue/src/main.js
/ /...

/** * Render function * run in the main application lifecycle hook/run when the child application starts separately */
function render(props = {}) {
  // Use the child application's own shared when the passed shared is empty
  // If the shared value passed to the primary application is not null, the shared value passed to the primary application overrides the shared value of the child application
  const { shared = SharedModule.getShared() } = props;
  SharedModule.overloadShared(shared);

  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/".mode: "history",
    routes,
  });

  // Mount the application
  instance = new Vue({
    router,
    render: (h) = > h(App),
  }).$mount("#app");
}
Copy the code

As you can see above, if the props shared field is not empty, we will use the passed shared field to override the child application’s own shared field. By doing so, the shared of the primary application and the shared of the child application behave the same when used.

Then we modify the communication page of the child application to obtain the token using the shared instance. The code implementation is as follows:

// micro-app-vue/src/pages/communication/index.vue
/ / introduce SharedModule
import SharedModule from "@/shared";
import { ApiGetUserInfo } from "@/apis";

export default {
  name: "Communication",

  data() {
    return {
      userInfo: {}}; }, mounted() {const shared = SharedModule.getShared();
    // Use shared to obtain the token
    const token = shared.getToken();

    // Not logged in - Return to the home page
    if(! token) {this.$message.error("No login information detected!");
      return this.$router.push("/");
    }

    this.getUserInfo(token);
  },

  methods: {
    async getUserInfo(token) {
      // ApiGetUserInfo is the function used to get user information
      const result = await ApiGetUserInfo(token);
      this.userInfo = result.data.getUserInfo; }}};Copy the code

Finally, let’s go to the page and see how it performs when running in the main application and when running independently. (See picture below)

As you can see in Figure 1, when we run a child application in the primary application, the shared instance is overloaded by the primary application. After logging in, the shared instance can obtain the token in the status pool and successfully obtain the user information using the token.

As can be seen from Figure 2, when we run the child application independently, the shared instance is the shared instance of the child application itself. The token cannot be obtained in the localStorage and is intercepted and returned to the home page.

In this way, our Shared communication is complete!

summary

From the above examples, we can also see the advantages and disadvantages of Shared communication scheme. Here are some simple analysis:

The advantages are:

  • Can freely choose the state management library, better development experience. – such asreduxSpecial development tools are available to track state changes.
  • The child application does not need to know the details of the state pool implementation of the primary applicationsharedThe function of the abstract, implement a set of its ownsharedEven an emptysharedCan, can better specification sub-application development.
  • Sub-applications cannot contaminate the primary application’s state pool at will. They can only be exposed by the primary applicationsharedInstance specific methods operate on the state pool, thereby avoiding the problem of state pool contamination.
  • Sub-applications will be able to run independently,SharedCommunication allows for better decoupling of father – son applications.

There are also two disadvantages:

  • The main application needs to maintain a separate set of state pools, which increases maintenance costs and project complexity.
  • Sub-applications need to be maintained separatelysharedInstance, which increases maintenance costs;

Shared communication has its pros and cons. Higher maintenance costs increase the robustness and maintainability of applications.

Finally, let’s draw a picture to analyze the principle and process of shared communication (see the figure below).

conclusion

Here, the two communication solutions between Qiankun apps are shared!

Both communication schemes have appropriate usage scenarios, and you can choose according to your own needs.

One last thing

If you have already seen this, I hope you still click “like” before you go ~

Your “like” is the greatest encouragement to the author, and can also let more people see this article!

If you found this article helpful, please help to light up the Star on Github to encourage!