This article mainly introduces some basic knowledge of micro-front-end and simple use and real implementation of single-SPA micro-front-end framework.
1. Basic knowledge of micro front-end
What is a micro front end
The micro front end is an architecture approach. The core idea is to separate and autonomous. A super large application is divided into several small decoupled autonomous applications.
Advantages of a micro front end (why)
- Each application has an independent Git project, an independent development/test team, and an independent construction and deployment process without affecting each other.
- Free technology stack Each application can use a different technology stack.
- Js and CSS that are naturally lazy to load child applications will only be downloaded and executed when the child application is mounted.
Microfront-end scenarios (and when to use them)
- With the passage of time, the system becomes more and more huge, difficult to maintain, often change a place and move the whole body, bringing online risk is not controllable; Intolerable build/deployment times, etc.
- The new system needs to integrate the functions of the old system, which has old technology and does not want to reconstruct or has no time to reconstruct.
- A single system application is maintained and developed by many teams simultaneously.
- Want to try different technology stacks.
Problems with the micro front end (Issues to be aware of when using)
The micro front end application usually consists of two parts, one is the sub-application that we split out (normal SPA project), and the other is the base application that integrates these sub-applications (resource loading and scheduling of sub-applications). Because these applications share a common runtime, there are bound to be problems.
-
Sharing depends on
- Pulling the NPM package Usually we abstract and encapsulate the common logic /UI in each project, maintain it in another Git repository, and then release the NPM package; Other projects install the NPM package for use.
- Packaging external + CDN For example, each sub-application uses the VUE technology stack, so the sub-application can be built and packaged without vUE/VUE-Router, which reduces the volume of the sub-application after packaging. Then introduce these VUE/VUE-Router dependent CDNS into the base application to ensure that these dependencies are loaded only once. Provided, of course, that the versions of these common dependencies are consistent.
- Depending on separate packaging, there is also a case where our child applications are on different technology stacks or the same technology stack but versions are different. What should we do in this case? The answer, of course, is that each child application is built and packaged normally, without external. Because external makes no sense at this point, there are no common dependencies between sub-applications.
-
Style conflicts this is not a new problem, but it has become more serious in the architectural pattern of the micro front end. So how to solve it? The answer is that styles need to be isolated from each other. There are two main types of style conflicts here: conflicts between sub-applications and conflicts between base applications and sub-applications.
- Conflicts between sub-applications are relatively easy to resolve. When the application is uninstalled, delete the corresponding link tag and style tag.
- Conflicts between base applications and subapplications For such conflicts, you can use CSS Modules or namespaces to give each micro-application Module a specific prefix to ensure that it does not interfere with each other, you can use the PostCSS plug-in of Webpack.
-
Js isolation because the micro front end is a common runtime, so it is likely to cause global variable pollution and other problems, to this end, it is necessary to load and uninstall each sub-application at the same time, as far as possible to eliminate this conflict and influence, the most common approach is to use standard convention and sandbox mechanism.
-
Application communication The communication between applications is a problem that the micro front end cannot get rid of. Although we have decoupled the applications sufficiently, we still encounter scenarios where applications need to communicate. Usually we communicate through intermediaries or global objects.
- Leverage custom events.
Such as the use ofwindow.dispatchEvent
Trigger event,window.addEventListener
Listen for events. - Use your browser’s address bar as a communication bridge
- Make use of some global objects
- Leverage custom events.
Implementation of micro front end (how to use)
plan | advantages | disadvantages |
---|---|---|
iframe | Natural support for isolation, developers do not need to worry about isolation Access to the simple Multiple subapplications can be mounted simultaneously |
The browser refresh iframe URL status is lost The pop-up box can only be displayed in the IFrame area Communication is difficult, only through postMessage . |
single-spa | Provides cli tools to generate base applications, micro-applications, micro-modules, and micro-components Support as many frameworks and build tools as possible and provide adaptation tools |
Js, CSS isolation needs to be handled by the developer Access has a cost |
qiankun | Based on single-SPA, it provides a more out-of-the-box API HTML Entry access allows you to access micro applications as easily as using iframe Style isolation ensures that styles do not interfere with each other between microapplications JS sandbox to ensure no global variable/event conflicts between microapplications Resource preloading, which preloads unopened microapplication resources during browser idle time |
Shared dependencies are inconvenient, which can lead to duplication of dependencies, making them bulky Do not support the SSR |
emp | Module Federation is a new feature based on Webpackage 5 to achieve third-party dependency sharing Load on Demand, developers can choose to load only the required parts of the micro-application, rather than forcing the entire application to be loaded Support the SSR Remotely pull the TS declaration file State sharing is extremely convenient |
Rely on webpack5 |
How do I dynamically load child applications
What form of resources does the child application provide as a rendering entry point? At present, there are two schemes: HTML Entry and Js Entry
- Js Entry sub-applications type resources into an entry script, including CSS. The disadvantage is that the package size is huge and the parallel loading of resources can not be utilized. Such is the case with VUE projects generated by CLI tools such as Single-SPA.
- HTML entry prints out HTML for the sub-application as the entrance. The main frame can fetch static resources of the sub-application by MEANS of HTML, and obtain corresponding JS, CSS, and entrance scripts through a lot of regulars. 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. Of course, static resources of sub-applications can also be directly provided as JSON files, which are called Config Entry and also BELONG to HTML entry.
2. Simple use of single-SPA
Single-spa is a JavaScript micro-front-end framework that aggregates multiple single-page applications into a Single application.
- compatibility
"browserslist": [ "ie >= 11", "last 4 Safari major versions", "last 10 Chrome major versions", "last 10 Firefox major versions", "last 4 Edge major versions" ] Copy the code
- Core API
- Base application code
import { registerApplication, start } from "single-spa"; registerApplication({ name: "vue-app1".app: () = > System.import("./app1.js"), activeWhen: "/app1".customProps: { title: 'app1'}}); start({urlRerouteOnly: true }); // name: string Application name // app: Application | () => Application | Promise<Application> // activeWhen: string | (location) => boolean | (string | (location) => boolean)[] // Application activation conditions // customProps? : Object = {} Custom property that is passed as an argument when the lifecycle hook function is executed Copy the code
- The child application
export function bootstrap(props) { // This lifecycle function is executed once before the application is first mounted. console.info("bootstrap", props); return Promise.resolve().then(() = > { console.info("bootstrap"); }); } export function mount(props) { // The mounted lifecycle function is called whenever the application route matches successfully, but the application is not mounted. When called, the function determines the currently active route based on the URL, creates DOM elements, listens for DOM events, and so on to present the rendered content to the user. // Any child route changes (such as hashchange or popState, etc.) do not trigger the mount again and need to be handled by each application. console.info("mount", props); return Promise.resolve().then(() = > { document.getElementById("app").innerHTML = I was `${props.title}Ah `; window.app1 = 'app1'; }); } export function unmount(props) { // The unmount lifecycle function is called whenever the application route match fails, but the application is mounted. When the unload function is called, it cleans up the DOM elements, event listeners, memory, global variables, message subscriptions, and so on that were created when the application was mounted. console.info("unmount", props); return Promise.resolve().then(() = > { document.getElementById("app").innerHTML = ""; window.app1 = undefined; }); } export function unload(props) { // Removing the implementation of the lifecycle function is optional and only fires when unloadApplication is called. If a registered application does not implement this lifecycle function, it is assumed that the application does not need to be removed. // The purpose of removal is for each application to perform some logic before removal. Once the application is removed, its state will change to NOT_LOADED and it will be reinitialized the next time it is activated. // The remove function is designed to be a "hot download" for all registered applications, but it can be useful in other scenarios, such as when you want to reinitialize an application and perform some logical operations before reinitialization. return Promise.resolve().then(() = > { console.info("unload", props); }); } Copy the code
This is the simplest use of single-SPA, where the base application registers the child application and the child application implements it
bootstrap
.mount
.unmount
These three life cycle functions.
We didn’t have to implement these three lifecycle functions ourselves in actual development; single-SPA did that for us.Zh-hans.single-spa.js.org/docs/ecosys…
In the case of vueimport { h, createApp } from "vue"; import singleSpaVue from "single-spa-vue"; import App from "./App.vue"; import router from "./router"; const vueLifecycles = singleSpaVue({ createApp, appOptions: { render() { return h(App, { name: this.name, }); }, el: '#app' }, handleInstance(app){ app.use(router); }});export const bootstrap = vueLifecycles.bootstrap; export const mount = vueLifecycles.mount; export const unmount = vueLifecycles.unmount; Copy the code
- Base application code
3. Real landing scene
- Modify the sub-application
- use
webpack-assets-plugin
Build generationjson
File, descriptionjs
.css
Resource in the following format
{ "css": [ "xxx.css"."yyy.css"]."js": [ "xxx.js"."yyy.js"]}Copy the code
- Package in UMD format
The base application needs to get some hook references exposed by the child application, such asbootstrap
.mount
.unmount
;
The simplest solution is to specify a global variable (such as the project name of the child application) between the child and the base application, mount the exported hook reference to the global variable, and then the base application takes the lifecycle function from it.
The sub-application needs to adjust the packaging method as follows
configureWebpack: { output: { library: name, libraryTarget: 'umd'.jsonpFunction: `webpackJsonp_${packageName}`,}}// name is the name of the child application project, and the window has a '${name}' attribute Copy the code
- use
- Handling of base applications
- How to get the js and CSS of the child application
The base application has a JSON file that is used to maintain the child applications{ "vue-app1": { "path": "va1" }, "vue-app2": { "path": "va2"}},// The key value is the activation condition of the subapplication, that is, the base route of the subapplication // path is used for forwarding Copy the code
The base application needs to request the output of assets.json from each child application according to the above JSON file, and then get the corresponding JS and CSS resources.
import slaveMapping from './slave-mapping.json'; async function resolveAppsFromMenu() { const avaliableApps: string[] = Object.keys(slaveMapping); const apps = await Promise.all( avaliableApps.map(appName= > { const timestamp = Date.now(); const appDir = slaveMapping[appName].path; return (fetch.get(` /${appDir}/assets.json? _t=${timestamp}`) .then((res) = > { const exports = res.data; return { name: appName, css: exports.css, js: exports.js, }; }}))));return apps } Copy the code
- How do I register a child application
import { registerApplication, start } from "single-spa"; async function load() { const apps = await this.resolveAppsFromMenu(); await registerApps(apps, { user: this.userInfo }) } function registerApps(apps, customProps) { for(const app of apps) { const { name } = app; registerApplication({ name, app: async () => { lifecycle = await loadApp(app); const{ mount, unmount, ... otherMicroAppConfigs } = lifecycle;return { mount: [async() :Promise<any> => await loadStyles(app), mount], unmount: [unmount, async() :Promise<any> => awaitunloadStyles(app)], ... otherMicroAppConfigs, } },activeWhen: (location) = > new RegExp(` ^ /${name}\\b`).test(location.pathname), customProps, }) } } Copy the code
loadApp
Why can the return value be deconstructedmount
.unmount
, etc life cycle function;async function loadApp(app) { const { js, css } = app; // Load js, and then mount the corresponding properties on the window, such as Winodw.vue-app1 await fetchAppScripts(js); // entryName is a property mounted on the window, such as winodw.vue-app1 const entryName = getGlobalProp(); return window[entryName] // The return value has a corresponding lifecycle hook } Copy the code
- How are style conflicts resolved
We can see that when the child application is loaded above, themount
Phase we load the style corresponding to the application, while inunmount
Phase we uninstalled the corresponding stylesasync function loadStyles(app) { await Promise.all(app.css.map(style= > loadStyle(style))); } // Load the style async function loadStyle(src) { const loadingPromise: Promise<void> = new Promise((resolve, reject) = > { const linkNode: HTMLLinkElement = document.createElement('link'); linkNode.rel = 'stylesheet'; linkNode.type = 'text/css'; let load: (() = > void) | null = null; let error: (() = > void) | null = null; load = (): void= > { linkNode.removeEventListener('load', load!) ; linkNode.removeEventListener('error', error!) ; resolve(); }; error = ():void= > { linkNode.removeEventListener('load', load!) ; linkNode.removeEventListener('error', error!) ; reject(); }; linkNode.addEventListener('load', load); linkNode.addEventListener('error', error); linkNode.href = src; document.head.appendChild(linkNode); }); return loadingPromise; } // Uninstall styles function unloadStyles(app: LoadableApp) :void { app.css.forEach(css= > { const links = document.querySelectorAll(`link[href="${css}"] `); links.forEach(link= > { if(link && link.parentNode) { link.parentNode.removeChild(link); }}); }); }Copy the code
- How to get the js and CSS of the child application
Refer to the article
- single-spa
- qiankun
- systemjs
- emp