Micro front-end

To get straight to the point, there has been a good article about the micro front-end online, here is no longer to repeat, share my own feel better about the micro front-end principle of the article: # micro front-end – the most easy to understand the micro front-end knowledge # 30 minutes to quickly master all the core technology of micro front-end qiankun # Micro front-end serial 6/7: Micro front end frame – Dafa Good

Hand-written micro front-end series: # Write a micro front-end framework from Scratch # Handwrite a simple micro front-end framework

Use and principle of Qiankun

Qiankun website quick-and-dirty: qiankun.umijs.org/zh/guide/ge…

  • All you need to do is install the qiankun for the master application, register the sub-application routing matching rules, and start the application.
  • The subapplication needs to expose the three life cycle functions bootstrap, mount, and unmount. The main application can obtain these life cycle functions when the output is packaged through UMD, and control the loading and rendering of the subapplication.

Qiankun configuration using demo Git

// Configure main-vue3/ SRC /main.js for the main application
import { registerMicroApps, start } from 'qiankun'
const apps = [
  {
    name: 'vue2'.// The application name
    entry: 'http://localhost:2001/'.// The HTML is loaded by default, and the js inside is parsed to execute dynamically (the child application must support cross-domain, internal use is fetch).
    container: '#sub-container'.// The container id to render to
    activeRule: '/vue2' // Which path to activate
  },
  {
    name: 'vue3'.entry: 'http://localhost:3001/'.container: '#sub-container'.activeRule: '/vue3',}];// When an activeRule match is reached, the entry resource is requested and rendered into the container
registerMicroApps(apps); // Register the application
start({
  sandbox: {
    strictStyleIsolation: true.// Use shadow DOM to resolve style conflicts
    / / experimentalStyleIsolation: true / / by adding the range selector to solve conflict style}});// Start the application
Copy the code

Handwritten microfront

The essence of the micro front end is to monitor the changes of routes, match the corresponding sub-applications according to the configured sub-application routing matching rules, fetch the HTML content according to entry remote fetch, parse script tags and CSS tags in HTML, fetch these resources. Execute the obtained script code to add the CSS retrieved content to the HTML DOM; According to the configured route rendering rules, the HTML is rendered into the configured main application Container.

This involves a lot of small knowledge points, through handwriting, not only can understand the realization principle of the micro front end, but also can reinforce their own foundation.

Monitoring Route Changes

The purpose of monitoring route changes is to find the subapplication information that should be rendered based on the route. There are two routing modes: Hash mode and history mode. The hash mode needs to monitor the window.onhashchange event; The history mode needs to monitor pushState, replaceState, and window. onpopState events. PushState and replaceState do not include browser forward and backward, so window. onpopState events need to be monitored as well. For more details: juejin.cn/post/684490…

// main-vue3/src/micro-fe/rewrite-router.js
import {handleRouter} from './handle-router'

// Cache the previous route, the next route
let prevRoute = ""
let nextRoute = window.location.pathname

export const getPrevRoute = () = > prevRoute
export const getNextRoute = () = > nextRoute

export const rewriteRouter = () = >{
  window.addEventListener('popstate'.() = >{
    // When popState is triggered, the route is already navigated
    prevRoute = nextRoute
    nextRoute = window.location.pathname
    handleRouter()
  })

  const rawPushState = window.history.pushState
  window.history.pushState = (. args) = > {
    Before / / navigation
    prevRoute = window.location.pathname
    rawPushState.apply(window.history, args)
    After the / / navigation
    nextRoute = window.location.pathname
    handleRouter()
  }

  const rawReplaceState = window.history.replaceState
  window.history.replaceState = (. args) = >{
    Before / / navigation
    prevRoute = window.location.pathname
    rawReplaceState.apply(window.history, args)
    After the / / navigation
    nextRoute = window.location.pathname
    handleRouter()
  }
}
Copy the code

Handwriting implements the history routing mode; The prevRoute and nextRoute variables record the values before and after the route change. Based on the values before and after the route change, you can locate the subapplication before and after the route change, uninstall the subapplication before and after the route change, and load the subapplication after the route change.

Matching subapplication

As mentioned above, the prevRoute and nextRoute variables can match the subapplication information before and after the route change. If the former subapplication exists, unmount the former subapplication. If the latter child application exists, load (bootstrap, mount) the new child application.

Bootstrap and mount unmount are the key lifecycle functions of the child application. How to obtain the three declared cycle functions exposed by the remote child application? We’ll talk about that when we load the child application.

Load child application

To enable CSS to be packaged separately during development mode, the following configuration is required for vue CLI-generated projects:

// vue.config.js
module.exports = {
  css: {
    extract: true  // Extract the CSS from the component into a separate CSS file}}Copy the code

The CSS files will also be generated separately during pattern development.


First, the HTML is obtained remotely based on the matched entry. After the HTML is parsed, the REQUIRED JS and CSS files are fetched asynchronously.

Render child application

Now that we have HTML files, CSS files and JS files, “everything is ready”, we just need to convert HTML strings into DOM elements, and add style tags generated by CSS files to DOM elements. Then the JS file is executed and the DOM element is loaded into the corresponding container, which completes the rendering of the child application.

In this process, we need to solve three problems:

  1. Style isolation
  2. JavaScript isolation
  3. Gets the bootstarp, mount, and unmount life cycles of the child application

Style isolation

Style isolation mainly includes the isolation of main application and sub-application styles, and the isolation of sub-application styles is not affected by each other. The core is to add a layer of style isolation to the child application as it loads the render. Common tools are CSS Modules, shadow DOM, or plug-ins using PostCSS.

CSS Modules: The Vue CLI project originally supports the use of CSS Modules, just add the module feature to the component

<style module>
.red {
  color: red;
}
.bold {
  font-weight: bold;
}
</style>
Copy the code

Access CSS Modules in the component, add module to

<template>
  <div>
    <p :class="$style.red">
      hello red!
    </p>
  </div>
</template>
Copy the code

More details of use: cloud.tencent.com/developer/a… CSS Modules give CSS style names a new class after compilation to ensure that styles are not the same.

The feature of Shadow DOM is that the style set in the DOM element after Shadow DOM is enabled only affects the DOM and its subset DOM, and does not affect other DOM elements. Use this feature to enable Shadow DOM on the DOM that the child application needs to render, so that the style of the child application does not affect the effect of the main application. This paper uses Shadow DOM to achieve CSS style isolation. The Shadow DOM element is invisible to JavaScript selectors in the main document, such as querySelector. Use shadowRoot to filter the render container of the child application:

// main-vue3/src/micro-fe/handle-router.js
// When loading the child application, add a DIV with Shadow DOM enabled
export const handleRouter = async() = > {...const container = document.querySelector(app.container)
  const subWrap = document.createElement('div')
  subWrap.id = "__inner_sub_wap__"
  const shadowDom = subWrap.attachShadow({mode: 'open'})
  shadowDom.innerHTML = template.innerHTML

  container.innerHTML = ""
  container.appendChild(subWrap)
  ...
}

Copy the code

Add a layer of judgment to the child application mount function:

// app-vue3/src/main.js
function render(props = {}) {
  instance = createApp(App)
  const { container } = props;
  const shadowApp = container.firstChild.shadowRoot.querySelector('#app')
  instance.mount(shadowApp ? shadowApp : '#app');
}
Copy the code

More details of use: useful. Javascript. The info/shadow – dom

Postcss PostCSs-selector – Namespace plugin can add a namespace to a style to achieve the effect of style isolation, of course, we can also implement postCSS plugin.

JavaScript isolation

This paper mainly introduces two kinds of JS sandbox: snapshot sandbox and proxy sandbox.

The main methods of snapshot sandbox are active and inactive. Active means to activate the sandbox, record variables on the window in snapshotWindow, and snapshot variables on the original Window. Assign the modifyMap value to the window variable. Inactive indicates that the sandbox is deactivated. In this case, the difference between the active snapshot and the value of the current window variable is compared and stored in the modifyMap variable. The next time the sandbox is activated, the value is reassigned to the Window.

// main-vue3/src/micro-fe/snapshot-sandbox.js
export class SnapshotSandbox{
  constructor(name){
    this.name = name
    this.proxy = window
    this.snapshotWindow = {}
    this.modifyMap = {}
  }
  active(){
    this.snapshotWindow = {}
    for(let key in window) {this.snapshotWindow[key] = window[key]
    }
    for(let key in this.modifyMap){
      window[key] = this.modifyMap[key]
    }
  }
  inactive(){
    for(let key in window) {if(this.snapshotWindow[key] ! = =window[key]){
        // Record the change
        this.modifyMap[key] = window[key]
        // Restore the snapshot value
        window[key] = this.snapshotWindow[key]
      }
    }
  }
}
Copy the code

Proxy sandbox the main methods of Proxy sandbox are active and inactive, Proxy Proxy window, get access, first go to fakeWindow, if not, will be the original rawWindow value. Set assignments are performed only when the sandbox is active.

// main-vue3/src/micro-fe/proxy-sandbox.js
export class ProxySandbox{
  active(){
    this.sandboxRunning = true
  }
  inactive(){
    this.sandboxRunning = false
  }
  constructor(name){
    this.name = name
    const rawWindow = window 
    const fakeWindow = {}
    const proxy = new Proxy(fakeWindow, {
      set: (target, prop, value) = >{
        if(this.sandboxRunning){
          target[prop] = value
          return true}},get: (target, prop) = >{
        // If there is one inside fakeWindow, fetch it from inside fakeWindow, otherwise, fetch it from outside window
        let value = prop in target ? target[prop] : rawWindow[prop]
        return value
      }
    })
    this.proxy = proxy
  }
}
Copy the code

Once you’ve covered the two methods of JavaScript isolation, it’s a matter of use. Initialize and activate the corresponding sandbox when the child applies mount. Freezing the sandbox while the child application is unmounted ensures that JavaScript is isolated from each other while the child application is running.

Another point is that when executing the JavaScript code of the child application, you need to adjust its execution environment to the sandbox environment.

There are two ways to execute JavaScript for strings: eval and new Function()

// main-vue3/src/micro-fe/import-html.js
async function execScripts(global){... scripts.forEach((code) = > {
      window.proxy = global
      const scriptText = `
        ((window) => {
          ${code}
        })(window.proxy)
      `
      //eval(scriptText)
      new Function(scriptText)()
    });
 }
Copy the code

Gets the child application lifecycle

There are two ways to do this. The first is to expose the bootstrap and mount unmount life cycles to the Window global object in the child application. After executing the JS code of a child application, the window object automatically adds the application’s information:

Application of communication

Communication between Applications: juejin.cn/post/684490… This article describes two ways to implement application communication: CustomEvent and publish-subscribe

customevent

Implement the Custom class to add Custom events to the Window object.

// main-vue3/src/micro-fe/global/data-custom.js
export class Custom{
  // Event listener
  on(name, cb){
    window.addEventListener(name, e= >{
      cb(e.detail)
    })
  }
  // The event is triggered
  emit(name, data){
    const event = new CustomEvent(name, {detail: data})
    window.dispatchEvent(event)
  }
}
Copy the code

Use:

// main-vue3/src/main.js
import { Custom } from './micro-fe/global/data-custom.js'

const globalCustom = new Custom()
// Event listener
globalCustom.on("build".(data) = >{
  console.log(data)
})
window.globalCustom = globalCustom


/ / application
// app-vue3/src/App.vue
    emitBuild(){
      const globalCustom = window.globalCustom
      // The event is triggered
      globalCustom.emit("build".100)}Copy the code

Custom events — Event and CustomEvent

Release subscription

The createStore method uses closures to store getStore, Update, and SUBSCRIBE in memory.

// main-vue3/src/micro-fe/global/data-store.js
export const createStore = (initData = {}) = > (() = >{
  let store = initData
  // Manage all subscribers
  const observers = []

  / / for the store
  const getStore = () = > store

  / / update the store
  const update = (value) = > {
    if(value ! == store) {// Execute the store operation
      const oldValue = store
      // Update the store
      store = value
      // Notify all subscribers and listen for store changes
      observers.forEach(async item => await item(store, oldValue))
    }
  }
  
  // Add subscribers
  const subscribe = (fn) = > {
    observers.push(fn)
  }

  return {
    getStore,
    update,
    subscribe,
  }
})()
Copy the code

Use:

// main-vue3/src/main.js
import { createStore } from './micro-fe/global/data-store.js'
const store = createStore()

window.store = store
/ / subscribe
store.subscribe((newValue, oldValue) = > {
  console.log(newValue, oldValue, The '-')})/ / application
// app-vue3/src/App.vue
    emitBuild(){
      const globalCustom = window.globalCustom
      / / update
      globalCustom.emit("build".100)}Copy the code

The source address

Demo effect: