35.qiankun微前端应用于vue应用的实践
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不生效)
更多推荐
所有评论(0)