Techniques, Strategies and recipes for building a modern Web app with multiple teams using different JavaScript frameworks. — Micro Frontends

preface

TL; DR

Those of you who want to skip the technical details and go straight to the practice can drag to the bottom of the paper and go straight to the last section.

There has been a lot of talk about microfront-end architecture in the community, but most of it has stayed at the concept introduction stage. This article will focus on a specific type of scenario, what the value of a microfront-end architecture can be and the technical decisions that need to be made in practice, along with specific code, to really help you build a production-usable microfront-end architecture system.

For those who are interested in or unfamiliar with the concept of micro front end, they can get more information through the search engine, such as the relevant content on Zhihu, which will not be introduced too much in this paper.

Two months ago, there was a “heated” discussion about the micro front on Twitter, and many people (Dan, Larkin, etc.) participated in the discussion. We will not comment on the “event” itself today (we may write an article to review it later)This articleKnow a thing or two.

Value of micro front end

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

  • Technology stack independence The main framework does not restrict access to the application of the technology stack, sub-applications have complete autonomy
  • Independent development, independent deployment sub-application warehouse is independent, the front and back end can be independent development, the main framework automatically complete synchronous update after the completion of deployment
  • When running independently, the status of each sub-application is isolated and the runtime status is not shared

The micro Frontend architecture is designed to solve the problem of individual applications becoming unmaintainable over a relatively long period of time due to the increasing number of people and teams involved in the evolution from a common application to a Frontend Monolith application. This type of problem is particularly common in enterprise-level Web applications.

Solutions for middle and background applications

Due to its long life cycle (often 3+ years), middle-background applications are more likely to evolve into a Megalithic application than other types of Web applications. From a technical implementation perspective, micro-front-end architecture solutions fall into two types of scenarios:

  • Single instance: When only one child application is displayed at a time, the child application has a full application life cycle. Typically, the child application is switched based on url changes.
  • Multiple instances: Multiple applications can be displayed at the same time. The Web Components solution is often used to encapsulate the sub-application, which is more like a business component than an application.

This article will focus on microfront-end architecture practices (based on single-SPA) for the single-instance scenario, as this scenario is closer to most middle and background applications.

Industry status quo

Traditional cloud console applications almost always face the problem of mono applications evolving into Stonehenge applications after the rapid development of business. In order to solve the various coupling problems between product development, most enterprises will have their own solutions. At the end of 2017, the author conducted a technical survey on several famous cloud product consoles at home and abroad:

product Architecture (as of 2017-12) Implementation technology
google cloud Pure SPA Main portal AngularJS, partial page Angular (ng2).
aws Pure MPA architecture Home page is based on AngularJS. Independent domain name of each system.
Seven cattle SPA & MPA hybrid architecture The portal dashboard and personal center module is spa, using the same portal module (AngularJs(1.5.10) + WebPack). Other modules are autonomous, or different versions of Portal are used, or other technology stacks are used.
And take the cloud Pure SPA architecture Based on AngularJS 1.6.6 + UI-bootstrap. The console content is simple.
ucloud Pure SPA architecture Angularjs 1.3.12

The MPA solution has the advantages of simple deployment, hard isolation between applications, and independent development and deployment of technology stack. The disadvantages are also obvious. Switching between applications will cause browser rebrush, and there will be breakpoints in the process experience due to the jump between product domain names.

SPA is born with the advantage of experience, the application of direct no refresh switch, can greatly ensure the process between multiple products when the process operation series. The disadvantage is that the application stacks are strongly coupled.

Is it possible for us to combine the advantages of MPA and SPA to build a relatively perfect micro front-end architecture solution?

At jSConf China 2016, ucloud students shared their angularJS based solution (Single-page application of “federalism” practices), which aptly refers to the concept of “federalism” as an early microfront-end architecture practice based on a coupled technology stack.

Problems in the practice of micro-front-end architecture

It can be found that the advantages of micro front-end architecture are exactly the combination of MPA and SPA architecture advantages. That is, the ability to ensure that the application has independent development rights, while also having the ability to integrate them together to ensure a complete process experience for the product.

With such a set of patterns, the architecture of the application would become:

As a core member of the main framework, the polishing Layer acts as a scheduler to determine the activation of different sub-applications under different conditions. Therefore, the positioning of the main frame is simply: navigation route + resource loading frame.

To implement such an architecture, we need to solve the following technical problems:

Routing system and Future State

In a product that implements a micro front-end kernel, a normal access to a child application’s page will have a link like this:

Graph LR A[visit //app.alipay.com] --> B(click in navigation) B --> C[//app.alipay.com/subApp] C --> D[subApp render and default redirect to list Page] D - > E / / / app.alipay.com/subApp/list E - > F [] one check list information F - > G [url into a browser / / app.alipay.com/subApp/:id/detail]Copy the code

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?

Because our child applications are lazy loaded, when the browser is refreshed, the resources of the main frame will be reloaded, and the static resources of the child application will be asynchronously loaded. Since the routing system of the main application is activated at this time, but the resources of the child application may not be fully loaded. This will cause the routing registry to find no rules matching the subapplication /subApp/123/detail, which will result in a NotFound page or a direct route error.

This problem is common to all lazy load child applications, and the AngularJS community used to refer to this problem as Future State a long time ago.

The solution is very simple. We need to design a mechanic like this:

SubApp: {url: ‘/subApp/**’, entry: ‘./ subapp. js’}, when the browser address is /subApp/ ABC, the framework needs to load the entry resources first. After the entry resources are loaded and the routing system of the sub-application is registered with the main framework, the routing system of the sub-application will take over the URL change event. When the subapplication is routing out, the main framework triggers the destroy event. When the subapplication hears the destroy event, it calls its own unload method to unload the application. For example, destroy = () in React scenarios => Reactdom.unmountatNode (container).

To implement such a mechanism, we can either hijack the URL change event to implement our own routing system, or we can build on the community’s existing UI Router library. In particular, react-Router implements Dynamic Routing capability after V4, we only need to copy part of the route discovery logic. Here we recommend to directly choose a more complete community practice of single-SPA.

App Entry

Once the routing problem is solved, the way the main framework integrates with the sub-applications will also become a key technical decision to focus on.

Build time composition vs. run time composition

In the micro front-end architecture mode, sub-application packaging can be divided into two basic ways:

plan The characteristics of
Build time The child application is packaged and distributed with the main application through the Package Registry, which can be an NPM Package, git tags, etc.
The runtime Sub-applications build their own packages, and the main application dynamically loads sub-application resources at runtime.

The pros and cons are also obvious:

plan advantages disadvantages
Build time The main application and sub-applications can be packaged and optimized, such as dependency sharing Product tool chain coupling between the sub-application and the main application. The tool chain is also part of the technology stack.

Each release of the child application relies on the main application to repackage the release
The runtime The main application is completely decoupled from the sub-application, and the sub-application is completely stack independent This adds some runtime complexity and overhead

Obviously, to achieve the two core goals of true technology stack independence and independent deployment, we need to use the run-time loader child application scenario in most scenarios.

JS Entry vs HTML Entry

After determining the runtime loading scenario, another decision point is, what kind of resources do we need the child application to provide as a rendering entry point?

JS Entry is usually done by a child application typing the resource into an Entry script, such as in the example of single-SPA. However, this solution has many limitations, such as the requirement that all the resources of the child application be packaged into a JS bundle, including CSS, images, and other resources. In addition to the potential size of the printed package, features such as the parallel loading of the resource cannot be exploited.

HTML Entry is more flexible. It directly types out THE HTML of the child application as the Entry. The main frame can fetch HTML to obtain the static resources of the child application, and at the same time, the HTML document is inserted into the container of the main frame as the child node. In this way, the access cost of the main application can be greatly reduced, and the development and packaging methods of the sub-applications basically do not need to be adjusted, and the problem of style isolation between sub-applications can be naturally solved (mentioned later). Imagine this scenario:

<! -- subapplication index.html -->
<script src="//unpkg/antd.min.js"></script>
<body>
  <main id="root"></main>
</body>
Copy the code
// Subapplication entry
ReactDOM.render(<App/>.document.getElementById('root'))
Copy the code

In the case of A JS Entry scheme, the main framework needs to build the corresponding container node (such as the “#root” node) before the child application loads, 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. The HTML Entry solution naturally solves this problem, preserving the full context of the child application and ensuring a good development experience for the child application.

Under the HTML Entry scheme, the main framework registers sub-applications as follows:

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

In essence, HTML acts as the application of static resource tables. In some scenarios, we can also optimize the HTML Entry scheme into a Config Entry scheme to reduce the number of requests, such as:

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

To summarize:

App Entry advantages disadvantages
HTML Entry 1. The development and release of sub-applications are completely independent

2. The child application has the same development experience as the independent application development
1. With one more request, the resource resolution consumption of the sub-application is transferred to the runtime

2. Host applications are not in the same build environment and cannot take advantage of some of Bundler’s build-time optimizations such as common dependency extraction
JS Entry Host applications use the same Bundler to facilitate build-time optimization 1. The release of the child application requires repackaging of the main application

2. The primary application must reserve a container node for each child application, and the node ID must be the same as the container ID of the child application

3. All resources of the sub-application need to be bundled into a bundle, resulting in low resource loading efficiency

Module import

In microfront-end architecture, we need to obtain some hook references exposed by the child application, such as bootstrap, mount, unmout, etc. (refer to single-SPA), so that we can have a complete life cycle control over the access application. Since sub-applications usually have the need to support both integrated deployment and independent deployment modes, we can only choose umD as a compatible module format to package our sub-applications. How to get a module reference exported in a remote script while the browser is running is another issue that needs to be resolved.

The first and simplest solution is to set up a global variable between the child application and the main framework, mount the exported hook reference to the global variable, and then the main application takes the lifecycle functions from it.

This solution works well, but the biggest problem is that there is a strong convention of packaging between the master and child applications. Can we find a loosely-coupled solution?

You can use the umD package format global export to get the export of the child application. The general idea is to mark the window variable and remember the global variable that is added at the end of each time. This variable is usually the one that is mounted to global after applying export. You can see systemJS Global Import for the implementation, which will not be discussed here.

Application isolation

There are two critical issues in a microfront-end architecture solution, and whether or not these two issues are addressed will directly signal whether or not your solution is truly production-usable. Unfortunately, previous attempts by the community to deal with this issue have tended to use “deroutes”, such as default conventions between host and host apps to avoid conflicts. Today we’re going to try to be more intelligent about how to solve conflicts between applications from a purely technical perspective.

Style isolation

Since sub-applications of different technology stacks are integrated into the same runtime in the micro front end scenario, we must ensure that the sub-applications do not interfere with each other’s styles at the framework level.

Shadow of the DOM?

For the “Isolated Styles” problem, 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 sub-application into a Shadow DOM, ensuring the absolute isolation of its runtime style.

However, there is a common problem with the Shadow DOM solution in engineering practice. For example, we build a sub-application to render 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">';
Copy the code

Since the style scope of the child application is only under the shadow element, once the run time in the child application runs outside to build the DOM, it will inevitably lead to the situation that the style of the child application cannot be applied to the DOM.

For example, sub-app calls antD modal component, because modal is dynamically mounted to document.body, and antD style only takes effect in the Shadow scope due to the features of Shadow DOM. The result is that the popup cannot be applied to antD’s style. The solution is to float the ANTD style one layer above the main document, but doing so means that the styles of the child application are leaked directly into the main document. gg…

CSS Module? BEM?

The common practice in the community is to avoid style conflicts by prescribing CSS prefixes, where each subapplication names its class with a specific prefix, or writes styles directly based on the CSS Module schema. For a brand new project, this is certainly feasible, but often the goal of a micro front-end architecture is more to solve the problem of access to stock/legacy applications. It is clear that legacy applications often have little incentive to make drastic changes.

Most of all, the convention approach has an unsolvable problem. What if the sub-application uses a tripartite component library that writes a large number of global styles but does not support custom prefixes? For example, if application A introduces ANTD 2.x and application B introduces ANTD 3.x, what if both versions of ANTD write the global.menu class, but are incompatible with each other?

Dynamic Stylesheet !

The solution is very simple, we just need to uninstall the style sheet after the application is cut out/uninstalled, the principle is that the browser will do the whole CSSOM refactoring for all the style sheet insert and remove, so as to achieve the purpose of inserting and uninstalling the style. This ensures that only one applied stylesheet is valid at a point in time.

The HTML Entry scheme mentioned above is inherently style isolated because the HTML structure is removed directly after the application is uninstalled, thereby automatically removing its style sheet.

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

<html>
  
  <body>
    <main id="subApp">// Subapply the complete HTML structure<link rel="stylesheet" href="//alipay.com/subapp.css">
      <div id="root">.</div>
    </main>
  </body>
  
</html>
Copy the code

When the subApp is replaced or uninstalled, the innerHTML of the subApp node is copied, 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 yet solved: how do we ensure that global variables between sub-applications do not interfere with each other, thus ensuring soft isolation between each sub-application?

This is a much trickier issue than style isolation, and the common way communities play it is to prefix global side effects to avoid conflicts. But in fact, we all know that such a way 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 create a good and completely unconstrained JS isolation scheme?

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

In other words, snapshots are taken for the global state before the bootstrap and mount life cycles of the application. Then, when the application is switched off or uninstalled, the state is rolled back to the phase before the bootstrap start to ensure that all pollution caused by the application to the global state is cleared. When the application is remounted, it is restored to the pre-mount state, ensuring that the application has the same global context when remounted as when it was first mounted.

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.

Ant micro front landing practice

Since at the beginning of the end of last year, we will try to based on micro front-end architecture model, build a set of products for the background scene in all links access platform, the purpose is to solve integration difficulties between different products, processes, fragmented, hope platform after the access to the application, no matter use which kinds of technology stack, can through the custom configuration at runtime, Achieve the freedom of page level combination between different applications to produce a personalized console with thousands of faces.

At present, this platform has been running in ant production environment for more than half a year, and it has been connected with 40+ applications of multiple product lines and 4+ different types of technology stacks. In the process, we summed up a complete set of solutions for a large number of problems in micro front end practice:

After sufficient technical verification and online testing, we decided to open source this solution!

Qiankun – A complete set of micro front end solutions

Github.com/umijs/qiank…

It was named Qiankun, meaning unity. We hope that with Qiankun, you can easily transform a megalge application into a micro front-end architecture system without having to pay attention to the technical details of the process, and make it truly out of the box and usable for production.

For UMI users, we also provide the associated Qiankun plugin @umijs/ plugin-Qiankun, so that UMI applications can access Qiankun at almost no cost.

Finally, we welcome everyone to put forward valuable comments and comments. 👻

Maybe the most complete micro-frontends solution you ever met🧐.

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