preface

As our business grew, our systems grew larger, challenging build speed, static resource size, and application performance

A system is composed of a large number of small modules, and most users do not have access to all modules. Therefore, our first optimization method is code split, which divides the codes of each small module and loads them as needed, which has achieved certain effects

However, as the number of systems increases and users begin to complain that there are too many portals and want a unified gateway to complete all functions, there are several solutions to this scenario

  • Consolidate all systems into one large system

    • advantages
      • The user experience can be the best, and the operation of a single page application is relatively smooth
    • defects
      • Easy to become a monolith application, development, build and performance issues
      • Any change to a small module could render the entire system unusable
      • Limited development framework, difficult to upgrade in the future
  • Build an application framework and embed IFrame into the target system

    • advantages
      • The transformation cost is low, only need to develop the application framework
      • Multiple systems can be opened at the same time and switched through labels
      • Switching between systems naturally preserves the state of the page, allowing the user to continue on the previous path
      • Each application is deployed independently and does not interfere with each other
    • defects
      • The route changes in IFrame cannot be reflected in the URL of the application framework, and the user will return to the initial page once refreshing, affecting the experience. Therefore, it is necessary to independently develop a communication mechanism for the application framework to save the route of the system in IFrame, and the existing system needs to be reformed
      • IFrame loading is slow
      • If there are too many IFrames on the interface, the DOM structure becomes complicated and system performance deteriorates
  • Develop a unified navigation bar, replace the navigation bar of each system, in the navigation bar through the label to achieve system switch

    • advantages
      • The cost of transformation is relatively low, so it is necessary to develop a navigation bar that can be quickly integrated into different systems. If a unified domain name is required, then the systems need to be modified and all requests must carry unique subpaths
      • It basically does not affect users’ experience of using a single system
    • defects
      • Switching between systems essentially opens up a new system, loading performance affects the user experience, and the user justlookLike using a system, if the user switches more frequently, the experience is more intense
      • Communication between systems can only be relied uponlocalStorage/sessionStorageWait for browser storage
      • Multiple systems cannot be opened at the same time, and the page status cannot be restored naturally
  • Using the micro front-end architecture, the application is reformed

    • advantages
      • It’s really possible to use all the functionality in one portal
      • The switching experience between different applications is good, except that the first switching takes some time to do JS parsing, and the subsequent switching is relatively smooth
      • The main application can provide common functionality for sub-applications to use
      • Different applications can be developed by different teams using different technology stacks
    • defects
      • There is a certain amount of transformation work
      • The main application carries all the flow inlets, which increases the system pressure virtually

Let’s take a look at the status quo

  • All systems are developed based on an internal unified framework, with a unified style of top bar and sidebar

  • All systems have their own Nodejs layer for page rendering and API request forwarding

  • All systems have different domain names and there is no specific domain name subpath

  • Different systems have their own small teams working on them, some using different versions of React and Ant Design

  • Development generally requires that future new functional modules can be developed using up-to-date technology

Based on the analysis of the current situation, the micro front end is a possible direction to try, so we began to step on the road of pit, transform the existing system into a sub-application of the micro front end

In order to unify the language, existing systems are referred to as sub-applications below

Hit the pit

The selection

We used Qiankun as an implementation library for the micro front end, which (supposedly) could be quickly retrofitted

Application of modified

Add subpaths

The sub-app loading mechanism of Qiankun is based on single-SPA encapsulation. The sub-app loading mechanism is realized based on browser URL, and the first sub-path is used to decide which sub-app to load, for example

  • ${your domain}/appA/…… : loads application A
  • ${your domain name}/appB/…… : loads application B

So, adapting each subapplication to add subpaths to all accesses is the first step we need to take

  • Adding a prefix to each route is shown in the following code example for KOA

    // Access the root path directly and add route prefixes for forwarding
    router.get('/', controller.redirect);
    
    // Render page, where authMiddleware is the validation middleware that implements login validation logic
    router.get('/appA/*', authMiddleware, controller.index);
    
    ${subapp name} + '_apis' = ${subapp name} + '_apis'
    // It is convenient to configure forwarding (shared domain name) in the main application.
    router.use('/appA_apis/*', authMiddleware, controller.transfer);
    
    // Ignore the rest of the routes.Copy the code
  • Modify each API request on the page to match ${child app name} + ‘_apis’

    This step is a bit of a hassle. Existing sub-apps prefix their page code with /apis, and if they are not handled in a unified place, it can be very difficult to change. Based on the status quo, we used a trick: we intercepted all Ajax requests and changed their prefixes as needed. The specific code is as follows

    ((a)= > {
      if(! XMLHttpRequest.prototype['nativeOpen']) {
        XMLHttpRequest.prototype['nativeOpen'] = XMLHttpRequest.prototype.open;
        const customizeOpen = function(method, url, ... params) {
          if (
            // Requests that do not need to change the prefix can be extracted separately if there are too many cases
            url.indexOf('hot-update.json') < 0
          ) {
            // Convert the /apis prefix to the /appA_apis prefix, which is in the framework
            RouterPrefix is injected into the window object
            url = `${window['routerPrefix']}_${url.slice(1)}`;
          }
          this.nativeOpen(method, url, ... params); }; XMLHttpRequest.prototype.open = customizeOpen; }}) ();Copy the code
  • Change the path of static files by changing the original /statics path to match ${subapp} + ‘_statics’

    This step is roughly the configuration of Webpack, mainly to modify the output and publicPath related configuration, according to the actual project operation, not described here

After the above steps, the subapplication can support the access of the subpath, but there is a key step missing, it does not affect your transformation, but will affect the normal access of users after your transformation. For example, if a user saves the address of a page on your system in a favorites folder, such as xxx.site.com/pages/user, this will result in a 404 for the user if you deploy it, so you need to make compatibility in the routing file

// redirectToNewPrefix is easy to implement by fetching ctx. URL and replacing the original route prefix
router.get('/pages/*', controller.redirectToNewPrefix);
Copy the code

At this point, we are done adding the route prefix for the child application.

Adding a main application

Refer to the official website to build the simplest main application, only need to have a node for mounting child applications

<div id="subViewport"></div>
Copy the code

Then call the registerMicroApps method to register for immediate application

registerMicroApps(
  [
    {
      name: 'appA',
      entry: appAEntryMap[process.env.NODE_ENV], / / according to the running environment, corresponding to the entrance of the load application, such as' http://localhost:3000/appA '
      container: '#subViewport',
      activeRule: '/appA'
    },
    {
      name: 'appB'.// app name registered
      entry: appBEntryMap[process.env.NODE_ENV],
      container: '#subViewport',
      activeRule: '/appB'}]); setDefaultMountApp('/appA'); // Set the application to be loaded by default

start();
Copy the code

The #subViewport child does not fill the container. Check the official issue. A solution is to use CSS to control the child div rendered under this node to fill the container (the div will inject hash, Can not be handled by id or class)

#subViewport { width: 100%; height: 100%; > div { width: 100%; height: 100%; }}Copy the code

Subapplication exposure lifecycle functions, packaged in UMD format

For details, refer to the official documentation

In addition, if you want the child application to be individually accessible, you can add code to the entry JS

// Not directly render while loading in the Qiankun frame
if (!window['__POWERED_BY_QIANKUN__']) {
  bootstrap().then(mount);
}
Copy the code

Cross-domain problem

Start the main application, visit the page, find a blank, check the console, there is a cross-domain problem

Access to fetch at 'http://localhost:3000/appA' from origin 'http://localhost:4001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Copy the code

Qiankun uses FETCH to fetch the HTML files of its sub-apps, so there are cross-domain problems. It is also relatively easy to handle, since it is rendered by Nodejs itself, just need to add koA2-CORS middleware to solve the problem

Note that if you are in development mode, you need webpack-dev-server to support cross-domain as well, see this article

Request forwarding and API validation issues

Finally, we come to a very critical part, API request processing, which is not mentioned in the official website and Demo, but it is the most important part, which determines whether your micro front-end transformation is successful

If the main application, child application, and back-end API are all the same domain name, then naturally this problem is not solved

The following solutions are based on a general premise: the main application and child application have their own Node side to handle page rendering, login verification, and API forwarding

First of all, it should be clear that when the Sub-application of Qiankun made API requests on the browser side, it actually requested the Node side of the main application in the format of /appA_apis/ XXX /appB_apis/ XXX, while the Node side of the main application did not have the logic to process these routes. Therefore, you need to add the forwarding logic to forward these requests to the Node end of the child application

Add sub-application configurations to the configuration file of the active application

subApps: [
  {
    name: 'appA',
    prefix: '/appA_apis'.// Host of the subapplication, for example, http://localhost:3000
    host: process.env['subApps.appA.host']
  },
  {
    name: 'appB',
    prefix: '/appB_apis',
    host: process.env['subApps.appB.host']}]Copy the code

Then add forwarding in the route configuration of the main application

subApps.forEach(subApp= > {
  router.all(`${subApp.prefix}/ * `.(ctx, next) = > {
    // Forward the request to '${subapp. host}/${ctx.url}'. Note that the parameters must be transparent and the content-type must be consistent. })})Copy the code

After forwarding, it will be found that the API request fails to pass the verification on the Node side of the child application. Let’s take a look at the verification process of the API request

  • Retrieves from the requested cookiex-auth-token(This key is stipulated by our project, not fixed)
  • Check whether there is a valid session corresponding to the token. If there is a valid session corresponding to the token, obtain the user information
  • Generate the JWT from the user information, pass through the other parameters, and forward to the real backend API

It is not difficult to see that the X-Auth-Token generated after the login of the main application cannot be recognized as a valid session ID by the Node side of the application

There are two ways to do it

  • The master application and all sub-applications share the same session storage. Our project uses Redis, so all applications share the same Redis

    • Advantages: simple and rough, less workload

    • Disadvantages: Common storage may cause some conflicts, and the careless development of one child application may mistakenly overwrite the critical data of other child applications; Each subapp cannot have special user information (for example, there is a special field in the user information of subA that the main app and other subapps do not have)

  • Sub-applications provide a special SSO interface. After logging in, the master application invokes the SSO interfaces of all sub-applications and transmits the X-Auth-Token and encrypted user account, enabling each sub-application to generate its own session

    • Advantages: Storage separation; Each sub-application can maintain specific user information as needed

    • Disadvantages: New interfaces need to be developed; When the number of sub-applications is large, the response time of the login action becomes long (ensure that the SSO interfaces of each sub-application are successful)

Based on the current situation, we choose the second solution, RC4 symmetric encryption for user accounts, each sub-application maintains a separate SALT, the master application maintains all the salt, and the sub-application configuration becomes

subApps: [
  {
    name: 'appA',
    prefix: '/appA_apis',
    salt: 'appA'.// Host of the subapplication, for example, http://localhost:3000
    host: process.env['subApps.appA.host']}]Copy the code

Then, after the main application has logged in, the SSO interface provided by the child application is invoked

for (const subApp of subApps) {
  // block the calling interface to make sure each request is correct
  await. }Copy the code

After the above steps, our page request problem is basically solved

Switching between sub-applications is a problem

Finally, there is switching between sub-applications. When you use the Link tag of the React Router, you cannot switch from one subapplication to another because each subapplication has its own route, and the history of each route is created by calling createBrowserHistory()

Check qiankun’s document again and find a sentence

Once the url of the browser changed after the microapplication information was registered, the matching logic of qiankun would be automatically triggered. All microapplications matched by activeRule rules would be inserted into the specified Container and the lifecycle hooks exposed by the microapplication would be called in turn.

The key is to trigger this browser URL change. The window.history.pushState method is used to do this

history.pushState(null, linkPath, linkPath);
Copy the code

When subapplication A switches to A route, subapplication B switches to subapplication B and performs operations. Then switch back to child application A. The URL is not the path of child application A when it was unmounted, but child application A will return to the page after reloading. This is good for the user experience, but it creates a mismatch between the URL address and the actual rendered interface

The solution is to switch to the previous route when switching to a sub-application. Therefore, the current route needs to be stored. This value is selected to be stored in sessionStorage because it affects only the currently open interface

First, record the current route before switching the subapplication

sessionStorage.setItem('appA-currentRoute'.window.location.href);
Copy the code

Then, after the child application is loaded, it gets the current route and jumps, then deletes the recorded route

const currentRoute = sessionStorage.getItem('appA-currentRoute');
if (currentRoute) {
  history.pushState(null, currentRoute, currentRoute);
  sessionStorage.setItem('appA-currentRoute'.' ');
}
Copy the code

Through the scheme above, the application state maintenance and URL matching of sub – application switching are realized

conclusion

So far, we have completed the preliminary practice of micro-front-end. Based on the micro-front-end framework qiankun, through the transformation of the original system and the development of a master application as a container, the effect of multi-application combination has been realized, and the user experience has been greatly improved when switching between applications. At the same time, also considered the compatibility of the problem, support sub-application access alone, also compatible with the original link, automatically redirected to the correct link

The micro front end is not a silver bullet. Only when you have a real business problem and need to improve the user experience should you consider introducing it. However, in the early stages of any future application development, the need to share domain names, micro-front-end transformation, etc., can be considered in advance to ensure that all requests have a unique subpath