[Introduction to micro-frontends]: from single-spa to qiankun

1. About micro-frontends

1.What? What is a micro frontend?

insert image description here

The micro front end is to split different functions into multiple sub-applications according to different dimensions. These sub-applications are loaded by the main application.
The core of the micro-frontend is to dismantle it, and reassemble it after dismantling!

2.Why? Why use him?

How can different teams develop the same application technology stack differently? I hope that each team can develop independently. How can independent deployment be broken? How to break the old application code that is still needed in the project?
We can divide an application into several sub-applications, and package the sub-applications into individual libs. Load a different sub-app when the path is switched. In this way, each sub-application is independent, and the technology stack does not need to be restricted! So as to solve the problem of front-end collaborative development

3. How? How to implement the micro-frontend?

In 2018, Single-SPA was born. single-spa is a JavaScript front-end solution for front-end micro-services (it does not handle style isolation itself, js execution isolation) to achieve route hijacking and application loading (that is, load different applications according to different routes). In 2019, qiankun is based on Single-SPA and provides a more out-of-the-box API (single-spa + sandbox + import-html-entry). Stack-independent, and easy to access (as simple as i frame) Summary: sub-applications can be built independently, dynamically loaded at runtime, main and sub-applications are completely decoupled, technology stack-independent, relying on protocol access (sub-applications must export bootstrap, mount, unmount methods) Isn’t
this
iframe
?
If you use iframe, it will be embarrassing for the user to refresh the page when the sub-application in the iframe switches the route.
Application communication:
data transfer based on URL, but weak message transfer ability (simplest)
communication based on CustomEvent (native API of window)
communication between main and sub-applications based on props
use global variables and Redux for communication
public dependencies:
CDN - externals
webpack federation module

2. SingleSpa Actual Combat

Let's start the micro-frontend development with SingleSpa.
Step 1: First we create two applications; a child application and a parent application; (we need the parent application to load the child application)

Step 2: Sub-application-related operations
1. Install single-spa-vue in the sub-application

npm i single-spa-vue -S

2. Using single-spa-vue
in the main.js of child-vue we use the following code:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'
Vue.config.productionTip = false

const appOptions = {
    
    
  el: '#vue', // 挂载到父应用中id为vue的标签中
  router,
  render: h => h(App)
}

// 使用 singleSpaVue 包装 Vue,
// 包装后返回的对象中vue包装好的三个生命周期,bootstrap,mount,unmount,
const vueLifeCycle = singleSpaVue({
    
     Vue, appOptions })
// 协议接入,我们定好了协议,父应用会调用这些方法
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;

3. Set the routing of the sub-application

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    
    
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    
    
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
    
    
  mode: 'history',
  base: "/myVueApp",
  routes
})

export default router

3. Because the parent application needs to load the child application, after we configure main.js, we need to package the child application into a lib for the parent application to use, so we use the following code in the vue configuration file vue.config.js of the child application: module.exports =
{

    configureWebpack:{
    
    
        output:{
    
    
            library:'singleVue',
            library:'umd'
        },
        devServer:{
    
    
            port :20000
        }
    }
}

When we execute npm run serve, the sub-application does not run, but is packaged into a class library
insert image description here

Step 3: Parent application-related operations
in App.vue:

<template>
  <div id="app">
    <router-link to="/myVueApp">加载Vue应用</router-link> |
    <!-- 将子应用将会挂载到 id="vue" 标签中 -->
    <div id="vue"></div>
  </div>
</template>

main.js:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import {
    
     registerApplication, start } from 'single-spa';
Vue.config.productionTip = false
async function loadScript (url) {
    
    
  return new Promise((resolve, reject) => {
    
    
    // 创建标签,并将标签加到head中
    let script = document.createElement('script');
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  })
}
// 注册应用
registerApplication('myVueApp',
  async () => {
    
    
    // 加载子组件导出的类库和资源,注意先后顺序
    await loadScript(`http://localhost:20000/js/chunk-vendors.js`);
    await loadScript(`http://localhost:20000/js/app.js`)
    return window.singleVue; // 返回子应用里导出的生命周期,mount,ummount,bootstrap
  },
  location => location.pathname.startsWith('/myVueApp'));// 当用户切换到/myVueApp的路径下时,加载刚才定义子子应用
start();
new Vue({
    
    
  router,
  render: h => h(App)
}).$mount('#app')

running result:
insert image description here

Personal understanding :
When we click the 'Load Vue Application' button, the route of the parent application is /myVueApp and the parent application does not configure the route of /myVueApp, so the resource of the child application will be requested from the address when the child application is registered, and then the resources will be mounted on the label of the parent component with the id of 'vue' (the label (id='vue') will display the content of the DOM of the default route of the child application)

3. CSS isolation scheme

Style isolation between sub-applications:
Dynamic Stylesheet dynamic style sheet, when the application is switched, remove the old application style and add a new application style Style
isolation between the main application and sub-applications:

  • BEM (Block Element Modifier) ​​convention item prefix
  • Generate non-conflicting selector names when packaging CSS-Modules
  • ShadowDOM really isolates css-in-js Shadow DOM isolates css:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div>
        <p>hello world</p>
        <div id="x"></div>
    </div>
    <script>
        // dom的api
        let shadowDOM = document.getElementById('x').attachShadow({
      
      mode:'closed'}); // 外界无法访问 shadow dom
        let pElm = document.createElement('p');
        pElm.innerHTML = 'hello zf';
        let styleElm = document.createElement('style');

        styleElm.textContent = `
            p{color:red}
        `
        shadowDOM.appendChild(styleElm)
        shadowDOM.appendChild(pElm);

        // document.body.appendChild(pElm);

    </script>
</body>
</html>

attachShadow is a browser-native method that can be used to create Shadow-dom; Shadow-dom is a node tree that is free from the DOM tree, and Shadow-dom has good airtightness; when the mode of attachShadow: 'closed'; the outside world cannot access shadow dom, so as to achieve true style isolation

Note:
Some elements in the project are directly added to the body, such as bullet boxes, so that these elements are not in the shadow box but are directly added to the body, so CSS style isolation cannot be achieved: as follows, when we finally execute document.body.appendChild(pElm); the pElm element will be moved from the shadow box to the body, as shown in the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div>
        <p>hello world</p>
        <div id="x"></div>
    </div>
    <script>
        // dom的api
        let shadowDOM = document.getElementById('x').attachShadow({
      
      mode:'closed'}); // 外界无法访问 shadow dom
        let pElm = document.createElement('p');
        pElm.innerHTML = 'hello zf';
        let styleElm = document.createElement('style');

        styleElm.textContent = `
            p{color:red}
        `
        shadowDOM.appendChild(styleElm)
        shadowDOM.appendChild(pElm);

        // document.body.appendChild(pElm);

    </script>
</body>
</html>

4. JS sandbox mechanism

insert image description here

When running a sub-application, it should run in the internal sandbox environment
Snapshot sandbox, record the snapshot when the application sandbox is mounted or unmounted, and restore the environment according to the snapshot when switching (cannot support multiple instances) Proxy agent sandbox , does not affect the global environment 1). Snapshot sandbox 1. Take a snapshot of the current window property when activated .
Take
the snapshot again and restore the window with the result of the last modification



<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <script>
    // 如果应用 加载 刚开始我加载A应用 window.a  B应用 (window.a);
    // 单应用切换  沙箱 创造一个干净的环境给这个子应用使用,当切换时 可以选择丢弃属性和恢复属性
    // 快照沙箱  1年前拍一张  现在再拍一张  (将区别保存起来) 在回到一年前
    class SnapshotSandbox {
      
      
      constructor() {
      
      
        this.proxy = window; // window属性
        this.modifyPropsMap = {
      
      }; // 记录在window上的修改那些属性 
        this.active();
      }
      active() {
      
       // 激活
        this.windowSnapshot = {
      
      }; // 拍照 
        for (const prop in window) {
      
      
          if (window.hasOwnProperty(prop)) {
      
      
             // 将window上的属性进行拍照 
            this.windowSnapshot[prop] = window[prop];
          }
        }
        Object.keys(this.modifyPropsMap).forEach(p => {
      
      
          window[p] = this.modifyPropsMap[p];
        })
      }
      inactive() {
      
       // 失活
        for (const prop in window) {
      
      // diff 差异 
          if (window.hasOwnProperty(prop)) {
      
      
               // 将上次拍照的结果和本次window属性做对比 
            if (window[prop] !== this.windowSnapshot[prop]) {
      
      
                // 保存修改后的结果 
              this.modifyPropsMap[prop] = window[prop];
               // 还原window 
              window[prop] = this.windowSnapshot[prop]
            }
          }
        }
      }
    }
    let sandbox = new SnapshotSandbox();

    // 应用的运行 从开始到结束 ,切换后不会影响全局 
    ((window) => {
      
      
      window.a = 1;
      window.b = 2;
      console.log(window.a, window.b);
      sandbox.inactive();
      console.log(window.a, window.b);
      sandbox.active();
      console.log(window.a, window.b);
    })(sandbox.proxy); // sandbox.proxy就是window
    
  </script>
</body>

</html>

Note:
Why use a sandbox? Because when our parent application loads the A application and then switches to the B application, if the A application is not cleared from the window, it will cause global resource pollution, and the sandbox can ensure that the application runs from the beginning to the end, and the global environment will not be affected after switching
. Use different agents to handle different applications
2). Proxy agent sandbox

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) {
    
                     
         return target[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);

Each application creates a proxy to proxy the window. The advantage is that each application is relatively independent, and there is no need to directly change the global window attribute!
Five, qiankun actual combat

install qiankun

npm i qiankun -S

1). Writing the main application

<template>
  <div>
    <el-menu :router="true" mode="horizontal" default-active="/">
      <!--基座中可以放自己的路由-->
      <el-menu-item index="/">基座</el-menu-item>
      <!--引用其他子应用-->
      <el-menu-item index="/vue01">vue应用01</el-menu-item>
      <el-menu-item index="/vue02">vue应用02</el-menu-item>
    </el-menu>
    <div><router-view></router-view></div>
    <div id="vue01"></div>
    <div id="vue02"></div>
  </div>
</template>

2). Register sub-applications

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);
import {
    
     registerMicroApps, start } from 'qiankun';
const apps = [
  {
    
    
    name: 'vue--01', // 应用的名字
    entry: '//localhost:10001', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
    container: '#vue01', // 容器名
    activeRule: '/vue01', // 激活的路径
    props:{
    
    a:1}, //传递参数数据
  },
  {
    
    
    name: 'vue--02',
    entry: '//localhost:20000', // 默认会加载这个html 解析里面的js 动态的执行 (子应用必须支持跨域)fetch
    container: '#vue02',
    activeRule: '/vue02',
  }
]
registerMicroApps(apps); // 注册应用
start({
    
    
  prefetch: true // 取消预加载
});// 开启

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

3). Sub-Vue application (sub-application 01 is the same as sub-application 02)

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'); // 这里是挂载到自己的html中  基座会拿到这个挂载后的html 将其插入进去
}
if (window.__POWERED_BY_QIANKUN__) {
    
     // 动态添加publicPath
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) {
    
     // 默认独立运行
  render();
}
// 子组件的协议就ok了
export async function bootstrap (props) {
    
    

};
export async function mount (props) {
    
    
  console.log(props)
  render(props)
}
export async function unmount (props) {
    
    
  instance.$destroy();
}

4). Packaging configuration (sub-application 01 is the same as sub-application 02, only the port number is different)

module.exports = {
    
    
  devServer: {
    
    
    port: 10001,
    headers: {
    
    
      'Access-Control-Allow-Origin': '*'
    }
  },
  configureWebpack: {
    
    
    output: {
    
    
      library: 'vueApp',
      libraryTarget: 'umd'
    }
  }
}

5). Run the parent application and child application:

insert image description here

Source address: https://github.com/Jason-chen-coder/MircoWebApp-vue

Guess you like

Origin blog.csdn.net/weixin_39085822/article/details/108832278