微前端-qiankun

1.什么是微前端?

微前端-qiankun
微前端就是将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应 用。
微前端的核心在于拆, 拆完后在合!

2.为什么使用微前端?

不同团队间开发同一个应用技术栈不同怎么破? 希望每个团队都可以独立开发,独立部署怎么破? 项目中还需要老的应用代码怎么破?
我们可以将一个应用划分成若干个子应用,将子应用打包成一个个的lib。当路径切换 时加载不同的子应用。这样每个子应用都是独立的,技术栈也不用做限制了!从而解决了前 端协同开发问题

3.微前端落地:

2018年 Single-SPA诞生了, single-spa 是一个用于前端微服务化的 JavaScript 前端解决 方案 (本身没有处理样式隔离, js 执行隔离) 实现了路由劫持和应用加载(即根据不同的路由加载不同的应用)
2019年 qiankun 基于Single-SPA, 提供了更加开箱即用的 API ( single-spa + sandbox + import-html-entry )做到了,技术栈无关、并且接入简单(像i frame 一样简单)
总结:子应用可以独立构建,运行时动态加载,主子应用完全解耦,技术栈无关,靠的是协 议接入(子应用必须导出 bootstrap、mount、unmount方法)
这不是 iframe 吗?
如果使用 iframe , iframe 中的子应用切换路由时用户刷新页面就尴尬了。
应用通信:
基于URL来进行数据传递,但是传递消息能力弱(最简单)
基于 CustomEvent 实现通信(window的原生API)
基于props主子应用间通信
使用全局变量、 Redux 进行通信
公共依赖:
CDN - externals
webpack 联邦模块

4.SingleSpa 实战

4.1.在子应用中安装single-spa-vue

npm i single-spa-vue -S

4.2 在child-vue的main.js中我们使用以下代码:

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;

4.3 设置子应用的路由
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
4.4 因为父应用需要加载子应用,所以我们配置好main.js之后,我们需要将子应用打包成一个lib去给父应用使用,所以我们在子应用的vue配置文件vue.config.js中使用以下代码:

module.exports = {
	configureWebpack:{
        output:{
            library:'singleVue',
            library:'umd'
        },
        devServer:{
            port :20000
        }
    }
}

当我们执行 npm run serve 时 子应用并不会跑起来,而是打包成了一个类库.

4.5 父应用相关操作
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')

5.CSS 隔离方案

子应用之间样式隔离:
Dynamic Stylesheet 动态样式表,当应用切换时移除老应用样式,添加新应用样式
主应用和子应用之间的样式隔离:

BEM (Block Element Modifier) 约定项目前缀
CSS-Modules 打包时生成不冲突的选择器名
ShadowDOM 真正意义上的隔离 css-in-js Shadow DOM隔离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是浏览器原生的方法,可以用来创建Shadow-dom;Shadow-dom 是游离在 DOM 树之外的节点树,Shadow-dom 具有良好的密封性;当attachShadow的mode:'closed"时; 外界无法访问 shadow dom,从而实现真正的样式隔离

注意:
项目中有些元素是直接加到body上的比如弹框,这样这些元素不再影子盒里而是被直接加在body中,所以无法实现css样式隔离:如下面当我们最后执行document.body.appendChild(pElm);时会将pElm元素从影子盒子里移动到body中,如下代码:

<!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>

6. JS 沙箱机制

微前端-qiankun
当运行子应用时应该跑在内部沙箱环境中
快照沙箱,在应用沙箱挂载或卸载时记录快照,在切换时依据快照恢复环境 (无法支持多实 例)
Proxy 代理沙箱,不影响全局环境
1).快照沙箱
1.激活时将当前window属性进行快照处理
2.失活时用快照中的内容和当前window属性比对
3.如果属性发生变化保存到 modifyPropsMap 中,并用快照还原window属性
4.在次激活时,再次进行快照,并用上次修改的结果还原window

<!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>

注意:
为什么要用沙箱呢?因为当我们父应用再加载完A应用后要切换成B应用时,如果不A应用从window中清除的话,会造成全局资源的污染,而沙箱就能保证应用的运行 从开始到结束 ,切换后不会影响全局
快照沙箱只能针对单实例应用场景,如果是多个实例同时挂载的情况则无法解决,只能通过 proxy代理沙箱来实现;es6的proxy代理沙箱可以实现多应用沙箱。把不同的应用用不同的代理来处理
2).Proxy 代理沙箱

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);

每个应用都创建一个proxy来代理window,好处是每个应用都是相对独立,不需要直接更 改全局window属性!

7、qiankun实战

npm i qiankun -S

主应用编写

<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>

注册子应用

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')

子Vue应用(子应用01与子应用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).打包配置(子应用01与子应用02一样仅端口号不同)

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

上一篇:浅析VUE项目如何使用qiankun搭建微前端架构


下一篇:使用qiankun整合项目记录