preface

With the development of technology, the content of front-end applications is becoming more and more complex, and a variety of problems arise based on this, from MPA (multi-page Application) to SPA (single-page Application). Although the delay problem of handover experience is solved, it also brings long first loading time and Monolithic application problems after the explosive growth of the project. For MPA, its deployment is simple, natural hard isolation between applications, and has the characteristics of technology stack independent, independent development, independent deployment and so on. If you can combine the characteristics of these two sides, will it lead to a better user experience for users and developers? At this point, under the reference of micro-service concept, micro-front came into being.

There is a lot of talk about micro-front-end architecture in the community, but most of it is at the concept stage. This article will focus on a specific type of scenario where the value of a microfront-end architecture can be brought, and the technical decisions that need to be paid attention to in the practical process, along with specific code, to really help you build a production-usable microfront-end architecture system.

What is a micro front end?

Micro-front-end is a kind of architecture similar to micro-service. It applies the concept of micro-service to the browser side. It transforms the single-page front-end application from a single single application to an aggregation of multiple small front-end applications. Each front-end application can also be independently developed and deployed.

The value of micro front ends

The micro front-end architecture has the following core values:

  • Technology stack agnostic The main framework does not restrict access to the technology stack of the application, and child applications have full autonomy
  • Independent development and independent deployment The sub-application warehouse is independent, the front and rear ends can be developed independently, and the main framework automatically completes synchronous update after deployment
  • A standalone runtime isolates state between each child application and does not share state at runtime

The micro front-end architecture is designed to solve the problem of unmaintainability when a monolithic application evolves from a common application to a Frontend Monolith due to the increase and change of the number of people and teams involved in a relatively long time span. This type of problem is especially common in enterprise Web applications.

Solutions for mid – and back-end applications

Due to the long application life cycle (usually 3+ years), middle and background applications are more likely to evolve into a rock app than other types of web applications. This mainly brings about two problems: backward technology stack and slow compilation and deployment. From the perspective of technical implementation, micro-front-end architecture solutions can be roughly divided into the following scenarios:

  • Front-end containerization:iframeEffectively embeds another page/single page application within the current page, between two pagesCSSandJavaScriptThey’re isolated from each other.iframeYou create a new, independent hosting environment, similar to sandbox isolation, which means that front-end applications can run independently of each other. If we make an application platform, we will integrate the third party system in the system, or the system of several different departments and teams, williframeAs a container for other front-end applications, this is obviously still a very plausible solution.
  • Microcomponents: With helpWeb ComponentsTechnology that allows developers to create reusable custom elements to build front-end applications across frameworks. Usually use Web ComponentsTo do subapplication encapsulation, a subapplication is more like a business component than an application. Really use it on a projectWeb ComponentsTechnology, we still have some distance from the present, but the combinationWeb ComponentsTo build a front-end application, is an architecture for future evolution.
  • Microapplications: a software engineering approach that combines multiple independent applications into a single application in a deployment build environment.
  • Micromodules: Develop a new build system that builds part of the business function into a separate onechunkCode, use only need to remote load.

Micro front-end architecture

Micro-front-end architecture is a good reference to the characteristics of SPA without refresh, and a new layer is introduced on top of SPA to realize the function of application switching:

Problems in the practice of micro-front-end architecture

It can be found that the advantages of the micro-front-end architecture are the combination of the advantages of MPA and SPA architectures. The ability to integrate them together to ensure a complete process experience for the product, while ensuring that the application has the right to develop independently.

The Stitching Layer acts as the core member of the master framework and acts as the scheduler that decides to activate different sub-applications under different conditions. Therefore, the main framework is simply positioned as: navigation route + resource loading framework.

In order to realize such an architecture, we need to solve the following technical problems:

Routing system and Future State

In a product that implements a microfront-end kernel, when we normally visit a page of a child application, we might have a link like this:

At this point the browser’s address may be https://app.alipay.com/subApp/123/detail, imagine, at this time we manually refresh the browser, what happens?

Since our children are lazy loaded, when the browser reloads, the resources of the main framework will be reloaded and the static resources of the children will be asynchronously loaded. Since the routing system of the main application has been activated at this time, but the resources of the children may not be fully loaded yet. As a result, there are no rules in the routing registry that match the child application /subApp/123/detail, which can result in either a jump to the NotFound page or a direct routing error.

This is a problem that occurs with all lazy load scenarios for subapplications, and was referred to as Future State by the AngularJS community a few years ago.

The solution is simple. We need to design a routing mechanism like this:

Subapp: {url: ‘/subApp/**’, entry: ‘./ subapp.js’}, then when the browser address is /subApp/ ABC, the framework needs to load the entry resource first. After loading the entry resource and ensuring that the routing system of the child application is registered in the main framework, the routing system of the child application can take over the URL change event. At the same time, when the child application route is cut out, the main framework needs to trigger the corresponding destroy event. When the child application listens to this event, it will call its own uninstall method to uninstall the application. As in the React scenario, destroy = () => reactdom.unmountNode (container).

To implement such a mechanism, we can either hijack the URL change event ourselves to implement our own routing system, or we can build on the community’s existing UI Router Library. In particular, the React Router has implemented Dynamic Routing capabilities since V4, and we only need to copy part of the Routing discovery logic. Here, we recommend directly choosing Single-Spa, a relevant practice that is well established in the community.

App Entry

Once the routing problem is resolved, the way in which the main framework is integrated with sub-applications becomes a technical decision that needs to be focused on.

Child application loading

Monorepo

The easiest way to use Single-Spa is to have a repository that contains all the code. Typically, you only have a package.json, a Webpack configuration that produces a package, which is referenced in an HTML file via the ‘ ‘tag.

NPM package

Create a parent application and NPM installs each Single-Spa application. Each child application is in a separate code repository and is responsible for releasing a new version with each update. When a single-spa application changes, the root application should be reinstalled, rebuilt, and redeployed.

Dynamic loading module

The child application builds its own packaging, and the main application dynamically loads the child application resources while it runs.

plan advantages disadvantages
Monorepo 1. Easiest to deploy

Single version control
1. For each individual project, oneWebpackThe configuration andpackage.jsonIt means less flexibility and freedom.

2. As your project gets bigger and bigger, packing gets slower and slower.

3. Build and deployment are tied together, which requires a fixed release schedule, not an AD hoc release
NPMpackage 1.npmInstallation is more familiar to development and easier to set up

2. IndependentnpmPackages mean that each application is released tonpmThe warehouse can be packaged separately before
1. The parent application must reinstall the child application to rebuild or deploy
Dynamic loading module The main application is fully decoupled from the child application, and the child application can use any technology stack There’s a little bit more runtime complexity andoverhead

Obviously, in order to achieve the two core goals of true technology stack independence and independent deployment, we need to use the run-time loading subapplication solution in most scenarios.

JS Entry vs HTML Entry

After deciding on the run-time loading option, another point to make is, what form of resource do we need the child application to provide as a render entry point?

The way JS Entry is usually done is when the child application types the resource into an Entry script, as in the Single-Spa example. However, this solution has many limitations, such as requiring all resources of the child application to be packaged in a JS bundle, including resources such as CSS and images. In addition to the problem of potentially large packages, features such as parallel loading of resources cannot be taken advantage of.

HTML Entry is more flexible. It directly prints HTML into the child application. The main frame can fetch static resources of the child application through the way of fetching HTML, and at the same time, it stuffs HTML Document into the container of the main frame as the child node. This not only greatly reduces the access cost of the main application, but also basically eliminates the need to adjust the way sub-applications are developed and packaged, and naturally solves the problem of style isolation between sub-applications (mentioned later). Imagine this scenario:

<! >< body> <main id="root"></main> </body> // ReactDOM.render(<App/>, document.getElementById('root'))

In the case of JS Entry, the main framework needs to build the container node (such as the “#root” node in this case) before the child application is loaded, otherwise the child application will report an error because the container cannot be found. The problem is that the master application does not guarantee that the container node used by the child application is a particular tag element. HTML Entry’s solution naturally solves this problem by preserving the full context of the child’s environment and ensuring a good development experience for the child.

Under the HTML Entry scheme, the main framework registers child applications as:

framework.registerApp('subApp1', { entry: '//abc.alipay.com/index.html'})

In essence, HTML is used to apply a static resource table. In some cases, we can also optimize the HTML Entry scheme to Configure Entry to reduce one request, such as:

framework.registerApp('subApp1', { html: '', scripts: ['//abc.alipay.com/index.js'], css: ['//abc.alipay.com/index.css']})

To summarize:

The remote loading

In the micro-front-end architecture, we need to obtain some hook references exposed by the child application, such as Bootstrap, mount, unmount, etc. (refer to Single-SPA), so that we can have a complete life cycle control over the access application. As subapplications are usually supported by both integrated deployment and stand-alone deployment, we can only choose UMD as a compatible module format to package our subapplications. How to get the exported module reference in the remote script while the browser is running is also an issue that needs to be resolved.

The first and simplest solution is to agree on a global variable between the child application and the main framework, mount the exported hook reference to that global variable, and then the main application fetchlifecycle functions from that global variable.

This works well, but the biggest problem is that there is a strongly contracted packaging protocol between the main application and its children. So can we find a loosely coupled solution?

We can simply use the UMD package format of global export to get the child’s export. The general idea is to mark the window variable and remember that the last global variable is added. This variable is usually the variable that is mounted on global after applying export. See SystemJS Global Import for details on how to do this.

How does the main application know which chunks of the child application to load? How do you load each of them into the main application?

Our implementation idea is to have subprojects use stats-webpack-plugin, and output a manifest. JSON file containing only important information after each packaging. Webpack configuration in React application built with create-react-app uses webpack-manifest-plugin to generate resource list by default. The parent project first requests this JSON file with Ajax, reads the JS directory that needs to be loaded from it, and then loads it synchronously.

The webpack-manifest-plugin generates a resource list called asset-manifest.json

{" files ": {". The main js" : "/ index. Js", "main. Js. Map" : "/ index. Js. Map", "static/js / 1.97 da22d3. The chunk. Js" : "/ static/js / 1.97 da22d3. The chunk. Js", "static/js / 1.97 da22d3. The chunk. Js. Map" : "/ static/js / 1.97 da22d3. The chunk. Js. The map", "static/CSS / 2.8 e475 c3e. The chunk. The CSS" : "/ static/CSS / 2.8 e475 c3e. The chunk. The CSS", "static/js / 2.67 d7628e. The chunk. Js" : "/ static/js / 2.67 d7628e. The chunk. Js", "static/CSS / 2.8 e475 c3e. The chunk. The CSS. The map" : "/ static/CSS / 2.8 e475 c3e. The chunk. The CSS. The map", "static/js / 2.67 d7628e. The chunk. Js. Map" : "/ static/js / 2.67 d7628e. The chunk. Js. The map", "static/CSS / 3.5 b52ba8f. The chunk. CSS" : "/ static/CSS / 3.5 b52ba8f. The chunk. CSS", "static/js / 3.0 e198 e04. The chunk. The js" : "/ static/js / 3.0 e198 e04. The chunk. The js", "static/CSS / 3.5 b52ba8f. The chunk. CSS. The map" : "/ static/CSS / 3.5 b52ba8f. The chunk. CSS. The map", "static/js / 3.0 e198 e04. The chunk. The js. Map" : "/ static/js / 3.0 e198 e04. The chunk. The js. The map", "index.html" : "/ index. The HTML"}, "entrypoints" : [" index. Js "]}

Application isolation

There are two critical issues in a micro front-end architecture solution that will be a direct indicator of whether your solution is actually production-usable. Unfortunately, the community’s approach to this issue has always been to take a “bypass” approach, such as using default conventions between host and child applications to avoid conflicts. Today we’re going to try to be more intelligent in resolving potential conflicts between applications from a purely technical point of view.

Style isolation

Because in the micro-front-end scenario, children of different technology stacks are integrated into the same runtime, we have to make sure that there is no style interference between the children at the framework level.

Shadow of the DOM?

When dealing with “Isolated Styles”, the first solution that usually comes to mind, regardless of browser compatibility, is Web Components. Based on the Shadow DOM capability of Web Components, we can wrap each child application in a Shadow DOM, ensuring absolute isolation of its runtime style.

A common problem with the Shadow DOM solution is that we build a child application that renders in the Shadow DOM like this:

const shadow = document.querySelector('#hostElement').attachShadow({mode: 'open'});
shadow.innerHTML = '<sub-app>Here is some new text</sub-app><link rel="stylesheet" href="//unpkg.com/antd/antd.min.css">';

Since the style scope of a child application is limited to the shadow element, any scenario in which the child application runs out of bounds to build the DOM will inevitably result in the DOM being built that does not apply the style of the child application.

For example, the sub-app calls the antd modal component, because modal is dynamically mounted to document.body, and because of the feature of Shadow DOM, antd style will only be effective in the scope of Shadow. The result is that the pop-up box does not apply to ANTD’s style. The solution is to float the ANTD styles one level above the main document, but doing so means that the child application styles leak directly to the main document. gg…

CSS Module? BEM?

A common practice in the community is to avoid style conflicts by agreeing on CSS prefixes, with sub-applications naming classes using specific prefixes, or simply writing styles based on the CSS module scheme. For a brand new project, this is certainly possible, but in general the goal of a micro front-end architecture is more to solve the problem of access to stock/legacy applications. It’s clear that legacy apps often have little incentive to make drastic changes.

Most importantly, the convention approach has an unsolvable problem. What if a child application uses a tripartite component library that writes a lot of global styles but doesn’t support custom prefixes? For example, if application A introduces ANTD 2.x, and application B introduces ANTD 3.x, both versions of ANTD write to the global.menu class, but they are incompatible with each other.

Dynamic Stylesheet !

The solution is very simple, we just need to unload the style sheet after the cut/unload application, the principle is that the browser will do all the inserts and removes of the style sheet refactoring the entire CSSOM, so as to insert and uninstall the style. This ensures that only one applied stylesheet is active at a time.

The HTML Entry scheme mentioned above is inherently styleshed-isolated, as the application automatically removes its stylesheets by removing the HTML structure when it unloads.

For example, in HTML Entry mode, the DOM structure after the child application is loaded might look like this:

< HTML > <body> <main id="subApp"> // Full HTML structure of child application <link rel="stylesheet" href="//alipay.com/subapp.css"> <div id="root">.... </div> </main> </body> </html>

When the subApp is replaced or uninstalled, the subApp node’s innerHTML is duplicated, and //alipay.com/subapp.css is removed and the style is uninstalled.

JS isolation

Having solved the problem of style isolation, there is a more critical issue that we have not solved yet: how do we ensure that global variables between sub-applications do not interfere with each other, thus ensuring soft isolation between sub-applications?

This problem is trickier than the problem of style isolation, and the common gameplay of the community is to prefix some global side effects with various prefixes to avoid conflicts. But in fact, we all know that this kind of “verbal” agreement between teams is often inefficient and fragile, and all schemes that rely on human constraints are hard to avoid online bugs caused by human negligence. So is it possible to make a good and completely unconstrained JS isolation scheme?

To solve the problem of JS isolation, we created an original runtime JS sandbox. I simply drew an architecture diagram:

Snapshots of the global state are taken before the application’s bootstrap and mount lifecycles, and then the state is rolled back to the stage before the application’s bootstrap/uninstall cycle to ensure that all pollution to the global state is cleared. When the application re-enters, it reverts back to the pre-mount state, ensuring that the application has the same global context when remount as it did when the application first mounted.

Snapshots can be thought of as temporary storage
mountIn front of the
windowObject,
mountThen a new one will be created
windowObject, when
umountAfter and back to the temporary
windowobject

Of course do far more than these in the sandbox, other also includes some of the global event listeners hijacked, etc., to ensure that the application after the cut out of the global event listener can get complete uninstall, also when the remount to monitor these global events, so as to simulate the consistent with the application of independent runtime sandbox environment.

Some events may be listened on globally when the script for the main application is initially loaded, such as
window.resizewhen
umountAfter and back to the temporary
windowObject needs to be listened on again

qiankun VS single-spa

QIAKUN is based on Single-Spa. What is the difference between QIAKUN and Single-Spa?

qiankun

From the perspective of Qiankun, the micro-front end is the “micro-application loader”, which mainly solves the problem of how to safely and quickly gather multiple scattered projects together, which can be seen from Qiankun’s own points:

All of these features serve the “microapplication loader” position.

single-spa

From the perspective of Single-Spa, the micro-front-end is a “micro-module loader”. Its main problem is how to realize the “micro-serviced” of the front-end, so that applications, components and logic can all become shared micro-services. This can be seen from the overview of the micro-front-end in Single-Spa:

From Single-Spa’s point of view, there are three types of microfront-ends: microapplications, microcomponents, and micromodules. In fact, Single-Spa requires that they all be packaged in SystemJS, in other words that they are essentially micromodules.

SystemJSIs a runtime loading module tool that is currently under (not yet officially supported by browsers)
importMap) native
ES ModuleA complete substitute for.
SystemJSA dynamically loaded module must be
SystemJSModules, or
UMDThe module.

Qiankun vs. Single-Spa?

Based on Single-SPA, Qiankun strengthens the micro application integration capability, but abandons the micro module capability. So, the difference between them is the granularity of micro-services, where the granularity of what can be served is at the application level, and Single-SPA is at the module level. Both can split the front end, but at different granularity.

  1. Micro-application loader: The granularity of “micro” is the application, i.eHTML, which can only be shared at the application level
  2. Micromodule loader: The granularity of “micro” is module, i.eJSModule, which can be shared at the module level

Why would you do that? We need to be clear about the context in which these two frameworks emerged:

Qiankun: Alibaba has a large number of projects in disrepair and the business side urgently needs tools to integrate them quickly and safely. In this perspective, there is no need to do module federation at all, it is only how to integrate projects quickly and safely. So Qiankun wanted to make a micro front end tool.

Single-SPA: Learn the micro-service of the back end, realize the micro-service of the front end, make the application, components and logic all become shareable micro-service, and realize the real micro-front end. So Single-Spa wanted to do a game-changer.

I’ve also compiled a diagram here to make it easier to understand:

conclusion

There are three main considerations for migrating the micro front end: child application loading, style isolation, and script isolation. After the above analysis, some feasible solutions are presented: SystemJS Dynamic loading script, Dynamic Stylesheet and Snapshot.

Refer to the article

Probably the most complete micro front end solution you’ve ever seen

Architecture design: Micro front-end architecture

Exploration of several micro front end schemes

It is suitable for the “micro front end” solution of existing large MPA projects

Let’s talk about the micro front end

Single-SPA based micro front – end architecture

Single- SPA + Vue CLI micro front end landing guide + video (project isolation remote loading, automatic introduction)