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 practice can drag it down to the bottom of the article and go straight to the last section.
There is a lot of talk in the community about microfront-end architectures, but most of it is at the conceptual stage. This article will focus on a specific type of scenario, what value can be brought by the micro-front-end architecture and the technical decisions that need to be paid attention to in the process of specific practice, supplemented by specific codes, so as to help you build a production-usable micro-front-end architecture system in a real sense.
Students who are interested in or unfamiliar with the concept of micro front-end can obtain more information through search engines, such as the relevant content on Zhihu, which will not be introduced too much in this paper.
There was a “hot” discussion about the micro front end on Twitter two months ago, with a lot of people involved (Dan, Larkin, etc.), and we won’t comment much on the “event” itself today (we may write a review later), Interested students can through this article (https://zendev.com/2019/06/17/microfrontends-good-bad-ugly.html) to know a thing or two.
The value of the micro front end
The micro front-end architecture has the following core values:
- Stack independent: The main framework does not limit access to application stacks, and sub-applications have full autonomy
- Independent development and deployment: The sub-application repository is independent, and the front and back ends can be independently developed. After deployment, the main framework automatically completes synchronous update
- Independent runtime: Each child application is isolated from each other and the runtime state is not shared
The micro front-end architecture is designed to solve the problem of unmaintainable application in a relatively long time span, when a single application evolves from a common application to a Frontend Monolith due to the increase and change of the number of people and teams involved. This type of problem is especially common in enterprise Web applications.
Solution for middle and background applications
Due to its long application life cycle (often 3+ years) and other characteristics, the probability of finally evolving into a boulder application is often higher than other types of Web applications. From the perspective of technical implementation, micro-front-end architecture solutions can be roughly divided into two scenarios:
- Single instance: That is, only one child application is displayed at a time, and the child application has a full application life cycle. Subapplications are usually switched based on URL changes.
- Multiple instances: Multiple child applications can be displayed at the same time. The Web Components scheme is typically used to encapsulate sub-applications, which are more like business Components than applications.
This article focuses on a microfront-end architecture practice (based on single-SPA) in a single-instance scenario, since this scenario is closer to most mid-background applications.
Industry status quo
Traditional cloud console applications are almost always faced with the problem of monomer applications evolving into boulder applications after the rapid development of business. In order to solve the various coupling problems between product development, most enterprises also have their own solutions. At the end of 2017, the author conducted a technical survey on several well-known cloud product consoles at home and abroad:
MPA has the advantages of simple deployment, hard isolation between applications, and independent development and deployment of technology stack. The disadvantages are also obvious, as switching between applications causes the browser to reload, and there are breakpoints in the process experience due to jumping between product domain names.
SPA naturally has the advantage of experience, and the application directly does not refresh the switch, which can greatly ensure the flow of the process operation series between multiple products. 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 scheme?
At JSCONF China 2016, ucloud students shared their AngularJS-based solution (single-page application of the “federalism” practice). The concept of “federalism” mentioned in it is apt and can be considered as an early micro-front-end architecture practice based on the coupled technology stack.
Problems in microfront-end architecture practice
It can be found that the advantages of micro-front-end architecture are the combination of MPA and SPA architecture advantages. The ability to develop applications independently and integrate them together to ensure a complete process experience.
With this set of patterns, the application architecture becomes:
The kevlar layer acts as a core member of the main framework and acts as a scheduler, which determines which subapplications to activate under different conditions. Therefore, the positioning of the main framework is only: navigation route + resource loading framework.
To implement such an architecture, we need to solve the following technical problems:
Routing systems and FutureStat
In a product that implements a micro front-end kernel, we might have a link like this when accessing a child application’s page normally:
Since our subapplications are lazy loaded, when the browser is refreshed, the resources of the main framework will be reloaded, while the static resources of the subapplication will be asynchronously loaded. Since the routing system of the main application has been activated at this time, the resources of the subapplication may not be fully loaded. As a result, no rule matching subApp/ subApp/123/detail is found in the routing registry, and the NotFound page or direct routing error is reported.
This problem occurs in all lazy load scenarios, and was called Future State by the AngularJS community a few years ago.
The solution is also very simple, we need to design such a set of mechanisms:
The main framework configures the subApp route as subApp: {url: ‘/subApp/**’, entry:’./ subapp.js’}, when the browser address is /subApp/ ABC, the framework needs to load entry resources first. After the entry resources are loaded, ensure that the routing system of the sub-application is registered in the main framework, The child application’s routing system takes over the URL change event. At the same time, when the subapplication route is cut out, the main framework needs to trigger the corresponding Destroy event. When the subapplication listens to this event, it calls its own uninstall method to uninstall the application. For example, in the React scenario, destroy = () => reactdom.unmountatNode (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 UI Router library that the community already has, In particular, the React-Router implements Dynamic Routing capability after V4, so we only need to duplicate part of the route discovery logic. Here we recommend going straight to single-SPA, a related practice with a better community.
App Entry
Once routing is resolved, the way the main framework integrates with sub-applications will also be a technical decision that needs to be focused on.
1. Build-time composition vs. run-time composition
In the micro-front-end architecture mode, there are basically two ways to package sub-applications:
The pros and cons of both are also obvious:
Obviously, to achieve the core goals of true stack independence and standalone deployment, we need to use the run-time loading of sub-applications in most scenarios.
2. JS Entry vs HTMLEntry
After determining the scheme to load at runtime, another decision point is, what form of resources do we need the child application to provide as a rendering entry point?
JS Entry is usually done by subapplications typing resources into an Entry script, as in the example of single-SPA. However, this solution has many limitations, such as requiring all resources of the child application to 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 parallel loading of resources are not available.
HTML Entry is more flexible, directly typing out HTML as the Entry of the sub-application. The main frame can fetch THE static resources of the sub-application by THE way of HTML, and at the same time, the HTML document is stuffed into the container of the main frame as a child node. In this way, not only can the access cost of the main application be greatly reduced, but the development mode and packaging mode of the sub-application basically do not need to be adjusted, and it can naturally solve the problem of style isolation between the sub-applications (mentioned later). Imagine this scenario:
In the JS Entry scenario, the main framework needs to build the corresponding container node (such as the “#root” node here) before the child application is loaded. Otherwise, the child application will fail to find the container when loading. The problem, however, is that the master application does not guarantee that the container node used by the child application is a particular tag element. HTML Entry naturally solves this problem by preserving the complete environment context of the sub-application, thus ensuring a good development experience of the sub-application.
In the HTML Entry scheme, the main framework registers child applications in the following way:
In essence, HTML acts as a static resource table. In some scenarios, we can also optimize the HTML Entry scheme into Config Entry to reduce one request, for example:
To sum up:
3. Import modules
In the microfront-end architecture, we need to get hook references exposed by the child application, such as Bootstrap, mount, unmout, etc. (see single-SPA), so that we can have a complete life cycle control of the access application. Since sub-applications usually have requirements supported by both integrated deployment and independent deployment modes, we can only choose the compatible module format of UMD to package our sub-applications. How to get a reference to a module exported in a remote script at browser runtime is also an issue that needs to be resolved.
Usually our first and simplest solution is to agree 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 gets the lifecycle function from it.
This works fine, but the biggest problem is that there is a strong packaging protocol between the master application and its children. Can we find a loose coupling solution?
It is very simple, we just need to go through the umD package format of global export to obtain the export of the sub-application. The general idea is to mark the window variable and remember the global variable added at the end of each time, which is generally the variable mounted to global after export is applied. See systemJS Global Import for details.
Application isolation
There are two critical issues in a microfront-end architecture solution that will directly determine whether your solution is productive or not. Unfortunately, previous community approaches to this issue have tended to take “devious” approaches, such as default conventions between host applications to avoid conflicts. Today we’re going to try to get smarter at solving conflicts between applications from a purely technical perspective.
1. Style isolation
Because in the microfront-end scenario, sub-applications of different technology stacks are integrated into the same runtime, 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, without considering browser compatibility, the first solution that usually comes to mind 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.
However, there is a common problem with the Shadow DOM scheme in engineering practice. For example, we built a child application rendering in Shadow DOM like this:
Since the style scope of the child application is only under the shadow element, if the runtime in the child application goes out of bounds to build the DOM, the DOM built will not be able to apply the style of the child application.
For example, the ANTD Modal component is called in the sub-App. Because modal is dynamically mounted to document.body, and because of the Shadow DOM nature, antD styles only work in the Shadow scope. The result is that pop-ups cannot be applied to antD styles. The solution is to float the ANTD style up one level and drop it into the main document, but doing so means that the child application’s style is leaked directly into the main document. gg…
- CSS Module? BEM?
A common practice in the community is to avoid style conflicts by conventions on CSS prefixes, that is, by naming classes with specific prefixes, or by writing styles directly based on the CSS Module scheme. This is certainly possible for a brand new project, but often the goal of a micro-front-end architecture is to solve the access problems of stock/legacy applications. It’s clear that legacy apps often have little incentive to make big changes.
Most importantly, the convention approach has a problem that cannot be solved. If the child application uses a third-party component library, the third-party library writes a lot 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 antD versions write to the global.menu class but are incompatible with each other?
- Dynamic Stylesheet !
The solution is simple, we just need to cut/uninstall the application and uninstall its style sheet at the same time. The principle is that the browser will do the entire CSSOM reconstruction for all the style sheet insertion and removal, so as to insert and uninstall the style. This ensures that only one of the applied stylesheets is active at any one time.
The HTML Entry scheme mentioned above is inherently style isolated because the application automatically removes its stylesheets by removing the HTML structure when it is unloaded.
For example, in HTML Entry mode, the DOM structure of the child application after loading might look like this:
When the child application is replaced or uninstalled, the innerHTML of the subApp node is also overwritten, and //alipay.com/subapp.css is naturally removed and the style is uninstalled.
2. JS isolation
Having solved the problem of style isolation, there is a more critical problem that we have yet to solve: 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 problem is even trickier than the issue of style isolation, and the common play of the community is to prefix some global side effects to avoid conflicts. But we all know that this approach of “verbal” agreements between teams is often inefficient and fragile, and any solution that relies on human constraints is hard to avoid online bugs caused by human oversight. So is it possible to build a nice and completely unconstrained JS isolation scheme?
To address the JS isolation problem, we created a runtime JS sandbox. A simple architecture diagram was drawn:
Snapshots are created for the global status before the bootstrap and mount life cycles of the application. Then, when the application is switched out or uninstalled, the status is rolled back to the phase before the bootstrap starts to ensure that the application has no contamination to the global status. When the application is remounted, it returns to the state before the mount, ensuring that the application has the same global context when it is remounted as it did 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 Financial micro front end 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, Free composition at the page level between different applications, resulting in 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 has been connected to 40+ applications and 4+ different types of technology stacks of multiple product lines. During the process, we concluded a complete set of solutions for a large number of problems in micro front-end practice:
After thorough technical verification internally and online testing, we decided to open source the solution!
Qiankun – A complete set of micro front end solutions
https://github.com/umijs/qiankun
It was named Qiankun, meaning unity. We hope that with qiankun technology, you can easily transform a boulder application into a system based on a micro front-end architecture, and no longer need to pay attention to the technical details of various processes, so that it can be truly out of the box and available for production.
For UMI users, we also provided a matching qiankun plug-in, so that UMI applications could be accessed at almost zero cost.
@umijs/plugin-qiankun
https://github.com/umijs/umi-plugin-qiankun/
Finally, we welcome you to put forward your valuable comments.
Maybe the most complete micro-frontends solution youever met.
Probably the most complete microfront-end architecture solution you’ve ever seen.
Activity recommended
The annual Hangzhou Cloud Computing Conference is coming! On the third day of the conference, Ant Financial will unveil core financial technologies for the first time, including financial level cloud native concept, shared intelligence, security computing, fusion computing, graph computing, SQLFlow and other technical practices, welcome to pay attention to.
Scan the QR code or click to read the original article for details