What is a micro front end?

The micro front end is to divide different functions into sub-applications according to different dimensions. These subapplications are loaded through the main application (base). The core of the micro front end lies in the demolition, demolition after the closing

Why use him?

  • How do different technology stacks work for different teams developing the same application?
  • Each team can develop and deploy independently
  • Some old application code in the project

The realization idea of micro-front-end is to divide an application into several sub-applications and package the sub-applications into lib. Load different subapplications when the path is switched. In this way, each sub-application is independent and the technology stack is not restricted! Thus, the problem of front-end collaborative development is solved

Several schemes of realizing micro front end

plan describe advantages disadvantages
Nginx route forwarding Nginx configures reverse proxy to map different paths to different applications Simple, fast and easy to configure The browser refreshes when switching apps, affecting the experience
Nested iframe The parent application is a single page, and each child application has a nested IFrame Simple implementation, the sub application between their own sandbox, natural isolation, mutual influence Frame style display and compatibility have limitations. Too simple and low
NPM package form The source code of the sub-project is distributed in the form of NPM package. Packaged build releases are still managed by the base project and integrated as packaged. The package deployment is slow and cannot be independently deployed The package deployment is slow and cannot be independently deployed
Universal center routing pedestal type Sub-projects can use different technology stacks; The sub-projects are completely independent without any dependence; The unified management is carried out by the base project, which is completed according to the registration, mounting and unmounting of DOM nodes. Unrestricted technology stack, deployed separately Communication is not flexible enough
Base type for specific center routing Use the same technology stack between sub-lines of business; Base engineering and sub-engineering can be developed and deployed separately; The sub-project has the ability to reuse the common infrastructure of the base project. Multiple communication modes are deployed separately Restricted technology stack

The current mainstream micro front-end framework

  • Single-spa Single-SPA is a JavaScript front-end solution for front-end microservitization (no style isolation itself, JS execution isolation) that implements route hijacking and application loading
  • With a more out-of-the-box API (Single-SPA + Sandbox + import-HTML-entry), technology stack independent and easy to access (as simple as iframe)

single-spa

Both the base and subprojects use the VUE technology stack. React works the same way: the base provides registration logic and the child application provides three protocol access methods and a packaging format

Base (VUE)

From the base we call the registerApplication and start methods given to us by single-SPA 1. The registerApplication parameter has four components: appNameOrConfig, appOrLoadApp, activeWhen, and customProps. This corresponds to the registered subproject name and some configuration. Fn executed when the subproject is loaded, rules executed, and parameters communicated

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import {registerApplication,start} from 'single-spa';

// Load the URL dynamically
async function loadScript(url){
  return new Promise((resolve,reject) = >{
    let script = document.createElement('script');
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script); })}// singleSpa is not flexible enough to load JS files dynamically
// Styles do not segregate without js sandbox mechanism

registerApplication('myVueApp'.async() = > {// When the match is successful, load the js of the child application
      await loadScript(`http://localhost:10000/js/chunk-vendors.js`);
      await loadScript(`http://localhost:10000/js/app.js`)
      return window.singleVue; // Subapplications are packaged in UMD format. bootstrap mount unmount
  },
  // Execute the above method when /vue is matched
  location= > location.pathname.startsWith('/vue'),// Start the application
start();


new Vue({
  router,
  render: h= > h(App)
}).$mount('#app')

Copy the code

subprojects

The most important part of the subproject is to provide three methods bootstrap, mount, unmount, and package formats

main.js

Use single-SPa-vue and React. It exports three methods by default.

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue';

const appOptions = {
    el:'#vue'.// Mount to the tag vue in the parent application
    router,
    render: h= > h(App)
}
const vueLifeCycle = singleSpaVue({
  Vue,
  appOptions
})
// In microfront-end mode, single-spa will hang a singleSpaNavigate property on the window.
// We need to change public_path to the address in the subproject.
if(window.singleSpaNavigate){
  __webpack_public_path__ = 'http://localhost:10000/'
}
// This allows subprojects to run independently
if(!window.singleSpaNavigate){
  delete appOptions.el;
  new Vue(appOptions).$mount('#app');
}

// Protocol access I set up the protocol parent application to call these methods
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;


Copy the code

Configure the packaging method of the subproject vue.config.js

module.exports = {
    configureWebpack: {output: {library:'singleVue'.libraryTarget:'umd'
        },
        devServer: {port:10000}}}Copy the code

The defects of single – spa

  • As can be seen from the above figure, the base needs to manually add JS when matching the path. If the subproject packages 10,000 JS,….
  • Styles not isolated
  • There is no JS sandbox mechanism

qiankun

As simple as accessing iframe, THE FETCH method inserts HTML directly into the container, so subprojects need to allow cross-domain access

base

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
Vue.use(ElementUI);
// Two methods are derived from Qiankun
import { registerMicroApps, start } from "qiankun";
const apps = [
  {
    name: "vueApp".// The application name
    entry: "//localhost:10000".// The HTML path to load
    container: "#vue"./ / container
    activeRule: "/vue".// Active path
  },
  {
    name: "reactApp".entry: "//localhost:20000".container: "#react".activeRule: "/react",},]; registerMicroApps(apps);// Register the application
start(); / / open

new Vue({
  router,
  render: (h) = > h(App),
}).$mount("#app");
Copy the code

subprojects

import Vue from 'vue'
import App from './App.vue'
import router from './router'

// Vue.config.productionTip = false


let instance = null
function render(props) {
    instance = new Vue({
        router,
        render: h= > h(App)
    }).$mount('#app'); // This is mounting to its OWN HTML and the base is going to take this mounted HTML and insert it into it
}
if (window.__POWERED_BY_QIANKUN__) { // Dynamically add publicPath
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) { // Run independently by default
    render();
}
// Subcomponent protocol is ok
export async function bootstrap(props) {};export async function mount(props) {
    render(props)
}
export async function unmount(props) {
    instance.$destroy();
}
Copy the code

Subprojects package configuration

module.exports = {
    devServer: {port:10000.// Cross-domain needs to be allowed
        headers: {'Access-Control-Allow-Origin':The '*'}},configureWebpack: {output: {library:'vueApp'.libraryTarget:'umd'}}}Copy the code

Application of communication

  • Data transmission is based on URL, but message transmission ability is weak
  • Implement communication based on CustomEvent
  • Communication between props master sub-applications
  • Use global variables, Redux for communication

CSS Isolation Scheme

Style isolation between child applications:

  • Dynamic Stylesheet, which removes the old application style and adds the new one when the application switches

Style isolation between primary and child applications:

  • BEM (Block Element Modifier) Specifies the project prefix
  • Css-modules package generates non-conflicting selector names
  • Shadow Dom is truly isolated

Js sandbox mechanism

  • Snapshot sandbox: Records snapshots when the sandbox is used for mounting or unmounting, and restores the environment based on the snapshot during switchover. (Multiple realtime is not supported

Example)

  • Proxy The Proxy sandbox does not affect the global environment

The snapshot sandbox

  • 1. Snapshot the current Window property during activation
  • 2. During deactivation, compare the snapshot with the current Window property
  • 3. If the property changes, save them to modifyPropsMap and restore the Window property using the snapshot
  • 4. During the second activation, take a snapshot again and restore Windows using the last modification result
class SnapshotSandbox{
        constructor(){
            this.proxy = window; 
            this.modifyPropsMap = {}; 
            this.active();
        }
        active(){ / / activation
            this.windowSnapshot = {}; / / photo
            for(const prop in window) {if(window.hasOwnProperty(prop)){
                    this.windowSnapshot[prop] = window[prop]; }}Object.keys(this.modifyPropsMap).forEach(p= >{
                window[p] = this.modifyPropsMap[p]; })}inactive(){ / / the deactivation
            for(const prop in window) {if(window.hasOwnProperty(prop)){
                    if(window[prop] ! = =this.windowSnapshot[prop]){
                        this.modifyPropsMap[prop] = window[prop];
                        window[prop] = this.windowSnapshot[prop]
                    }
                }
            }
        }
    }
Copy the code

Agent sandbox

Each application creates a proxy to represent the Window. The advantage is that each application is relatively independent and does not need to change the global Window property directly!

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
    }
}
Copy the code

Micro-front-end reconstruction based on jquery project

Project background

The project uses the JQ framework of Backbonejs, including a series of dependencies such as Bootstrap. Jquery is a shuttle, but it can be a bit of a chore to maintain. This led to the idea of refactoring with a micro front end.

  • The routing mode used in the project is hash mode

Firstly, as this project is an interface similar to background management system, there are many menus on the left side. If the jquery project is a sub-project, it might change a lot, so the JQ project is used as the base station directly. When the left route is clicked, the child application is loaded via a matching mechanism and embedded into the container.

jquery

First, qiankun was introduced

import {registerMicroApps, addGlobalUncaughtErrorHandler, start} from 'qiankun';
Copy the code

mian.js

function genActiveRule(routerPrefix) {
    return location= > location.hash.startsWith(The '#' + routerPrefix);
}
$(function() {
    / / layout
    createLayout();
    / / routing
    createRoute();
    // Go to the previous page
    gotoLastPage();
    registerMicroApps([
        {
            name: 'micro'.entry: '//xxx.com/home/vue/'.container: '#main-app'.activeRule: genActiveRule('/vue')}]); addGlobalUncaughtErrorHandler(event= > {
        console.error(event);
        const {message: msg} = event;
        // Load failed
        if (msg && msg.includes('died in status LOADING_SOURCE_CODE')) {
            console.error('Child application failed to load, please check if application is running'); }}); start(); monitorOrders.init(); });Copy the code

The first entry content is that the address that can be accessed by our sub-application after packaging and deployment is added with an error message provided by Qiankun

Sub-project (VUE)

main.js

import Vue from "vue";
import VueRouter from "vue-router";
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

// import "./public-path";
import App from "./App.vue";
import routes from "./routes";

Vue.use(VueRouter);
Vue.use(ElementUI);
Vue.config.productionTip = false;

let instance = null;
let router = null;

/** * Render functions * two cases: run in the main application lifecycle hook/run when the micro-application starts alone */
function render() {
  console.log("window.__POWERED_BY_QIANKUN__".window.__POWERED_BY_QIANKUN__);
  // Create VueRouter in render to ensure that the location event listener is removed when the microapplication is uninstalled to prevent event contamination
  router = new VueRouter({
    // Add the routing namespace /vue when running in the main application
    base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/".mode: "hash",
    routes,
  });

  // Mount the application
  instance = new Vue({
    router,
    render: (h) = > h(App),
  }).$mount("#app");
}

// Mount the application directly when running independently
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

/** * Bootstrap will only be called once during the micro-application initialization. The next time the micro-application re-enters, the mount hook will be called directly. Bootstrap will not be triggered again. * This is usually where we can initialize global variables, such as application-level caches that will not be destroyed during the unmount phase. * /
export async function bootstrap() {
  console.log("VueMicroApp bootstraped");
}

/** * The application calls the mount method every time it enters, and usually we trigger the application's render method here */
export async function mount(props) {
  console.log("VueMicroApp mount", props);
  render(props);
}

/** * The method that is called each time the application is cut/unloaded, usually here we unload the application instance of the microapplication */
export async function unmount() {
  console.log("VueMicroApp unmount");
  instance.$destroy();
  instance = null;
  router = null;
}

Copy the code

vue.config.js

const path = require("path");
function resolve(dir) {
  return path.join(__dirname, dir);
}

const stage = process.env.ENV === "testing";

const publicPath =
  process.env.ENV === "production"
    ? xxxx:xxx
    

let output = {
  library: "Micro".// Expose your library in a way that all module definitions work
  libraryTarget: "umd".WebpackJsonp_VueMicroApp = webpackJsonp_VueMicroApp
  jsonpFunction: `webpackJsonp_customerMicro`};let cssExtract = {};
if (stage) {
  output.filename = `js/[name].js? v=[hash]`;
  output.chunkFilename = `js/[name].js? v=[hash]`;
  cssExtract = {
    filename: "css/[name].css? v=[hash]".chunkFilename: "css/[name].css? v=[hash]"}; }module.exports = {
  publicPath,
  devServer: {
    // Listen on the port
    port: 10200.// Turn off host checking so that microapplications can be fetched
    disableHostCheck: true.// Configure cross-domain request headers to solve cross-domain problems in the development environment
    headers: {
      "Access-Control-Allow-Origin": "*",}},css: {
    extract: cssExtract,
  },
  configureWebpack: () = > {
    const conf = {
      resolve: {
        alias: {
          "@": resolve("src"),}},output: output,
    };
    return conf;
  },
  chainWebpack(config) {
    config.plugins.delete("preload");
    config.plugins.delete("prefetch");
    config.module
      .rule("vue")
      .use("vue-loader")
      .loader("vue-loader")
      .tap((options) = > {
        options.compilerOptions.preserveWhitespace = true;
        return options;
      })
      .end();
    config.when(process.env.NODE_ENV === "development".(config) = > config.devtool("cheap-source-map")); }};Copy the code

My configuration above distinguishes between a test environment and a formal environment. You can also do it based on the actual situation. The entry on the base station side can also be based on the environment.

A collection of microfront-end resources

A collection of micro front ends

Thank you for your

1. If the article is helpful to you, please move your little hands to point out a praise 👍, your praise is the power of my creation.

2. Pay attention to the public account “front-end elite”! Push high-quality articles from time to time.