In this paper, starting from vivo Internet technology WeChat public links: mp.weixin.qq.com/s/2qH9qMNpU… Author: Tan Xin

In this paper, the concept and scene of micro front end are popularised, some mainstream realization libraries of micro front end and their usage are introduced, and some principles and practical knowledge of these libraries are explained.

First, micro front end

In a project iteration, as the business grows, the number of functional modules usually increases. It might have been that all the code modules were in one repository, under one team. However, as the number of functional modules increases, one team may not be able to handle it, and multiple teams are required to maintain different modules. The corresponding code will also be split into multiple repositories, and each module can be independently developed, deployed and updated. Often the project is split into modules, but in order to maintain the overall unity and user experience, each module is still hung under a unified entrance.

The scenario described above is a typical micro-front-end scenario, similar to the back-end micro-service architecture, which transforms the Web application from a single single application into an aggregation of multiple small front-end applications.

In general, to implement a requirement like the one above, it is easy to think of using iframe to implement it. In the entry frame, iframe is used to display the page of the submodule. When the submodule is switched, iframe is also switched to the URL of the page of the corresponding submodule.

Although iframe is relatively easy to implement, there are usually some problems:

  1. The display area is limited. For example, when a popover mask is displayed in a subproject, the mask only covers the iframe area, not the entire page, and the content is not really centered.
  2. The page browsing history cannot be automatically recorded. After the page is refreshed, iframe automatically returns to the home page.
  3. The global context is completely isolated, variables are not shared, and the communication between pages is troublesome, such as the communication between subprojects and topic framework, and between subprojects, etc., and only postMessage can be adopted.
  4. It is slow, and the entire context is rebuilt each time you enter a child application.

Some of the problems listed above are solvable, while others are impossible or difficult to solve. In general, iframe is a faster solution, but it is not the best solution and can have many limitations on the experience. If a variety of patches are forced, the complexity will rise again, and the final gain may outweigh the loss.

Second, the single – spa

We’ve talked about some of the drawbacks of iframe’s micro front end implementation. The main reason is that these applications are still in separate pages, which leads to some natural limitations. The single-SPA microfront solution combines the advantages of MPA and SPA to integrate multiple applications within a single page and is stack independent.

As shown in the figure above, the whole process of single-SPA micro front-end is realized:

Resource module loader: Used to load initialization resources for subprojects. We build the entry JS of the subproject into umD format and load it remotely using the module loader, usually using the SystemJs generic module loader (not required).

Subapplication resource configuration table: records the url information of the entry resources of each subapplication, which can be remotely loaded by using the module loader when switching between different subapplications. Since the hash of the entry resource usually changes after each subapplication update, the server needs to update the configuration table periodically so that the framework can load the latest resources of the subapplication in a timely manner.

Note: Single-spa itself does not support child application resource lists, and each child application can only pack all its initialized resources into a single entry JS. If there are multiple files for the child application initialization resource (a list of application initialization resources can be generated using the webpack-manifest-plugin), additional processing needs to be added as described above.

1. Frame entrance

<! DOCTYPE html> <html> <head> <! Register the module in systemJS --> <scripttype="systemjs-importmap">
    {
      "imports": {
        "app1": "http://localhost:8081/js/app.js"."app2": "http://localhost:8082/js/app.js"."single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"."vue": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"."vue-router": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.min.js"."vuex": "https://cdnjs.cloudflare.com/ajax/libs/vuex/3.1.2/vuex.min.js"} } </script> </head> <body> <div></div> <! -- load systemjs --> <script SRC ="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-exports.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-register.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script>
  <script>
    (function() {// load the public JS library promise.all ([system.import ())'single-spa'), System.import('vue'), System.import('vue-router'), System.import('vuex')]).then(function(modules) { var singleSpa = modules[0]; var Vue = modules[1]; var VueRouter = modules[2]; var Vuex = modules[3]; Vue. Use (VueRouter) Vue. Use (Vuex) / / single - spa registration application singleSpa son. RegisterApplication ('app1',
          () => System.import('app1'),
          location => location.pathname.startsWith('/app1')
        )
  
        singleSpa.registerApplication(
          'app2',
          () => System.import('app2'),
          location => location.pathname.startsWith('/app2') // Start singlespa.start (); }) })() </script> </body> </html>Copy the code


For simplicity, the above is just a simple demo of the framework entry HTML and does not parse the subapplication resource configuration table to load the corresponding resources. In the entry we register the child application and determine the activation time of the child application.

The subapplication resource configuration table is completely customized, as long as the entry loader side parses loaded resources according to the convention and handles the mount of these resources according to the single-SPA lifecycle hooks.

We can also extract some common resource libraries (such as Vue and vue-Router) to the entry, so that each sub-application does not need to contain these library files, which can reduce the size of resource files and improve the loading speed. These libraries should be built outside of a child application, such as when building with Webpack:

externals: ['vue'.'vue-router'.'vuex']Copy the code

2. Sub-app entry

import './set-public-path'
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'
  
Vue.config.productionTip = false
  
if (process.env.NODE_ENV === 'development'Render new Vue({router, render: h => h(App)})$mount('#app')
}
  
const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    render: (h) => h(App),
    router
  }
})
  
export const bootstrap = vueLifecycles.bootstrap
export const mount = vueLifecycles.mount
export const unmount = vueLifecycles.unmountCopy the code


Our child application, developed by VUE, needs to be wrapped in single-SPa-Vue, and then export the lifecycle hook functions. To facilitate development, we can determine the runtime environment and, if it is a development environment, render it directly to the page.

set-public-path.js

Careful students will notice that set-public-path.js is run in the child application code. So what is this file for? Let’s take a look:

import { setPublicPath } from 'systemjs-webpack-interop'
setPublicPath('app1', 2)Copy the code


As can be seen from the name, SystemJS-webpack-interop is intended for scenarios where bundles are built in SystemJS using WebPack. As we all know, when Webpack builds code, you can specify the URL prefix of the resource to load with the output.publicPath option, which is not a problem in traditional spas, but can be a problem in single-SPA pages. Such as the output. PublicPath: In the case of ‘/xx’, Webpack will assume that the url domain name of the asynchronous resource is the domain name of the current page, which is fine in traditional SPA, but in single-SPA the asynchronous resource will fail to load because the sub-application’s asynchronous resource is not the same as the url domain name of the frame page. Therefore, each child application needs to execute the above code in the entry. This will set the asynchronous resource URL prefix of the child application to be consistent with the entry JS of the child application, so that the loading path will not be wrong.

The setPublicPath code is as follows:

export function setPublicPath(systemjsModuleName, rootDirectoryLevel) {
  if(! rootDirectoryLevel) { rootDirectoryLevel = 1; }if( typeof systemjsModuleName ! = ="string" ||
    systemjsModuleName.trim().length === 0
 
  ) {
 
    throw Error(
      "systemjs-webpack-interop: setPublicPath(systemjsModuleName) must be called with a non-empty string 'systemjsModuleName'"
 
    );
 
  }
 
 
  if( typeof rootDirectoryLevel ! = ="number"|| rootDirectoryLevel <= 0 || ! Number.isInteger(rootDirectoryLevel) ) { throw Error("systemjs-webpack-interop: setPublicPath(systemjsModuleName, rootDirectoryLevel) must be called with a positive integer 'rootDirectoryLevel'"
    );
 
  }
 
 
  let moduleUrl;
  try {
    moduleUrl = window.System.resolve(systemjsModuleName);
    if(! moduleUrl) { throw Error() } } catch (err) { throw Error("systemjs-webpack-interop: There is no such module '" +
        systemjsModuleName +
        "' in the SystemJS registry. Did you misspell the name of your module?"
    );
 
 
  }
 
  __webpack_public_path__ = resolveDirectory(moduleUrl, rootDirectoryLevel);
 
}
 
function resolveDirectory(urlString, rootDirectoryLevel) {
  const url = new URL(urlString);
  const pathname = new URL(urlString).pathname;
  let numDirsProcessed = 0,
    index = pathname.length;
 
  while(numDirsProcessed ! == rootDirectoryLevel && index >= 0) { const char = pathname[--index];if (char === "/") { numDirsProcessed++; }}if(numDirsProcessed ! == rootDirectoryLevel) { throw Error("systemjs-webpack-interop: rootDirectoryLevel (" +
        rootDirectoryLevel +
        ") is greater than the number of directories (" +
        numDirsProcessed +
        ") in the URL path " +
        fullUrl
    );
 
  }
 
  url.pathname = url.pathname.slice(0, index + 1);
  return url.href;
 
}Copy the code

3. Disadvantages of single-SPA

  1. As mentioned above, if the sub-application initializes resources with multiple files (for example, CSS and NPM modules are usually separated into a single file), then we have to maintain a sub-application resource list and do some extra processing, which is often tedious.

  2. When multiple sub-applications are integrated into a single page, CSS and JS are likely to clash. Although we can make norms, such as the use of unique naming prefixes for each subproject, such artificial conventions are often unreliable. For CSS, we can also use some tools to add prefixes automatically at build time, which can be relatively reliable to avoid conflicts; A more plausible approach for JS might be to create artificial sandboxes in which the js of child applications run in their own sandboxes, but this is more complicated to implement.

Four, qiankun

In fact, an open source library, Qiankun, based on single-SPA, has helped us solve the problems mentioned above. It has the following features:

  • Parsing child application entry, not parsing JS file, second is directly parsing child application HTML file. On the operator application update, the url of the entry HTML file is always the same, and complete contains all initialized resource urls, so there is no need to maintain the resource list of the child application.

  • When a child application is mounted, special processing is automatically performed to ensure that all dom resources of the child application (including style tags added by JS) are concentrated under the DOM of the child application root node. When the child application is uninstalled, the entire DOM is removed, thus avoiding style conflicts.

  • Js sandbox is provided. When the child application is mounted, it will hijack the global Window object proxy, global event listener, etc., to ensure that each child application is running in its own sandbox, so as to avoid JS conflicts.

Demo with multiple SPA applications

The DOM structure of the child application is as follows

Of course, in the increasingly large and complex scene of the front end, the micro front end solution is not a silver bullet, but it is worth exploring the direction of practice.

5. References

  1. single-spa

  2. qiankun

  3. Probably the most complete microfront-end solution you’ve ever seen

For more content, please pay attention to vivo Internet technology wechat public account

Note: To reprint the article, please contact our wechat account: Labs2020.