“This article has participated in the call for good writing activities, click to view: the back end, the big front end double track submission, 20,000 yuan prize pool waiting for you to challenge!”

I began to write this article because the big front end department of the company began to implement its own micro front end framework. In the cooperation with the big front end department, the knowledge, technical points and difficulties related to micro front end were summarized

What is the micro front end

The concept of microfront-end is derived from microservice architecture. An architectural style in which multiple front-end applications are delivered independently. Specifically, the front-end application is broken down into smaller, simpler pieces that can be developed, tested, and deployed independently, yet remain cohesive as a single product from the user’s perspective

Why have a micro front end

Our normal single application is mainly responsible for a complete business, so it is also called a single stone application (a building is made entirely of a stone sculpture).

But there are a lot of pain points as version iterations:

  • Incremental update slow
    • The more project files you have, the longer it takes to package and compile each time
    • Unmodified files need to be recompiled every time they come online (chunkhash and DLLS are not the root cause of the problem)
  • High coupling
    • Code changes have a large impact on the association
    • The sheer size of the project increases the difficulty and time for newcomers to get to know the project
  • Cannot be independently deployed: Irrelevant function modules cannot be independently deployed because they are not split
  • No team autonomy: Team self-maintenance cannot be achieved if modules are broken up into smaller teams

From the perspective of the company and users, it is not conducive to the efficiency improvement of a company’s OA, CRM, ERP, PMS and other background. There is no unified entrance, which is inconvenient to use and reduces work efficiency

From the user level, it is not conducive to user experience and traffic management. A more empowered product or application is more likely to gain users’ favor and traffic

Therefore, based on the reference of micro-service architecture, a micro-front-end architecture was born

As a solution for large-scale applications, the micro front end aims to solve the pain points mentioned above and do the following:

  • Independent technology selection: Each development team chooses its own technology stack (Vue,React,Angular,Jquery), not influenced by other teams
  • Business independence: Each deliverable can be used independently or as a single large application
  • Style isolation: No style conflicts or overwrites between parent and child applications

Technical solution

The current mainstream scheme

  • Large warehouse split into separate module folders, throughwebpackUnity to build. No changes in nature, just optimizations in project structure and compilation subcontracting.
  • The large warehouse was disassembled into smaller ones. Go through each otherlocation.hrefSwitching. It is suitable for background applications
  • Big warehouse into small warehouse, send the packagenpmAnd then integrate. The higher one goes a step further, mainly aimed atheader,footer,siderBarAnd other common part components.
  • Large warehouse into small warehouse, not through the page jump, through the way of injection into the main application
    • Iframe (natural micro front-end solution, but with many drawbacks)
    • single-spa
    • Web Components (best but least compatible)

The trend is toward technical solutions that inject integration

Advantages and disadvantages of iframe

The advantages of the iframe

  • Browser native hard isolation solution, low transformation cost
  • Natural supportCSSIsolation,JSisolation

The problem of the iframe

  • The URL is not synchronized
    • iframeInternal page jump,urlDon’t update
    • Browser refresh resultsiframe urlStatus lost, back forward button unavailable.
  • The UI is not synchronized
    • DOMThe structure is not shared.iframeThe popover mask in popover cannot be overwritten across the entire parent application
  • The global context is completely isolated and memory variables are not shared.iframeInternal and external system communication, data synchronization and other requirements, the main applicationcookieTo be transparent to the root domain name are different in the sub-application to achieve the effect of no login.
  • Slow. Each child application entry is a process of browser context reconstruction and resource reloading.
  • Double the scroll bar

Overall, iframe is not suitable as a micro front-end solution, and can only be used as a transitional solution at most

Technical point

Entry mode

Entry Is used by the parent application to import resource files (including JS and CSS) corresponding to the child application. There are two methods:

  • JS Entry
  • HTML Entry

JS Entry way

JS Entry works as follows:

  1. theCSSPacked intoJS, generate amanifest.jsonThe configuration file
  2. manifest.jsonIndicates the relative path address of the subapplication resource file
  3. Main application through insertscriptThe labelsrcProperty to load the subapplication resource file (subapplication domain name +manifest.jsonRelative path address in

Based on this principle, JS Entry is therefore flawed:

  • When packaging, additional modifications to the engineered code are required to generate a resource configuration filemanifest.jsonLoad for the main application
  • When packaging, you need to make additional changes to the style packaging, you need to putCSSPacked intoJSIn, the compiled package size is also increased
  • When packing, not inhtmlInsert into the linescriptThe code. becausemanifest.jsonCan only store address paths. So ban itwebpackType in the configuration code directlyhtml
// vue-cli 3.x vue.config.js
config.optimization.runtimeChunk('single') // Cannot be used
Copy the code
  • The parent application loads the child application because the domain names of the parent application are inconsistentmanifest.jsonCross domains occur and require additional processing

HTML Entry way

The HTML Entry function uses import-html-entry to directly obtain the HTML file of the child application. The resources in the HTML file are parsed and loaded into the master application. The first step is to parse the remote HTML file and obtain an object

/ / use
import importHTML from 'import-html-entry'
importHTML(url, opts = {})

// Get the object
{
    templateThe link and script tags are commented out.scripts: [HTTP address of the script or {async: true.src: xx} or code block],styles: [style HTTP address],entryThe address of the entry script is SRC of the script marked with entry, or SRC of the last script tag.Copy the code

The second step is to process the object and expose a Promise object that returns the value below

// Import-html-entry source code to obtain the object processing
{
     // template is the template after link is replaced with style
    template: embedHTML,
    // Static resource address
    assetPublicPath,
    // Get the external script, and finally get the code content of all the scripts
    getExternalScripts: () = > getExternalScripts(scripts, fetch),
    // Get the contents of the external style file
    getExternalStyleSheets: () = > getExternalStyleSheets(styles, fetch),
    // A script executor that lets JS code (scripts) run in the specified context
    execScripts: (proxy, strictGlobal) = > {
        if(! scripts.length) {return Promise.resolve();
        }
        returnexecScripts(entry, scripts, proxy, { fetch, strictGlobal }); }}Copy the code

What does getExternalStyleSheets do? GetExternalStyleSheets does two things

  1. SubapplicationlinkThe label tostyleThe label
  2. The correspondinghrefRemote file content passesfetch getIn the way ofstyleIn the label
    • If it isinline styleThrough thesubstringThe way to get inlinestyleCode string
    • If it isRemote styleThrough thefetch getWay to obtainhrefThe code string corresponding to the address
// import-html-entry getExternalStyleSheets source code
export function getExternalStyleSheets(styles, fetch = defaultFetch) {
    return Promise.all(styles.map(styleLink= > {
        if (isInlineCode(styleLink)) {
            // if it is inline style
            return getInlineCode(styleLink);
        } else {
            // external styles
            return styleCache[styleLink] || (styleCache[styleLink] = fetch(styleLink).then(response= >response.text())); }}}))Copy the code

What does getExternalScripts do? GetExternalScripts also does two things

  1. Get the child applications in orderhtmlIn thescript“And put together ascriptsAn array of
  2. usefetch getThe way the loop loadsscriptsAn array of
    • If it isinline scriptThrough thesubstringThe way to get inlineJSCode string
    • If it isRemote scriptThrough thefetch getWay to obtainsrcThe code string corresponding to the address

Finally, it returns a scriptsText array, where each element is a string of executable code in the subapplication scripts array, which is the actual parameter execScripts uses

Here are some problems:

  1. Cross domain

Parent application FETCH The CDN file of the third-party library of the child application. Most CDN sites support CORS cross-domain, but a few CDN sites do not. As a result, the cross-domain FETCH file fails

  1. reloading

Some common CDN files are loaded by both parent and child applications. When the parent application loads the child application, the JS code executing this part of CDN will be loaded repeatedly, leading to errors

Solution: Directly hardcode the CDN script to be loaded into the HTML of the parent application. The parent application directly loads all the CDN required by the parent application. The child application determines whether to run independently by identifying whether to load the CDN file through the micro front end

The advantage of this scheme is that the parent application does not need to make logical judgment of repeated loading, while the child application makes its own judgment. The corresponding disadvantage is that the CDN of the child application of B, which does not need to be used by the child application of A, is also loaded at the first time, which consumes performance

What does execScripts do? ExecScripts is the function that actually executes the child application’s JS files

  1. First callgetExternalScriptsGets executableJSCode array
  2. Finally usingevalExecute in the current contextJSThe code.
  3. proxyThe parameter supports passing in a context object, thus ensuring thatJS sandboxThe feasibility of

HTML Entry is better than JS Entry

  1. You don’t have to generate extramanifest.json
  2. Don’t put thecssPacked intojs
  3. globalcssIndependent packaging, no redundancy
  4. Not using buildscriptInsert into the child applicationJSCode, will not generate additionalDOMnode

JS sandbox

The purpose of a JS sandbox is to isolate two child applications from each other. There are two ways to implement a JS sandbox

  • Proxy sandbox: leverageproxy API, can realize multi-application sandbox, different applications corresponding to different agents
  • Snapshot sandbox: Save the differences between different sandboxes. There are only two sandboxes

Agent sandbox

  • Get attributes:proxyObj[key] || window[key]
  • Set properties:proxyObj[key] = value

Use the function scope parameter window (argument proxyObj) instead of the global object window

/ / proxy demo
class ProxySandbox {
    constructor() {
        const rawWindow = window;
        const fakeWindow = {}
        const proxy = new Proxy(fakeWindow, {
            set(target, p, value) {
                target[p] = value;
                return true
            },
            get(target, p) {
                returntarget[p] || rawWindow[p]; }})this.proxy = proxy
    }
}
let sandbox1 = new ProxySandbox();
let sandbox2 = new ProxySandbox();
window.a = 1;
((window) = > {
    window.a = 'hello';
    console.log(window.a)
})(sandbox1.proxy);
((window) = > {
    window.a = 'world';
    console.log(window.a)
})(sandbox2.proxy);
Copy the code

The snapshot sandbox

When the sandbox is deactivated, assign the modified record on the Window to modifyPropsMap and wait for the next sandbox activation to become a windowSnapshot of the current Window. Assigning modifyPropsMap, the Window modification object recorded on the sandbox, to the Window sandbox actually uses the global Window object

/ / the snapshot of the demo
class SnapshotSandbox {
    constructor() {
        this.windowSnapshot = {}; // Window state snapshot
        this.modifyPropsMap = {}; // The window property modified while the sandbox is running
        this.active();
    }
    
    / / activation
    active() {
        // Set a snapshot
        this.windowSnapshot = {};
        for (const prop in window) {
            if (window.hasOwnProperty(prop)) {
                this.windowSnapshot[prop] = window[prop]; }}// Restore the last recorded environment of the sandbox
        Object.keys(this.modifyPropsMap).forEach(p= > {
            window[p] = this.modifyPropsMap[p]
        })
    }
    
    / / the deactivation
    inactive() {
        // Record this change
        // Restore the window to its state before activation
        this.modifyPropsMap = {};
        for (const prop in window) {
            if (window.hasOwnProperty(prop) && this.windowSnapshot[prop] ! = =window[prop]) {
                this.modifyPropsMap[prop] = window[prop]; // Save the changes
                window[prop] = this.windowSnapshot[prop] // change back to the original}}}}let sandbox = new SnapshotSandbox();
((window) = > {
    window.a = 1
    window.b = 2
    console.log(window.a) / / 1
    sandbox.inactive() / / the deactivation
    console.log(window.a) //undefined
    sandbox.active() / / activation
    console.log(window.a) / / 1
})(sandbox.proxy);
/ / the sandbox. The proxy is the window
Copy the code

The current mainstream approach is proxy sandbox first, or snapshot sandbox if the Proxy API is not supported

CSS sandbox

Subapplication style

The child application is isolated by BEM + CSS Module to ensure that the style of child application A does not take effect on the DOM of child application B

Subapplication switch

The child application is inactivated. The style style does not need to be removed because the isolation has been done to reactivate the loaded child application, nor does it need to reinsert the style tag to avoid repeated loading

Parent-child application communication

Parent-child application communication is divided into data and events

data

  • url
  • localStorage
  • sessionStorage
  • cookie
  • eventBus

The event

  • The child application main.js exports to the parent application’s window object
  • The parent applies custom events
  • The parent window. The eventBus
  • H5 api sharedWorker
  • H5 api BroadcastChannel

The most popular solutions are eventBus and custom events

Application of monitoring

Each project has its own application monitoring

  • User Behavior Monitoring
  • Monitoring errors
  • Performance monitoring

If the proxy sandbox is used, the proxy API can only handle the get set of objects, but cannot handle the monitoring and removal of events. The child application monitoring cannot be performed on the proxy object, so the parent application can only listen for the events of the parent application directly

If you use the snapshot sandbox, because only one child application is active at the same time, only one child application JS is executing, and you operate the Window object directly, you can consider directly using the child application’s own monitoring, because they are listening to the window event, so you can listen to the events of both the parent and the child application

The monitoring schemes of Single-SPA and Qiankun are listed below

// Exception capture for single-SPA
export { addErrorHandler, removeErrorHandler } from 'single-spa';

// Anomalous capture of Qiankun
// Listen for error and unhandlerejection events
export function addGlobalUncaughtErrorHandler(errorHandler: OnErrorEventHandlerNonNull) :void {
  window.addEventListener('error', errorHandler);
  window.addEventListener('unhandledrejection', errorHandler);
}

// Remove the error and unhandlerejection event listeners
export function removeGlobalUncaughtErrorHandler(errorHandler: (... args: any[]) => any) {
  window.removeEventListener('error', errorHandler);
  window.removeEventListener('unhandledrejection', errorHandler);
}
Copy the code

Comparison of existing frameworks

Refer to the above

single-spa

The relatively basic micro front-end framework is also the choice of our company’s large front-end department to build its own framework. More parts need to be customized, including

  • Entry mode
  • JS sandbox
  • CSS sandbox
  • Parent-child application communication mode
  • Application monitoring event handling

Single-spa.js.org/ github: github.com/single-spa/…

icestark

Icestark is the micro front-end framework of Alibaba. Currently, there are two different access methods for the React main application and the main application with unlimited framework

PS: According to the following reference description, the coexistence of multiple subapplications should not be supported at present (to be confirmed)

In general, there are no multiple microapplications running at the same time

There is only one microapplication at a time when a page is running, so there is no contamination of styles between multiple microapplications

In Entry mode

  • throughfetch + Create script tagIn the way of injection. There’s a little bit of a transition between JS Entry and HTML Entry
  • The child application does not need to generate a configuration file, but it doesscriptDOMnode

On the JS sandbox

  • If it is an uncontrollable sub-application, it is recommended to use iframe scheme embedding
  • If it is a controlled child application, use proxy sandboxes (haven’t studied the source code yet, but snapshot sandboxes should also be used as a demotion strategy, waiting to be confirmed)

On the CSS sandbox

  • The main scheme isBEM + CSS Modules
  • The experimental solution isShadow DOM
  • Global style library, for examplenormalize.css,reset.cssUnification is introduced by the main application

In application communication

  • Using theeventBusTo deal withdataandThe event

In application monitoring

  • It is monitored by the master application

Website: Micro-Frondos.ice. work/ github: github.com/ice-lab/ice…

qiankun

Also as ali’s micro front-end framework, Qiankun has made some constraints and sandbox capabilities on the construction level for single-SPA’s one-layer packaging core, which supports the coexistence of multiple sub-applications, but the modification cost of access is relatively high. In general, it is the preferred micro front-end framework at present

In Entry mode

  • Has supportedHTML EntryIs also dependent within the frameworkimport-html-entry

On the JS sandbox

  • Use three sandboxes
    • legacySandBoxSupport:proxy APIAnd only monad applications coexist
    • proxySandBoxSupport:proxy APIAnd many sub-applications coexist
    • snapshotSandBox: don’t supportproxy APISnapshot sandbox
  • legacySandBoxIs actuallyproxySandBoxsnapshotSandBoxThe combination of both wantsproxyAgent capability, and want to have direct operation to a certain extentwindowObject capabilities

On the CSS sandbox

  • The main scheme isBEM
  • BEMIt does not need to be handled by the child application itself, but accessed by the child applicationqiankunFrame can be added uniformly through configurationprefix
  • Global style library, for examplenormalize.css,reset.cssUnification is introduced by the main application

In application communication

  • ActionsSolution: Suitable for less communication
    • Data: Use a store to store data, use observer mode to listen
    • On events: Trigger event communication that uses the observer to dispatch events
  • SharedSolution: Suitable for more communication
    • Main application based onreduxTo maintain a state pool, passsharedExamples expose methods for child applications to use
    • A sub-application needs to be maintained separatelysharedExamples to ensure consistency in usage and performance
      • The standalone runtime uses its ownsharedThe instance
      • Used in the master application when embedded in the master applicationsharedThe instance
    • Both data and events can pass throughreduxTo communication

In application monitoring

  • It is monitored by the master application

qiankun.umijs.org/zh github: github.com/umijs/qiank…

Garfish

The solution we saw at the developer conference, from ByteDance, promises to be the best solution

  • Supports the coexistence of multiple sub-applications
  • supportHTML Entry,JS Entry
  • JSThe sandbox directly uses the snapshot sandbox
  • Through the HTML overall snapshot, to achieveCSSsandbox
  • communication
    • Data: Also through onestoreTo save the data
    • Event: Customize events
  • monitoring
    • keepwindow addEventListener removeEventListenerA copy of the
    • In the sandboxdocumentObject listening monitoring

The biggest feature is that it can snapshot the DOM node of the child application, keep the DOM tree plus JS sandbox, CSS sandbox, can maintain the integrity of the entire child application

Garfish. dev/ github: github.com/bytedance/g…