preface

Unlike architectural solutions, technology can appear out of nowhere and explode without warning. However, the solution or architecture must be to solve a problem, before practice, please be sure to find out whether it can solve the current problem, and whether the research is suitable for the team, considering the engineering value and product value, please do not blindly pursue.

The original address

Micro front-end

Those familiar with it prefer to call it front-end microservices.

define

“A microfront-end is an architectural style in which a number of independently delivered front-end applications are combined into a large whole.”

Why?

In the traditional mode development, such as Ali Cloud, Tencent cloud console. It is difficult to maintain a large mid-background and iterate quickly because they have several common problems.

  • The stack is too old and the application is not maintainable. Imagine that your oldest project in your company suddenly asks you to add features. It’s ok to use jQuery, but you use Angular1 or even Java Web.
  • Because of its huge size, Frontend Monolith transforms from a common application into a Monolith application, the 10W+ line of code ancestral project compiles, even if the DLL is removed, the main package takes at least 5M, and the compilation is slow and the development experience is terrible.
  • Single technology stack cannot meet business needs. Each frame has its advantages. Isn’t it beautiful to choose its advantages and use them?
  • The cost of reconstruction is high, and the step reconstruction is not possible, that is, only one module is reconstructed at a time, and the stability of the existing version is not affected. You can only release all modules at once, which is risky.

Is there a solution to these problems?

Using the design idea of server-side microservice for reference, front-end microservitization appears. It can’t solve everything, but it can minimize the burden and risk. Its implementation is more like turning the entire project into a “component” that the platform can freely assemble. In short, a single application becomes an aggregation of many small front-end applications.

Before microservitization

What problem was solved

Module complexity is controllable, and the team is independent and autonomous

Each module (microservice) is fully controlled by a development team, easy to manage and maintain, and quickly integrate the business. While it may lead to more fragmentation of teams, it does more good than harm if kept to a reasonable level.

Independent development and deployment, independent sub-warehouse

Like microservices, each module has the ability to run independently, which means it can be deployed independently. Reduce risk by gradually reducing coverage per deployment.

More scalable, incremental upgrades

Year old large front application technology stack master and technical personnel are not on the job, time to rewrite the whole front end application one-time rewrite the whole application risk is too big, can to rewrite, the style of the incremental upgrades, iteration, a little to replace the old application, at the same time on the premise of not affected by monomer architecture drag to provide new features. And theoretically it can support unlimited expansion of large single-page applications. Although it does not have the natural advantages of SPA applications, it also gets rid of the strongly coupled application technology stack.

Technology stack independent, innovation independent

The main framework does not limit the stack of technologies that can be accessed from an application, giving us full autonomy if we want to try out new technologies or better implementations based on performance.

The existing micro front-end solutions are as follows:

  • single-spa
  • icestark
  • qiankun
  • widget
  • magix
  • Luigi
  • Ara Framework
  • ucf-web

What kind of scene

The answer is obvious: prepare for heirloom projects.

There is no reason for a single team to adopt a microfront end, nor for applications that need to be developed quickly, or for small applications with small granularity.

HOW DO

But it also faces some problems and challenges.

  • How to render multiple stacks in one page.
  • How modules of different technology stacks communicate with each other.
  • How to combine the routing of different technology stacks and make it trigger correctly; Hash and history mode processing
  • Application loading and lifecycle management.
  • How to isolate applications, namely sandbox applications.
  • Consider how each project is packaged and merged together in the case of package optimization.
  • How to conduct business development after microservitization.
  • How multiple teams should collaborate.

How to render multiple stacks in one page.

Build-time integration

Single-spa it helps us use multiple frameworks (React, Vue, AngularJS, Svelte, Ember, etc.) on the same page. In addition, the code of each independent module can be loaded on demand and run independently. Its working mechanism is to activate the corresponding entry application when the prefix is hit.

Use registerApplication to register the application and sign it

AppName: string Application name applicationOrLoadingFn:(a)= > <Function | Promise> must be a load function that returns either the loaded application or onePromise. activityFn:(location) = >Boolean must be pure functions. This function useswindow.location as the first parameter returns the value of the status when the application is active. customProps? :Object= {} props will be passed to the application during each lifecycle method.Copy the code

It is finally started with singlespa.start ().

import * as singleSpa from 'single-spa';
const appName = 'reactapp';
// Load the React application entry file
const loadingFunction = (a)= > import('./react/app.js');

True if the current route is /reactapp
const activityFunction = location= > location.pathname.startsWith('/reactapp');

// Register the application
singleSpa.registerApplication(appName, loadingFunction, activityFunction ,{ token: 'xxx'});

/ / start single - spa
singleSpa.start();
Copy the code

Single-spa has four built-in lifecycle hooks: Bootstrap, mount, unmount and Unload. Each lifecycle must return a Promise or asyncFunction.

// app1.js
let domEl;
const gen = (a)= > Promise.resolve();

export function bootstrap(props) {
    return gen().then((a)= > {
          // It is called once during the first installation, when the route hits
            domEl = document.createElement('div');
            domEl.id = 'app1';
            document.body.appendChild(domEl);
            console.log('app1 is bootstrapped! ')}); }export function unload(props) {
  return gen().then((a)= > {
      // Uninstall the registered application, which can be interpreted as deletion, will only be triggered by an active call to unloadApplication, corresponding to bootstrap
      console.log('app1 is unloaded! ');
    });
}

export function mount(props) {
    return gen().then((a)= > {
          // mounted Component
            domEl.textContent = 'App1.js mounted'
            console.log('app1 is mounted! ')}); }export function unmount(props) {
    return gen().then((a)= > {
          // unmounted Component
            domEl.textContent = ' ';
            console.log('app1 is unmounted! ')})}Copy the code

This is a simple application. React, Angular, Vue, svelte, etc.

Start by defining the HTML structure

<div class="micro-container"> <div class="navbar"> <ul> <a onclick="singleSpaNavigate('/react')"> <li>React App</li> </a> <a onclick="singleSpaNavigate('/vue')"> <li>Vue App</li> </a> <a onclick="singleSpaNavigate('/svelte')"> <li>Svelte  App</li> </a> <a onclick="singleSpaNavigate('/angular')"> <li>Angular App</li> </a> </ul> </div> <div id="container"> <div id="react-app"></div> <div id="vue-app"></div> <div id="angular-app"></div> <div id="svelte-app"></div> </div> </div>Copy the code

The div. Micro-container above is called the container application. In addition to one container application, each page may contain multiple micro-frontend applications. The singleSpaNavigate method is a built-in Navigation Api for single-SPA that performs URL Navigation between registered applications without having to deal with event.preventDefault pushState etc. Then you define entry, and here is the pseudo-structure.

├ ─. Babelrc ├ ─ assets │ └ ─ styles ├ ─ index. The HTML ├ ─ package. The json ├ ─ SRC │ ├ ─ presents │ │ ├ ─ app. Js │ │ ├ ─ root.com ponent. Ts │ │ ├ ─ components │ │ └ ─ routes │ ├ ─ baseApplication │ │ └ ─ index, js// register Application│ ├ ─ the react │ │ ├ ─ app. Js │ │ ├ ─ components │ │ ├ ─ root.com ponent. Js │ │ └ ─ routes │ ├ ─ svelte │ │ ├ ─ app. Js │ │ ├ ─ components │ │ ├ ─ root.com ponent. Svelte │ │ └ ─ routes │ └ ─ vue │ ├ ─ app. Js │ ├ ─ components │ ├ ─ root.com ponent. Vue │ └ ─ routes ├ ─ tsconfig. Json └ ─ webpack. Config. JsCopy the code
// src/baseApplication/index.js
import * as singleSpa from 'single-spa';

singleSpa.registerApplication('react', () = >import ('.. /react/app.js'), pathPrefix('/react'));
singleSpa.registerApplication('vue', () = >import ('.. /vue/app.js'), pathPrefix('/vue'));
singleSpa.registerApplication('angular', () = >import ('.. /angular/app.js'), pathPrefix('/angular'));
singleSpa.registerApplication('svelte', () = >import ('.. /svelte/app.js'), pathPrefix('/svelte'));

singleSpa.start();

function pathPrefix(prefix) {
  return function(location) {
    return location.pathname.startsWith(`${prefix}`); }}Copy the code

Take React and Vue as an example. After the application is imported, boostrap and mount and unmount will be executed.

// src/vue/app.js
import Vue from 'vue/dist/vue.min.js';
import singleSpaVue from 'single-spa-vue';
import router from './router';
import Loading from './Loading';

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    router,
    el:'#vue-app'.template: ` 
      
`
.loadRootComponent: Loading }, }); export const bootstrap = (props) = > { console.log('vue-app is bootstrap') return vueLifecycles.bootstrap(props); } export const mount = (props) = > { console.log('vue-app is Mounted') return vueLifecycles.mount(props); } export const unmount = (props) = > { console.log('vue-app is unMounted') return vueLifecycles.unmount(props); } Copy the code

Bootstrap and mount these hooks.

// src/react/app.js
import React from 'react';
import ReactDOM from 'react-dom';
import singleSpaReact from 'single-spa-react';
import Root from '@React/root.component.js';

const reactLifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: Root,
  domElementGetter: (a)= > document.getElementById('react-app') Getter / / node
});
/ /... other
Copy the code
// src/svelte/app.js
import singleSpaSvelte from 'single-spa-svelte';
import AppComponent from './root.component.svelte';

const svelteLifecycles = singleSpaSvelte({
  component: AppComponent,
  domElementGetter: (a)= > document.getElementById('svelte-app')});/ /... other
Copy the code

Single-spa already provides a docking library for most major frameworks and ADAPTS them internally, injecting entry baseApplication and common-dependencies into HTML and placing them in common dependencies if only a single version is needed.

// webpack.config.js
entry: {
    'baseApplication': 'src/baseApplication/index.js'.'common-dependencies': [
      'core-js/client/shim.min.js'.'@angular/common'.'@angular/compiler'.'@angular/core'.'@angular/platform-browser-dynamic'.'@angular/router'.'reflect-metadata'.'react'.'react-dom'.'react-router'.'react-router-dom'."vue"."vue-router"."svelte"."svelte-routing"],},plugins: [new HTMLWebpackPlugin({
      template: path.resolve('index.html'),
      inject: true.chunksSortMode: 'none'}),]Copy the code

The source code has been uploaded to Github

With the Events Hooks provided by single-SPA, you can implement LiftCycle Hooks for child applications. In order to avoid variable contamination in child applications such as boostrap and unmount, do things like global variable freezing.

window.addEventListener('single-spa:before-routing-event',evt => {
    'Route Event before the Event occurs (after hashchange, PopState, or triggerAppChange)';
});

'single-spa:routing-event'= >'Route Event triggered after Event'

'single-spa:app-change'= >'app change'

'single-spa:no-app-change'= >'As opposed to app-change, trigger app nochange'

'single-spa:before-first-mount'= >'Before mounting the first application'

'single-spa:first-mount'= >'After mounting the first application'
Copy the code

Create a setDefaultMountedApp method that specifies the default mounted App.

function setDefaultMountedApp(path) {
  window.addEventListener(`single-spa:no-app-change`, () = > {const activedApps = getMountedApps()
    if (activedApps.length === 0) {
      navigateToUrl(path)
    }
  }, {
    once: true})}Copy the code

Single-spa is used here to avoid the child application 404 problem of refreshing the page. We succeeded in routing from application to application. It seems to have achieved the desired effect. Has the previous problem really been solved?

Pack the result

As the business gets more complex, the component library and other libraries in the project quickly become a “snowball effect”. The increase in the size of the dependency package means that FCP(First Contentful Paint) becomes longer. Even if multiple technology stacks are rendered in one page, FCP(First Contentful Paint) becomes longer. Its fundamental significance is still large overall application, poor decoupling, can not be deployed independently, the application is not isolated, once an application crash will still cause the overall application crash. So the problem is still there, but in a different form,

This approach is called build-time integration, and it typically generates a deployable Javascript package, although we can remove duplicate dependencies from various applications. But this meant that we had to recompile and publish all the microfrontend whenever we changed any functionality of the app. This kind of step-by-step publishing process is good enough for microservices, so it’s highly recommended not to use it for a microfront-end architecture. Once you’ve achieved decoupling and independence, don’t go back in the release phase.

Back to the essence, our goal was to decouple and decouple the application, to support standalone, standalone deployment as well as integrated deployment, and to integrate the micro front end at runtime as well.

Runtime integration

In addition to using native JavaScript, runtime integration is typically implemented in three ways:

  • iframe
  • web Component
  • SystemJs

iframe

<iframe id="micro-frontend-container"></iframe>
<script type="text/javascript">
  const microFrontendsByRoute = {
    '/': 'https://browse.example.com/index.html'.'/order-food': 'https://order.example.com/index.html'.'/user-profile': 'https://profile.example.com/index.html'};const iframe = document.getElementById('micro-frontend-container');
  iframe.src = microFrontendsByRoute[window.location.pathname];
</script>
Copy the code

advantages

Simple and crude, with inherent sandbox, suitable for three-party service introduction.

disadvantages

SEO is poor; Slow page response; Poor flexibility; Routing deep connection is complex; Using postMessage for message communication is too intrusive; Double scroll bar; The DOM inside the iframe gets the page height; The mask cannot cover the outside; Refresh back to iframe home page and other issues. This kind of first sweet after bitter things we can not do, strongly do not recommend.

web Component

The Web Component consists of four parts,

  • Custom Elements Custom elements
  • Shadow DOM Isolation style
  • HTML templates templates
  • HTML Imports to import

Here’s a simple Demo

React, Preact, Vue, Angular all support Web Components, for example

class SearchBar extends HTMLElement {
  constructor() {
    super(a);this.mountPoint = document.createElement('div');
    this.attachShadow({ mode: 'open' }).appendChild(this.mountPoint); // Specify open mode
  }

  connectedCallback() {
    const initialValue = this.getAttribute('initialValue') | |' ';
    ReactDOM.render(<input value={initialValue} placeholder="Search..." />, this.mountPoint);
  }

  disconnectedCallback() {
    console.log(`${this.nodeName} is Remove`);
  }
}
customElements.define('search-bar', SearchBar);

// index.html
<search-bar defaultValue="field" />
Copy the code

The connectedCallback is executed when it is inserted into the DOM, which is equivalent to react.ponentdidmount. With the matching is ponentWillUnMount disconnectedCallback – React.com.

In this way, we can create app custom application components and dynamically insert them according to the route.

<script src="https://base.xxx.com/bundle.js"></script>
<script src="https://order.xxx.com/bundle.js"></script>
<script src="https://profile.xxx.com/bundle.js"></script>

<div id="root-contariner"></div>

<script type="text/javascript">
  const webComponentsByRoute = {
    '/': 'base-dashboard'.'/order-food': 'order-food'.'/user-profile': 'user-profile'};const webComponentType = webComponentsByRoute[window.location.pathname];

  const root = document.getElementById('root-contariner');
  const webComponent = document.createElement(webComponentType);
  root.appendChild(webComponent);
</script>
Copy the code

advantages

Meet all your needs

disadvantages

  1. Invasive, equivalent to rewriting all existing front-end applications, not suitable for transition.
  2. The ecology is not yet established and it takes time to build the wheel manually.
  3. Communication between components becomes difficult to manage as the business becomes more complex.
  4. Still compatibility issues, we don’t need to “ditch the car for good”.

SystemJs

SystemJs is a module loader, support AMD, CommonJS, ES6 and other formats of JS module dynamic loading. Perfect with single-spa.

Firstly, each sub-application is extracted, and the overview structure is as follows:

├ ─ cra - ts - app │ ├ ─ config │ ├ ─ images, which s │ ├ ─ package. The json │ ├ ─ public │ ├ ─ scripts │ ├ ─ SRC │ │ ├ ─ index. The CSS │ │ ├ ─ index. The TSX │ ├─ ├─ ├─ exercises, ├─ ├─ exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises/ / the navigation bar│ ├ ─ package. Json │ ├ ─ SRC │ │ ├ ─ app. Js │ │ └ ─ root.com ponent. Js │ └ ─ webpack. Config. Js ├ ─ package. The json ├ ─ portal/ / the entry│ │ ├ ─ index. The HTML ├ ─ index. The js │ ├ ─ package. The json │ └ ─ webpack. Config. Js ├ ─ the react │ ├ ─ package. The json │ ├ ─ SRC │ │ ├ ─ app. Js │ │ ├ ─ main. Js │ │ ├ ─ root.com ponent. Js │ │ ├ ─ routes │ └ ─ webpack. Config. Js ├ ─ RTS │ ├ ─ build │ ├ ─ package. The json │ ├ ─ postcss. Config. Js │ ├ ─ public │ ├ ─ SRC │ │ ├ ─ app. The TSX │ │ ├ ─ index. The TSX │ │ └ ─ views │ ├ ─ tsconfig. Json │ └ ─ types ├ ─ svelte │ ├ ─ package. Json │ ├ ─ SRC │ │ ├ ─ app. Js │ │ ├ ─ root.com ponent. Svelte │ │ └ ─ routes │ └ ─ webpack. Config. Js ├ ─ VTS │ ├ ─ Babel. Config. Js │ ├ ─ package. The json │ ├ ─ public │ ├ ─ SRC │ │ ├ ─ App. Vue │ │ ├ ─ assets │ │ ├ ─ components │ │ ├ ─ main. The ts │ │ ├ ─ registerServiceWorker. Ts │ │ ├ ─ the router │ │ ├ ─ shims - TSX. Which s │ │ ├ ─ shims - vue. Which s │ │ ├ ─ store │ │ └ ─ views │ ├ ─ tsconfig. Json └ ─ vue ├ ─ package. The json ├ ─ SRC │ ├ ─ app. Js │ ├ ─ app. Vue │ ├ ─ components │ ├ ─ main. Js │ ├ ─ root.com ponent. Vue │ ├─ ├─routes ├─ webpack.config.jsCopy the code

Eight technology stack or final version of different applications, each application can exist independently as a warehouse and management, the portal as a portal project, used for integration and register the application, the portal is also a main project, to its positioning is the resource loading frame, Nav as a navigation route, other application as a child.

Framework applications are essentially centralized widgets, and the simpler they are, the more stable they are, so don’t do any UI and business logic in Portal. You can use Portal to provide system-level public support, such as login authentication, permission management, authentication, performance monitoring, and error call stack reporting.

The portal main application code is as follows:

import { getMountedApps, registerApplication, start, navigateToUrl, getAppNames } from 'single-spa';
import SystemJS from 'systemjs/dist/system' / / 0.20.24 DEV!!!!!!

const apps = [
  { name: 'nav'.url: true.entry: '//localhost:5005/app.js'.customProps: {}}, {name: 'react'.url: '/react'.entry: '//localhost:5001/app.js'.customProps: {}}, {name: 'vue'.url: '/vue'.entry: '//localhost:5002/app.js'.customProps: {}}, {name: 'svelte'.url: '/svelte'.entry: '//localhost:5003/app.js'.customProps: {}}, {name: 'react-ts'.url: '/rts'.entry: '//localhost:5006/app.js'.customProps: {}}, {name: 'cra-ts'.url: '/crats'.entry: '//localhost:5007/app.js'.customProps: {}}, {name: 'vts'.url: '/vts'.entry: '//localhost:5008/vts/index.js'.customProps] : {}},/** * RegisterApp * @returns */
async function registerAllApps() {
  await Promise.all(apps.map(registerApp))
  await setDefaultMountedApp('/react');
  start();
}

registerAllApps();

/** * set default App * @param {*} path default app path */
function setDefaultMountedApp(path) {
  window.addEventListener(`single-spa:no-app-change`, (evt) => {
    const activedApps = getMountedApps()
    if (activedApps.length === 0 && evt.target.location.pathname === '/') {
      navigateToUrl(path)
    }
  }, {
    once: true})}/** * register App * @param {*} name App Name * @param {*} url visit Url * @param {*} entry entry file * @param {*} customProps custom Props */
function registerApp({ name, url, entry, customProps = {} }) {
  // You can pass store and user permissions, etc. through customProps
  return registerApplication(name, () => SystemJS.import(entry), pathPrefix(url), customProps);
}
Copy the code

As mentioned above, independent operation and independent deployment are still incomplete. We want to build a micro front-end architecture that meets the requirements. We want to dynamically obtain the entry of each sub-application and write it into the Portal master application, as well as route and public dependency extraction after packaging, etc.

The solution meituan uses is similar to building single-page applications with a micro front end

  • Publish the latest static resource file
  • Re-generate entry-xx.js and index.html (update entry references)
  • Restarting front-end Services

Micro front end of a single child in my ideal application should have separate as a project product online, so you need to separate the entry documents, single – spa entrance with ordinary application separation, there are many ways, such as double entry file processing, package configuration or doubles, but this is not only troublesome error prone and more complex than I imagined, It’s not just a solution problem. Imagine a single step deployment of a child application, and a login and authentication system in one of Portal’s children applications, merging the two projects into a new micro front end? It makes me laugh to think about it.

Besides, there are some problems in this scheme.

  • use@vue/cliThe route dynamic Import Component returns an HTML.
  • Older projects may involve multiple entries.
  • The style is not cleaned after the child application is uninstalled.
  • Public dependence has not been removed.
  • The Entry can only be a single JavaScript package. The JS Entry package packaged is too large to be loaded with parallel resources using code Splitting subcontracting.

Later, the method of USING HTML Entry in Qiankun was used for these problems. In {entry: ‘/ / localhost: 5001 / index. HTML’} in the form of introduction; It can easily solve most of these problems.

function render({ appContent, loading }) {
  ReactDOM.render(<Framework loading={loading} content={appContent} />, document.getElementById('container')); } render({ loading: true }); function genActiveRule(routerPrefix) => location => location.pathname.startsWith(routerPrefix); const appGroup = [ { name: 'react app', entry: '//localhost:7100', render, activeRule: genActiveRule('/react') }, { name: 'react15 app', entry: '//localhost:7101', render, activeRule: genActiveRule('/react15') }, { name: 'vue app', entry: '//localhost:7102', render, activeRule: GenActiveRule ('/vue')},] // Register application set registerMicroApps(appGroup);Copy the code

RegisterMicroApps is roughly implemented as follows

let microApps: RegistrableApp[] = [];

export function registerMicroApps<T extends object = {}>(
  apps: Array<RegistrableApp<T>>,
  lifeCycles: LifeCycles<T> = {},
  opts: RegisterMicroAppsOpts = {},
) {
  const { beforeUnmount = [], afterUnmount = [], afterMount = [], beforeMount = [], beforeLoad = [] } = lifeCycles;
  const { fetch } = opts;
  microApps = [...microApps, ...apps];

  let prevAppUnmountedDeferred: Deferred<void>;

  apps.forEach(app= > {
    const { name, entry, render, activeRule, props = {} } = app;

    registerApplication(
      name,

      async ({ name: appName }) => {
        await frameworkStartedDefer.promise;

        // Get the entry HTML template and script loader and resource Domain
        const { template: appContent, execScripts, assetPublicPath } = await importEntry(entry, { fetch });
        // Load it after uninstallation
        if (await validateSingularMode(singularMode, app)) {
          await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise);
        }
        // The first time the setup is loaded, the dom structure of the visible area is applied
        // Ensure that the container DOM structure is set before each application is loaded
        render({ appContent, loading: true });

        let jsSandbox: Window = window;
        let mountSandbox = (a)= > Promise.resolve();
        let unmountSandbox = (a)= > Promise.resolve();

        if (useJsSandbox) {
          const sandbox = genSandbox(appName, assetPublicPath);
          jsSandbox = sandbox.sandbox;
          mountSandbox = sandbox.mount;
          unmountSandbox = sandbox.unmount;
        }

        await execHooksChain(toArray(beforeLoad), app);

        // eval
        let { bootstrap: bootstrapApp, mount, unmount } = await execScripts(jsSandbox);
        / /... other
        return {
          bootstrap: [bootstrapApp],
          mount: [
            async() = > {if ((await validateSingularMode(singularMode, app)) && prevAppUnmountedDeferred) {
                return prevAppUnmountedDeferred.promise;
              }
              return undefined;
            },
            async () => execHooksChain(toArray(beforeMount), app),
            async () => render({ appContent, loading: true }),
            mountSandbox,
            mount,
            async () => render({ appContent, loading: false }),
            async () => execHooksChain(toArray(afterMount), app),
            async() = > {if (await validateSingularMode(singularMode, app)) {
                prevAppUnmountedDeferred = new Deferred<void> (); } }, ], unmount: [async () => execHooksChain(toArray(beforeUnmount), app),
            unmount,
            unmountSandbox,
            async () => execHooksChain(toArray(afterUnmount), app),
            async () => render({ appContent: ' ', loading: false }),
            async() = > {if ((awaitvalidateSingularMode(singularMode, app)) && prevAppUnmountedDeferred) { prevAppUnmountedDeferred.resolve(); }},]}; }, activeRule, props, ); }); }Copy the code

The script styles and other resources were extracted from the HTML template, and the styles were directly written into the HTML template. In the sandbox part, Qiankun used the Proxy to hijack the operation of Window and apply it to an empty dictionary. Before the bootstrap and mount life cycles, get snapshots of the global state and record them using a Map to avoid polluting global objects. In this way, when the sandbox is unmounted, there is no need to manually destroy it. It is also easy to set the default window of the script to point to the empty dictionary. Use eval to point the window to window.proxy, which is the empty dictionary.

geval(`; (function(window){;${inlineScript}\n}).bind(window.proxy)(window.proxy); `);
Copy the code

With regard to CSS isolation, as HTML Entry was rewritten, the previous inline styles naturally disappeared. In fact, there is another way to isolate CSS, which is the same as BEM. Postcss is used to set the class prefix in the sub-application, and third-party libraries are supported at the same time. As for CSS-Module, compatibility problems are not mentioned. / Manual funny

What is left then is the processing within Lifecycle.

function execHooksChain<T extends object> (hooks: Array<Lifecycle<T>>, app: RegistrableApp<T>) :Promise<any> {
  if (hooks.length) {
    return hooks.reduce((chain, hook) = > chain.then((a)= > hook(app)), Promise.resolve());
  }

  return Promise.resolve();
}
Copy the code

If JS Entry is adopted, more time and energy will be wasted to optimize. Finally, HTML Entry was adopted, almost like HTMLless.

This completely independent project solution can avoid many problems, but there is also a problem of performance optimization — common dependencies. If ten sub-applications are all on the same technology stack, then there is no relationship between dependencies on ion extraction applications when packaging, which is actually not a good solution. Packages like React, React-DOM, Svelte, Vue, etc. that take up most of the volume should have a common dependency pool, which should be mounted under the same CDN for external chain loading and introduced via extenals.

e.g.

A subapplication [email protected] +B subapplication [email protected] => [email protected]

As the revision number remains downward compatible, the problem can be fixed but the feature will not be affected. As long as the secondary version number is the same, the function will be the same if the revision number remains upward compatible. CDN cache can be used to avoid the loading of repeatedly dependent resources to the greatest extent, and methods such as resource mapping table can also be used.

Finally, cross-application communication. Most people are used to the existence of global state management libraries such as Redux. However, in order to reduce the coupling degree, we should avoid inter-application communication. Another way is to pass data and callbacks down using the Portal host application Bridge.

May some people feel that I pulled a lot of in front of the end all overthrow feelings waste of time, “know and do not know why”, can not know what is good directly used to do not know where good? What suits you is the best, but without practice, how can you know?

Finally, a case and repo written by Qiankun are attached

reference

  • Probably the most complete microfront-end solution you’ve ever seen
  • Micro Frontends
  • Front-end microservitization solution
  • Micro-front-end architecture in big front-end era: incremental upgrade, code decoupling, independent deployment