First, the origin of micro front-end

With the development of front-end history, there are two front-end development modes, MPA multi-page application mode and SPA single-page application mode, which have their own uniqueness and shortcomings respectively.

(1) MPA mode

For example, the middle and background system covers multiple service modules, which are managed by different teams. Each service module has an independent domain name. When accessing different service modules, the browser will be refreshed or the TAB page will be opened to realize the jump between systems. MPA mode has the advantages of simple deployment, isolation between service modules, and independent development and deployment of technology stack. Its disadvantages are also obvious, the switch between different modules will cause browser rebrush, different product domain names jump to each other, there will be obvious breakpoints in the process experience.

(2) SPA mode

It is believed that front-end applications are almost built and developed by SPA, Vue, React and Angular. Page hopping between applications unloads/mounts pages by listening to browser URLS. Therefore, it has inherent advantages in experience, and there is no need to refresh browsers when switching between pages. It can greatly ensure the flow of multi-product process operation in series; The disadvantage is that each application module is strongly coupled, and with the iteration of application requirements, there will be boulder applications. Microfront-end is a kind of architecture similar to microservices, which applies the concept of microservices to the browser side, that is, the single-page front-end application is transformed from a single single application to a number of small front-end applications into one application. It combines the advantages of MPA mode and SPA mode respectively. The common micro front-end architecture has the following advantages:

  • Stack independent: use multiple front-end frameworks on the same page without refreshing the page (Vue, React, Angular, etc.);

  • Strong independence: independent development, independent deployment and incremental update of different business applications;

  • Runtime isolation sharing: Data can be shared and communicated between sub-applications of different services, but JS and CSS do not affect each other.

  • Experience advantages: Consistent operation of application processes on a single page without refreshing pages;

Second, the problems of IFrame

In the early days, before the concept of a micro front end, when we were integrating multiple teams and multiple applications, our unanimous choice was iframe. The most important feature of iframe is that it provides a browser native hard isolation scheme, no matter the style isolation, JS isolation and other problems can be solved perfectly. However, its biggest problem is that its isolation can not be broken, leading to the application context can not be shared, resulting in the development experience, product experience problems.

  • The URL is not synchronized. The browser refresh iframe URL status is lost, and the back forward button is unavailable.

  • The UI is not synchronized and the DOM structure is not shared. Imagine a pop-up with a mask layer in the bottom right corner of the iframe, and we want the pop-up to center the browser and automatically center the browser when resize.

  • The global context is completely isolated and memory variables are not shared. Iframe internal and external system communication, data synchronization and other requirements, the cookie of the main application should be transparently transmitted to the sub-applications with different root domain names to achieve the effect of free registration.

  • Sub-applications are slowly loaded in the first batch. Each child application entry is a process of browser context reconstruction and resource reloading.

3. Single-spa example

Single-spa is the cornerstone of many micro-front-end frameworks (it is itself a micro-front-end framework, but many large manufacturers and micro-front-end frameworks are based on it for secondary packaging), so a deep understanding of its principle is the basis for the exploration and practice of micro-front-end. Single-spa is a javascript micro-front-end framework that aggregates multiple single-page applications into a single application. For details on single-SPA tutorials and apis, see the single-SPA website. It is also recommended that you do a quick read of the website before reading the following sections to find out what it is and what it can do. In this chapter, we directly describe the use of single-SPA using practical examples provided on the official website of single-SPA, and lead to the principle analysis of the next chapter with thinking behind the phenomenon.

1. Clone instance project

git clone https://github.com/joeldenning/coexisting-vue-microfrontends.git
Copy the code

2. Start the project

In the base app and subapp directories, install the dependency packages and start the apps separately:

// root-html-file
cd root-html-file
npm install
npm run serve​
// navbar
cd navbar
npm install
npm run serve
​// app1 
cd app1
npm install
npm run serve​
// app2
cd app2
npm install
npm run serve
Copy the code

3. Observing examples & thinking

(1) We look at the instance code of the base application. The single spa registerApplication function is called by the base application to register the child application. Start the registerApplication. What does start do and how to implement it internally?

Bootstrap /mount/unmount: Bootstrap /mount/unmount: bootstrap/mount/unmount: bootstrap/mount/unmount: bootstrap/mount/unmount

(3) We can visit http://localhost:5000/ by browser and get the following page. By looking at the example code, we can see that the following page is the base application navbar page. How does single-spa achieve this route matching? Clicking App1 and App2 will redirect to the page of App1 and App2, respectively, without refreshing the page. The key feature of single-SPA — URL routing matching;

(4) We repeatedly click App1 App2 and observe the Network TAB option in the browser debugging box. We will find that: when switching to a different subapplication, the resources required for subapplication rendering will be loaded only when the subapplication is rendered for the first time, and relevant resources will not be loaded in subsequent switching; In the browser debugbox Elements option, we can see that the Dom structure of different child apps is mounted and unmounted as the child apps switch. How does single-SPA download, mount, and unmount resources for children?

If you are confused by some of the above phenomena or implementations, and you would like to understand the principles, then the next chapter, principle analysis, is for you.

Iv. Principle of single-SPA

** Single-SPA is a state machine. The framework is only responsible for maintaining the state of each sub-application. How to load, mount and uninstall sub-applications are controlled by the sub-application itself, so single-SPA framework has good scalability. ** We can check the clone source code on github website of single-SPA. The functional source code of Sinlge-SPA is mainly concentrated in the SRC directory. The main functions of each file in the SRC directory are summarized as follows

Once we have an overview of the source directory and functionality, we’ll go back and explore some of the apis and functionality in Part 2. In some code comments, we omit some parameters verification that does not affect the understanding of the principle.

1. What does the registerApplication do

The registerApplication registration method is defined in the file/SRC/Applications /app.js with the code and comments shown below. It does three main things:

  • SanitizeArguments: Normalize parameters to make sure that the parameters registered with each new child app are valid;

  • Apps. push: Add the registered child apps to the array of apps, and add internal attributes for each child app. For example, the very important attribute status marks the status of the child app.

  • Reroute: This method will be covered in the next section, for now we know that the method internally determines if it is registered to load the resources of the child application (loadApps method) and adds the relevant lifecycle hooks to the child application’s app (toLoadPromise method) when the load is complete.

(1) The code of the registerApplication registration method is as follows:

export function registerApplication( appNameOrConfig, appOrLoadApp, activeWhen, customProps ) { // hb: Format the application configuration parameters passed in by the user, Make sure the parameters passed in are valid const registration = sanitizeArguments(appNameOrConfig, appOrLoadApp, activeWhen, customProps); If (getAppNames().indexof (registration.name)! == -1) throw Error( formatErrorMessage( 21, __DEV__ && `There is already an app registered with name ${registration.name}`, registration.name ) ); Push (assign({loadErrorTime: null, status: NOT_LOADED, system: {}, devtools; // Store the configuration information of each application in the app array. Apps (push({loadErrorTime: null, status: NOT_LOADED, system: {}, devtools) { overlays: { options: {}, selectors: [], }, }, }, registration ) ); if (isInBrowser) { ensureJQuerySupport(); reroute(); }}Copy the code

(2) The code of loadApps method is as follows:

Function loadApps() {return promise.resolve (). Then () => {// hb: Const loadPromises = Appstoload.map (toLoadPromise); Console. log(' Check loadPromises :', loadPromises); return ( Promise.all(loadPromises) .then(callAllEventListeners) // there are no mounted apps, before start() is called, so we always return [] .then(() => []) .catch((err) => { callAllEventListeners(); throw err; })); }); }Copy the code

(3) toLoadPromise method core code is as follows:

export function toLoadPromise(app) { return Promise.resolve().then(() => { if (app.loadPromise) { // hb: Return app.loadPromise; } if (app.status ! == NOT_LOADED && app.status ! == LOAD_ERROR) { return app; } app.status = LOADING_SOURCE_CODE; let appOpts, isUserErr; return (app.loadPromise = Promise.resolve() .then(() => { // hb: LoadApp => system.import ('navbar'), // So the child application is actually loaded by the user's own loading method, even if the System. Import is not used; // getProps(app) is used for this function. const loadPromise = app.loadApp(getProps(app)); // hb: The child application must export an object with three life cycles: bootstrap, mount, and unmount Return loadPromise. Then ((val) => {app.loadErrorTime = null; appOpts = val; app.status = NOT_BOOTSTRAPPED; // hb: Mount lifecycle methods on app objects. Each method takes a props as a parameter and internally executes the lifecycle functions exported by the child application. And make sure the life cycle function returns a promise app. Bootstrap = flattenFnArray(appOpts, "bootstrap"); app.mount = flattenFnArray(appOpts, "mount"); app.unmount = flattenFnArray(appOpts, "unmount"); app.unload = flattenFnArray(appOpts, "unload"); app.timeouts = ensureValidAppTimeouts(appOpts.timeouts); // hb: The subapp has been successfully loaded. Delete app.loadPromise. return app; }); }). Catch ((err) => {// hb: failed to load the delete app. LoadPromise; let newStatus; if (isUserErr) { newStatus = SKIP_BECAUSE_BROKEN; } else { newStatus = LOAD_ERROR; app.loadErrorTime = new Date().getTime(); } handleAppError(err, app, newStatus); return app; })); }); }Copy the code

2. What does the start method do

As we already know from the previous section, registerApplication will download the child application that is registered and whose URL path matches. In other words, if only registered, resources matching the child application will be downloaded, but no initialization or rendering will be performed. The start method is used to initialize and render the child application, as shown in the following code. We can see that the reroute method is mainly called in the start method. Reroute distinguishes between before and after the start method, and if before the start method, as explained in the previous section, Download sub-application resources when registering; If it is start, the performAppChanges method is called to perform operations on child applications in different states

  • AppsToUnload // Remove the application to be removed

  • AppsToUnmount // Unmount the application to be uninstalled

  • AppsToLoad // The application that needs to be loaded is loaded

  • AppsToMount // Mount the application to be mounted

(1) Start method code

Export function start(opts) {started = true; // hb: The application is loaded before calling start, but not initialized, mounted, or unmounted. if (opts && opts.urlRerouteOnly) { setUrlRerouteOnly(opts.urlRerouteOnly); } if (isInBrowser) { reroute(); }}Copy the code

(2) Reroute method

export function reroute(pendingPromises = [], eventArguments) { const { appsToUnload, // hb: AppsToUnmount that needs to be removed, // hb: appsToLoad that needs to be unloaded, // hb: appsToMount that needs to be loaded, // hb: appsToMount that needs to be mounted} = getAppChanges(); let appsThatChanged; Hb: whether to call isStarted if (isStarted()) {appChangeconspicuous = true; appsThatChanged = appsToUnload.concat( appsToLoad, appsToUnmount, appsToMount ); return performAppChanges(); } else { appsThatChanged = appsToLoad; return loadApps(); Function loadApps() {return promise.resolve ().then() => {// hb: Const loadPromises = Appstoload.map (toLoadPromise); return ( Promise.all(loadPromises) .then(callAllEventListeners) // there are no mounted apps, before start() is called, so we always return [] .then(() => []) .catch((err) => { callAllEventListeners(); throw err; })); }); } function performAppChanges() { return Promise.resolve().then(() => { // hb: Unload Life Cycle (Unload Life Cycle) const unloadPromises = AppstounLoad. map(toUnloadPromise); // hb: Const unmountUnloadPromises = Appstounmount.map (toUnmountPromise). Map ((unmountPromise) => unmountPromise.then(toUnloadPromise)); const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises); const unmountAllPromise = Promise.all(allUnmountPromises); unmountAllPromise.then(() => { window.dispatchEvent( new CustomEvent( "single-spa:before-mount-routing-event", getCustomEventDetail(true) ) ); }); // hb: Const loadThenMountPromises = Appstoload.map ((app) => {return toLoadPromise(app). Then ((app) => tryToBootstrapAndMount(app, unmountAllPromise) ); }); // hb: Const mountPromises = Appstomount. filter((appToMount) => Appstoload.indexof (appToMount) < 0).map((appToMount)  => { return tryToBootstrapAndMount(appToMount, unmountAllPromise); }); return unmountAllPromise .catch((err) => { callAllEventListeners(); throw err; }) .then(() => {}); }); }}Copy the code

3. The execution time of the life cycle function exported by the subapplication

Bootstrap /mount/unmount: Bootstrap /mount/unmount: bootstrap/mount/unmount: bootstrap/mount/unmount: bootstrap/mount/unmount In fact, after downloading the child app resources, it will add the life cycle function of the child app to the property of the app (in single-SPA, each child app is an app object, and then summarized into an array of apps). Then, single-SPA will execute its life cycle function corresponding to the status update of the child app.

(1) Load the code that handles the child application lifecycle method in the method

export function toLoadPromise(app) { app.status = NOT_BOOTSTRAPPED; // hb: Mount lifecycle methods on app objects. Each method takes a props as a parameter and internally executes the lifecycle functions exported by the child application. And make sure the life cycle function returns a promise app. Bootstrap = flattenFnArray(appOpts, "bootstrap"); app.mount = flattenFnArray(appOpts, "mount"); app.unmount = flattenFnArray(appOpts, "unmount"); app.unload = flattenFnArray(appOpts, "unload"); app.timeouts = ensureValidAppTimeouts(appOpts.timeouts); }); })Copy the code

(2) a flattener fnarray method

// hb: Returns a function that takes props, which is responsible for executing the lifecycle function in the child application, // Make sure that the life cycle function returns a promise export function fnarray (appOrParcel, lifecycle) { let fns = appOrParcel[lifecycle] || []; fns = Array.isArray(fns) ? fns : [fns]; if (fns.length === 0) { fns = [() => Promise.resolve()]; } return function (props) { return fns.reduce((resultPromise, fn, index) => { return resultPromise.then(() => { const thisPromise = fn(props); }); }, Promise.resolve()); }; }Copy the code

4. Sub-application switching

Our example shows that our base app can mount/uninstall our child app based on different urls without refreshing the page. So how does single-SPA achieve URL routing matching? The hashChange event is used to listen for changes in hash routes, and the popState event is used to listen for changes in history routes. Its single – spa url routing matching principle is the same (the base API, is can also turn into flowers to), in/SRC/navigation/navigation – events. Js file defined in the relevant operation.

  window.addEventListener("hashchange", urlReroute);
  window.addEventListener("popstate", urlReroute);
Copy the code

And we are in the registration application, traverse to the child as a parameter to the incoming based on routing matching rules, single – spa in/SRC/applications/app. Helpers. Use this incoming route matching in js judgment conditions to determine whether the child application should be in an active state (mount), The relevant code is as follows, where app.activeWhen is the matching function of the incoming route. At this point, we have a pretty good idea of how single-SPA matches the appropriate child application when the URL is switched.

Export function shouldBeActive(app) {try {return app.activeWhen(window.location); } catch (err) { handleAppError(err, app, SKIP_BECAUSE_BROKEN); return false; }}Copy the code

5. How to mount/uninstall sub-applications

We have already known from the above that single-SPA controls the state changes of the sub-application, such as downloading the sub-application resources and mounting/unmounting the sub-application during registration. In the third chapter, we download the sub-application through the downloading method of system. import introduced during application registration. It’s not that single-SPA has an internal way to download resources; What about mount/unmount? In fact, mount/unmount is also carried out by the corresponding life cycle function of each child application. We check the mount/unmount life cycle function of the plug-in single-SPa-vue (not single-SPA) used by the child application. You can see dom elements mounted and unmounted by the corresponding lifecycle functions. While in single-SPA, it only performs the corresponding life cycle function of the corresponding sub-application in the corresponding sub-application state. Single-spa itself only controls the state, and it does not operate itself. In this way, it can also achieve better scalability. They decide, as long as you pass in the parameters of the specification.

(1) Single-SPA-Vue mount/unload life cycle function

Function mount(opts, mountedInstances, props) { return Promise .resolve() .then(() => { const appOptions = {... opts.appOptions} if (props.domElement && ! appOptions.el) { appOptions.el = props.domElement; } if (! appOptions.el) { const htmlId = `single-spa-application:${props.name}` appOptions.el = `#${htmlId.replace(':', '\\:')} .single-spa-container` let domEl = document.getElementById(htmlId) if (! domEl) { domEl = document.createElement('div') domEl.id = htmlId document.body.appendChild(domEl) } // single-spa-vue@>=2 always REPLACES the `el` instead of appending to it. // We want domEl to stick around and not be replaced. So we tell Vue to mount // into a container div inside of the main domEl if (! domEl.querySelector('.single-spa-container')) { const singleSpaContainer = document.createElement('div') singleSpaContainer.className = 'single-spa-container' domEl.appendChild(singleSpaContainer) } mountedInstances.domEl = domEl } if (! appOptions.render && ! appOptions.template && opts.rootComponent) { appOptions.render = (h) => h(opts.rootComponent) } if (! appOptions.data) { appOptions.data = {} } appOptions.data = {... appOptions.data, ... props} mountedInstances.instance = new opts.Vue(appOptions); if (mountedInstances.instance.bind) { mountedInstances.instance = mountedInstances.instance.bind(mountedInstances.instance); Function unmount(opts, mountedInstances) { return Promise .resolve() .then(() => { mountedInstances.instance.$destroy(); mountedInstances.instance.$el.innerHTML = ''; delete mountedInstances.instance; if (mountedInstances.domEl) { mountedInstances.domEl.innerHTML = '' delete mountedInstances.domEl } }) }Copy the code

** Single-SPA is a state machine. The framework is only responsible for maintaining the state of each child application. How to load, mount, and uninstall the child application is controlled by the child application itself. As a result, the single-SPA framework has good scalability. ** Through reading this chapter, we have a deep understanding of the operation mechanism of single-SPA framework. However, as the lowest level architecture, single-SPA still has some problems in practical scenarios, as shown below. In the next chapter, we will discuss how to solve these problems.

  • Single-spa uses JS Entry as a sub-application entrance, which costs a lot to transform old projects.

  • Sub-applications have CSS style interaction;

  • Global JS pollution exists in sub-applications.

  • The single-SPA framework does not provide a mechanism for communication between sub-applications or between sub-applications and dock applications;

5. Principle of Qiankun

Qiankun is a relatively mature micro front-end framework launched by Ant Financial. Based on the secondary development of Single-SPA, it is used to transform the Web application from a single single application to multiple small front-end applications. For those who are not familiar with the framework, please visit the website of Qiankun. In this chapter, we mainly discuss how qiankun dealt with several questions raised in Section 3.

1. Sub-applications run independently

The import-html-Entry plugin used the child application’s HTML as an entry point. The framework would plug the HTML document into the main framework’s container as a child node. 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. In addition, the cost of accessing old projects as sub-applications is almost zero, and the development experience remains unchanged from that of independent development. Compared with JS Entry of Single-SPA, it is more flexible, convenient and has better experience.

2. CSS style isolation

In the micro-front-end scenario, sub-applications of different technology stacks will be integrated into the same runtime, and it is inevitable that the sub-applications will interfere with each other. There are two ideas for style isolation. The first is to use a CSS Module or BEM solution, which is essentially a convention to avoid conflicts. This solution is cheap for new projects, but costly if it involves running with older projects. The second idea is to uninstall the stylesheet at the same time as the child application uninstalls. The technical principle is that the browser will do the entire CSSOM reconstruction for all the stylesheet inserts and removers to achieve the purpose of inserting and uninstalling styles, so that only one application stylesheet is valid at a time. The Qiankun framework takes a second approach. It uses import-html-entry to obtain style information by parsing and tags in HTML entry, download style files, and insert them into the container of the main framework in the form of tags, and remove them when the child application is uninstalled. This ensures that style conflicts are avoided between different child applications.

3. Js global isolation

Js isolation is more important than style isolation. Because in the SPA scenario, problems such as memory leaks and global variable conflicts are magnified, problems in one sub-application may affect the operation of other applications. Such problems are often very difficult to troubleshoot and locate, and costly to resolve once they occur. The Qiankun framework enables a sandbox environment for each child application based on Proxy, where access to Proxy/Window object values is controlled for all child applications. The value is applied only to the updateValueMap collection inside the sandbox. The value is also taken first from the child application independent state pool (updateValueMap). If not found, the value is taken from the proxy/ Window object of the main application. This ensures that the global JS of each sub-application conflicts with each other.

4. Communication between sub-applications

In general, we divide each sub-application from the perspective of business to reduce the communication between applications as much as possible, thus simplifying the whole application and making our micro-front-end architecture more flexible and controllable. However, in some scenarios, the mutual communication between sub-applications still exists. The Qiankun framework provides Actions communication (observer mode) and internally provides an initGlobalState method for registering MicroAppStateActions instances for communication. This instance has three methods:

  • SetGlobalState: setGlobalState – when a new value is set, a shallow check is performed internally. If a globalState change is detected, a notification is triggered to notify all observer functions.

  • OnGlobalStateChange: Registers the observer function – in response to globalState changes, the observer function is triggered when globalState changes;

  • OffGlobalStateChange: Cancel the observer function – the instance no longer responds to globalState changes.

Six, summarized

This paper introduces the origin of micro front end and analyzes the necessity of micro front end. Then it summarizes some problems of iframe in aggregative applications. Then through the example of single-SPA phenomenon and API use to discuss the principle of single-SPA implementation; Finally, through the Qiankun micro-front-end framework, it discusses the problems that must be solved in the micro-front-end model in the actual scene and how to solve them. Through step by step, from understanding the micro front end, to understand the principle of the relevant framework, and then to the actual scene problem solving, so as to “understand” the whole aspect of the micro front end.

The blog github address is fengshi123, which summarizes all the author’s blogs. Welcome to follow and star ~

reference

1. Official website of Single-SPA

2, Qiankun official website

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