微前端(qiankun)

159 阅读3分钟

1.主应用(基座)

在主应用中需要注册微应用的配置文件

// src/App.vue:
<template>
  <div id="app">
    <img src="./assets/logo.png">
    <!-- <router-view/> -->
    <p>当前页面在主应用(基座)</p>
    <!-- <router-link to="/oms-admin/">oms-admin应用入口</router-link>
    <router-link to="/config-admin/">config-admin应用入口</router-link> -->
    <ul>
      <li class="route_item"
        @click="goto('/sub-vue')">微应用1的入口</li>
      <li class="route_item"
        @click="goto('/sub-react')">微应用2的入口</li>
    </ul>
    <div id="app-viewport"></div> <!-- 主应用挂载子应用容器的元素节点 -->
  </div>
</template>

<script>
export default {
  name: 'App',
  methods: {
    goto(route) {
      history.pushState(null, route, route);
    }
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

// src/micro-app.js:
const microApps = [
  {
      name: 'sub-vue', // 微应用名称;
      // entry: process.env.VUE_APP_OMS_ADMIN, // 微应用入口;
      entry: 'http://localhost:8081',
      activeRule: '/sub-vue', // 微应用激活规则;
  },
  {
      name: 'sub-react',
      // entry: process.env.VUE_APP_CONFIG_ADMIN,
      entry: 'http://localhost:8082',
      activeRule: '/sub-react',
  }
];

const apps = microApps.map(item => {
  return {
      ...item,
      container: '#app-viewport', // 微应用的容器节点的选择器或者Element实例;
      props: { // 主应用需要传递给微应用的数据;
          routerBase: item.activeRule // 下发基础路由;
      }
  }
});

export default apps;

在主应用的入口文件 main.js 文件中注册微应用

// src/main.js:
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import microApps from './micro-app';
import { registerMicroApps, start } from 'qiankun';

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

registerMicroApps(microApps, {
  beforeLoad: app => {
      console.log(`app`, app.name);
  },
  beforeMount: [
      app => {
          console.log(`app`, app.name);
      }
  ],
  afterMount: [
      app => {
          console.log(`app`, app.name);
      }
  ],
  afterUnmount: [
      app => {
          console.log(`app`, app.name);
      }
  ],
});
start();

当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。

2.微应用(子应用)

在微应用入口文件 main.js 文件(通常就是你配置的 webpack 的 entry js)中导出bootstrapmountunmount 三个生命周期钩子,以供主应用在适当的时机调用。

// src/main.js:
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

let vueInstance = null;

function render(props = { }) {
  const { container } = props;
  vueInstance = new Vue({
    // el: '#app',
    router,
    components: { App },
    template: '<App/>'
  }).$mount(container ? container.querySelector('#sub_app') : '#sub_app');
}

if(!window.__POWERED_BY_QIANKUN__) {
  render();
}
/**
* 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用mount钩子,不会再重复触发
* bootstrap。通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用
* 级别的缓存等。
*/
export async function bootstrap() { 
  console.log('子应用 sub: bootstrap ~');
}

/**
* 应用每次切出/卸载会调用的方法,通常在这里我们会卸载微应用的应用实例。
*/
export async function unmount() { 
  vueInstance = null
}

/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法。
*/
export const mount = async props => { 
  render(props);
}
// index.html:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>sub-vue</title>
  </head>
  <body>
    <div id="sub_app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

3.子应用 webpack 配置

// build/webpack.dev.conf.js:
devServer: {
    ...
    headers: { 
      'Access-Control-Allow-Origin': '*' // 配置允许跨域访问子应用
    }
    ...
}

// webpack.base.conf.js:
const { name } = require('../package.json');

output: {
    // 把子应用打包成 umd 库格式 (主应用加载子应用需要是 umd 规范的打包后文件)
    library: `${name}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${name}`,
    
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath
}

4.HTML Entry

5.样式隔离

6.运行时沙箱

7.资源预加载

8.应用间通信

主应用与子应用间通信。

/**
* 主应用
* src/main.js:
*/
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import microApps from './micro-app';
import { registerMicroApps, start, setDefaultMountApp, initGlobalState } from 'qiankun';

const actions = initGlobalState({iptValue: '初始值'})

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

registerMicroApps(microApps, {
    beforeLoad: app => {
        console.log(`app`, app.name);
    },
    beforeMount: [
        app => {
            console.log(`app`, app.name);
        }
    ],
    afterMount: [
        app => {
            console.log(`app`, app.name);
        }
    ],
    afterUnmount: [
        app => {
            console.log(`app`, app.name);
        }
    ],
});

// 注册一个观察者函数;
actions.onGlobalStateChange((state, prev) => {
    console.log('main-app onGlobalStateChange', state, prev)
});

// 设置 iptValue 的值;
setTimeout(() => {
    actions.setGlobalState({ iptValue: '改变了值' });
}, 100);
start();

/**
* 子应用
* src/main.js:
*/
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

let vueInstance = null;

function render(props = { }) {
  console.log(787878, props);
  const { container } = props;
  vueInstance = new Vue({
    // el: '#app',
    router,
    components: { App },
    template: '<App/>'
  }).$mount(container ? container.querySelector('#sub_app') : '#sub_app');
}

if(!window.__POWERED_BY_QIANKUN__) {
  render();
}

/**
* 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount钩子,不会再重复触发
* bootstrap。通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用
* 级别的缓存等。
*/
export async function bootstrap() {
  console.log('子应用 sub: bootstrap ~');
}

/**
* 应用每次切出/卸载会调用的方法,通常在这里我们会卸载微应用的应用实例。
*/
export async function unmount() {
  vueInstance = null
}

/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法。
*/
export const mount = async props => {
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log('子应用获取的数据', state, prev);
  });
  props.setGlobalState({ iptValue: '子应用中修改值' });
  render(props);
}

文章引用

【微前端】qiankun源码阅读(2):加载子应用与沙箱隔离

微前端qiankun框架的底层实现原理

vue项目落地(qiankun.js)微前端服务

JS 沙箱隔离简单实现

qiankun微前端 如何确保主应用跟微应用之间的样式隔离

qiankun微前端主子应用通信方案

qiankun__子应用之间的通信

基于 QIANKUN 的微前端最佳实践(图文并茂) - 应用间通信篇