qiankun微前端应用于vue应用的实践

前言
本文介绍了用qiankun微前端框架用来集成vue应用的基本方案以及集成过程中遇到的一些常见问题。

一、基本使用
主应用
1.先安装 qiankun :

$ yarn add qiankun # 或者 npm i qiankun -S

2.注册微应用并启动:

import { registerMicroApps, start } from 'qiankun'

/**
 * step1 注册微应用
 */
registerMicroApps([
  {
    name: 'child-vue2', // 注册应用名
    entry: '//localhost:7012',// 注册服务
    container: '#sub-container', // 挂载的容器
    activeRule: '/vue2', // 路由匹配规则
  },
  {
    name: 'child-vue3',
    entry: '//localhost:7013',
    container: '#sub-container',
    activeRule: '/vue3',
  }
])

/**
 * Step2 设置默认进入的子应用(可选)
 */
setDefaultMountApp('/')

/**
 * Step3 启动qiankun应用
 */
start()

微应用 Vue 微应用

微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。

以 vue 2.x 项目为例

在 src 目录新增 public-path.js,并在main.js最顶部引入 public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

微应用建议使用 history 模式的路由,需要设置路由 base,值和它的 activeRule 是一样的

const router = new VueRouter({
  mode: 'history',
  base: window.__POWERED_BY_QIANKUN__ ? '/vue2' : '/',
  routes
})

在入口文件main.js中修改并导出bootstrap、mount、unmount三个生命周期函数,保证微应用单独运行且能在qiankun环境中运行。为了避免根 id #app 与其他的 DOM 冲突,需要限制查找范围。

import './public-path'
import Vue from 'vue'
import App from './App.vue'
import routes from './router'
import store from './store'

Vue.config.productionTip = false
//避免样式冲突
const getComputedStyle = window.getComputedStyle;
window.getComputedStyle = (el, ...args) => {
    // 如果为shadow dom则直接返回
    if (el instanceof DocumentFragment) {
        return {};
    }
    return Reflect.apply(getComputedStyle, window, [el, ...args]);
};
let instance = null
function render(props = {}) {
  const { container } = props
  instance = new Vue({
    router,
    store,
    render: (h) => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app')
}

// 独立运行微应用时
if (!window.__POWERED_BY_QIANKUN__) {
  render()
}

export async function bootstrap() {
  console.log('[vue] vue app bootstraped')
}

// 在qiankun中运行时
export async function mount(props) {
  render(props)
}

export async function unmount() {
  instance.$destroy()
  instance.$el.innerHTML = ''
  instance = null
}

修改 webpack 打包,允许开发环境跨域和 umd 打包(vue.config.js):

const { name } = require('./package');
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};

二、细节
1.生命周期
在这里插入图片描述
2.样式隔离
样式隔离,需在启动主应用时加上以下配置:

start({sandbox: {strictStyleIsolation: true}})

但是此种情况下,父应用的body样式会被子应用继承,要使子应用原来的body样式生效,需把子应用的body样式拷贝出来挂在#app节点下(可单独提取成一个css文件),当子应用走qiankun的逻辑时加载

export async function mount(props) {
  require('./style/config.css')
}

基于 ShadowDOM 的严格样式隔离并不是一个可以无脑使用的方案,大部分情况下都需要接入应用做一些适配后才能正常在 ShadowDOM 中运行起来。

fix shadow dom
getComputedStyle
当获取shadow dom的计算样式的时候传入的element是DocumentFragment,会报错。

// 解决报错问题
const getComputedStyle = window.getComputedStyle;
window.getComputedStyle = (el, ...args) => {
  // 如果为shadow dom则直接返回
  if (el instanceof DocumentFragment) {
    return {};
  }
  return Reflect.apply(getComputedStyle, window, [el, ...args]);
};

elementFromPoint
根据坐标(x, y)当获取一个子应用的元素的时候,会返回shadow root,并不会返回真正的元素。

const elementFromPoint = document.elementFromPoint;
document.elementFromPoint = function (x, y) {
  const result = Reflect.apply(elementFromPoint, this, [x, y]);
  // 如果坐标元素为shadow则用该shadow再次获取
  if (result && result.shadowRoot) {
    return result.shadowRoot.elementFromPoint(x, y);
  }
  return result;
}

;

document 事件 target 为 shadow
当我们在document添加click、mousedown、mouseup等事件的时候,回调函数中的event.target不是真正的目标元素,而是shadow root元素。

// fix: 点击事件target为shadow元素的问题
const {addEventListener: oldAddEventListener, removeEventListener: oldRemoveEventListener} = document;
const fixEvents = ['click', 'mousedown', 'mouseup'];
const overrideEventFnMap = {};
const setOverrideEvent = (eventName, fn, overrideFn) => {
  if (fn === overrideFn) {
    return;
  }
  if (!overrideEventFnMap[eventName]) {
    overrideEventFnMap[eventName] = new Map();
  }
  overrideEventFnMap[eventName].set(fn, overrideFn);
};
const resetOverrideEvent = (eventName, fn) => {
  const eventFn = overrideEventFnMap[eventName]?.get(fn);
  if (eventFn) {
    overrideEventFnMap[eventName].delete(fn);
  }
  return eventFn || fn;
};
document.addEventListener = (event, fn, options) => {
  const callback = (e) => {
    // 当前事件对象为qiankun盒子,并且当前对象有shadowRoot元素,则fix事件对象为真实元素
    if (e.target.id?.startsWith('__qiankun_microapp_wrapper') && e.target?.shadowRoot) {
      fn({...e, target: e.path[0]});
      return;
    }
    fn(e);
  };
  const eventFn = fixEvents.includes(event) ? callback : fn;
  setOverrideEvent(event, fn, eventFn);
  Reflect.apply(oldAddEventListener, document, [event, eventFn, options]);
};
document.removeEventListener = (event, fn, options) => {
  const eventFn = resetOverrideEvent(event, fn);
  Reflect.apply(oldRemoveEventListener, document, [event, eventFn, options]);
};

3.history模式下publicPath不能使用相对路径,需使用绝对路径
主应用需配置

publicPath:'/'

4.应用间的通信
qiankun提供了initGlobalState onGlobalStateChange setGlobalState用于通信。

主应用我们可以直接导入

import { initGlobalState } from 'qiankun'
// 初始化全局变量
const { onGlobalStateChange, setGlobalState } = initGlobalState({
  user: 'qiankun'
})

// 把监测变量改变和改变状态挂载到Vue上
Vue.prototype.$onGlobalStateChange = onGlobalStateChange
Vue.prototype.$setGlobalState = setGlobalState

// 监测变量改变
onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - master]:', value, prev),true)

// 改变全局变量
setGlobalState({
  user: 'master'
})

子应用我们需在生命周期里把onGlobalStateChange setGlobalState挂载到子应用的vue对象上:

export async function mount(props) {
    Vue.prototype.$onGlobalStateChange = props.onGlobalStateChange
    Vue.prototype.$setGlobalState = props.setGlobalState
}

5.如何在主应用的多级嵌套路由页面下加载微应用
主应用注册这个路由时给 path 加一个 *,使其在未匹配到主应用的某个页面时,也能加载主应用的外框,确保子应用要挂载的容易存在。

import empty from "./../empty"

export default {
  path: '/app',
  name: 'AppLayout',
  component: AppLayout,
  children: [{
    path: "manage",
    name: 'AppManage',
    component: Manage,
  },{
    path: "*",
    name: 'empty',
    component: empty //空的div标签
  }]
}

另外在注册微应用时,activeRule 需要包含主应用的这个路由 path

registerMicroApps([
  {
    name: 'website', // 注册应用名
    entry: '//localhost:8888',// 注册服务
    container: '#con', // 挂载的容器
    activeRule: '/app/website', // 路由匹配规则
  }
])

子应用的路由注册时base也需要加上这个path

function render(props = {}) {
  const {container} = props
  router = new VueRouter({
    mode: 'history',
    base: container ? /app/website : '/',
    routes
  })
  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app')
}
echats样式不生效
   if (window.__POWERED_BY_QIANKUN__) {
        const microDocument =
          document.getElementById("container").firstChild.shadowRoot;
        this.echart = this.$echarts.init(
          microDocument.getElementById(this.chartId)
        );
      } else {
        this.echart = this.$echarts.init(document.getElementById(this.chartId));
      }

**

参考文档:

**1.https://cdmana.com/2021/09/20210901182945380x.html
2.https://qiankun.umijs.org/zh/guide/tutorial#vue-%E5%BE%AE%E5%BA%94%E7%94%A8
3.https://www.jianshu.com/p/a86dfa736b35 (echarts不生效)

GitHub 加速计划 / vu / vue
207.54 K
33.66 K
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:2 个月前 )
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 4 个月前
e428d891 Updated Browser Compatibility reference. The previous currently returns HTTP 404. 5 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐