本次接入主应用(vue3 + vite)路由采用history模式,子应用(vue2 + webpack)路由也采用history模式;
主应用改造如下:
安装依赖;
npm i -D qiankun
创建目录src/qiankun用于存放qiankun相关的方法和配置;src/qiankun 创建config.ts,添加子应用配置:
import { Session } from '/@/utils/storage';
const isDev = !['test', 'prod'].includes(import.meta.env.MODE);
/** 魔方ui子应用前缀 */
export const magicCubeUi = 'magic-cube-ui';
// 微应用-子应用的配置
export const subAppsConfig = {
subApps: [
{
name: magicCubeUi, // 子应用名称,跟package.json一致
// entry: '//localhost:8081/', // 子应用入口,本地环境下指定端口
entry: isDev ? '//localhost:8081/' : '/magicCubeUi/', // 子应用入口,本地环境下指定端口
container: '#sub-container', // 挂载子应用的dom
activeRule: /${magicCubeUi}, // 路由匹配规则
// 主应用与子应用通信传值
props: {
getToken: () => Session.get('token'),
},
},
],
};
创建src/qiankun/index.ts,添加子应用启动、通信相关方法;
import { subAppsConfig } from './config';
import { loadMicroApp, registerMicroApps, prefetchApps, start, initGlobalState } from 'qiankun';
const { subApps } = subAppsConfig;
/** 注册子应用(registerMicroApps + start) */
export const registerApps = () => {
try {
registerMicroApps(subApps, {
beforeLoad: [(app) => Promise.resolve(app)],
beforeMount: [(app) => Promise.resolve(app)],
afterUnmount: [(app) => Promise.resolve(app)],
});
start({
sandbox: {
experimentalStyleIsolation: true,
},
});
} catch (err) {
console.log(err);
}
};
/** 手动加载微应用,该方法加载在不切换子应用时候,可以实现keep-alive */
export const loadApp = () => {
const vm = loadMicroApp(subApps[0], {
sandbox: {
experimentalStyleIsolation: true,
},
});
onUnmounted(() => vm.unmount());
};
/** 预加载子应用 */
export const preFetchSubApp = () => {
prefetchApps([
{
name: subApps[0].name,
entry: subApps[0].entry,
},
]);
};
/** 应用间通信 */
export const microAppCommunicate = () => {
// 初始化 state
const state: { action: string; payload: any } = {
action: '',
payload: null,
};
const actions = initGlobalState(state);
return {
state,
actions,
};
};
创建组件,作为启动子应用的vue挂载容器;
qiankun提供两种方式启动子应用。第一种为 registerMicroApps + start方式,该方式需要注意:第一是挂载的容器不能被transition包裹,否则路由切换后,该元素被销毁,后续无法再次拉起子应用;第二是,因为路由切换到主应用其他路由时候,子应用被销毁,所以子应用keep-alive无法生效;所以采用手动加载子应用(loadMicroApp)的方式,来拉起子应用,该方式在主应用main.ts可以预加载指定子应用,来更快拉起子应用;
添加路由,用于匹配子应用相关页面;
{
path: '/magic-cube-ui',
name: 'magic-cube-ui',
meta: {
title: '智能运营平台',
},
component: () => import('/@/layout/routerView/parent.vue'),
children: [
{
path: '/:pathMatch(.)',
meta: {
title: '智能运营平台',
},
component: () => import('/@/views/qiankun/subContainer.vue'),
},
],
},
子应用改造如下:
创建src/public-path.js(需在main.js引入);
if (window.POWERED_BY_QIANKUN) {
// eslint-disable-next-line no-undef
webpack_public_path = window.INJECTED_PUBLIC_PATH_BY_QIANKUN
}
main.js 暴露生命周期给主应用:
let instance = null;
function render(props = {}) {
const { container } = props;
// 每次渲染的时候调用redirectPopup事件
redirectPopup(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');
}
export async function mount(props) {
// console.log("[vue] props from main framework", props);
setToken(props.getToken());
actions.setActions(props);
// 加载完毕,通知主应用
actions.setGlobalState({
action: 'loaded',
payload: '1',
});
actions.onGlobalStateChange((state, _prev) => {
// state: 变更后的状态; prev 变更前的状态
}, true);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
document.body.appendChild = originFn;
}
改造路由为history模式
const router = new VueRouter({
mode: isQianKun() ? "history" : "hash",
base: isQianKun() ? /magic-cube-ui : process.env.BASE_URL,
scrollBehavior: (to, from, savedPosition) => savedPosition || { x: 0, y: 0 },
routes: basicsRoutes,
});
创建action.js,用于和主应用之间进行通信
function emptyAction() {
console.warn('action is empty');
}
class Action {
actions = {
onGlobalStateChange: emptyAction,
setGlobalState: emptyAction,
};
setActions(actions) {
this.actions = actions;
}
onGlobalStateChange(...args) {
this.actions.onGlobalStateChange(...args);
}
setGlobalState(...args) {
this.actions.setGlobalState(...args);
}
}
/** 用于和主应用进行通信 */
export const actions = new Action();
vue.config.js改造
第一是需要配置headers来允许跨域;第二是需要打包成umd格式,来满足qiankun的调用要求;这里需要将package.json里面的name修改为我们子应用公共路由前缀magic-cube-ui。
const { name } = require('./package')
module.exports = {
devServer: {
port: 7002,
headers: { 'Access-Control-Allow-Origin': '*', },
},
configureWebpack: {
output: {
library: ${name}-[name], libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: webpackJsonp_${name},
},
},
}
到这里,在主应用和子应用的代理里面配置一下之后,就可以运行起来了(子应用的导航和顶部需要判断是否运行在微前端环境,来进行隐藏)。接入的过程中,最初是使用registerMicroApps + start方式,坑很多,比如因为transition包裹,切换路由后挂载元素被干掉,导致子应用无法拉起。比如在子应用路由下刷新页面,会导致页面主应用部分消失.....所以采用了loadMicroApp手动加载子应用的方式。到这里就剩下两个主要的问题了,第一个是部署问题,第二个是主应用、子应用样式冲突问题。第一个问题,采用的是同域部署,即在主应用(这里是播控)目录下面创建一个目录,来存放子应用文件。这里需要注意的是子应用的目录名称(magicCubeUi)要和子应用路由公共前缀(magic-cube-ui)区分开,否则将主应用路由后面部分去掉,会直接进入到子应用。然后需要主应用打包配置(vite.config.ts)里面的base修改为 / ,子应用的publicPath为 /magicCubeUi/ , 然后需要配置nginx来避免刷新后因为history路由导致页面资源404,配置如下:
location / {
root iptv-ui;
index index.html index.htm;
try_files uri/ /index.html;
}
location /magicCubeUi {
root iptv-ui;
index index.html index.htm;
try_files uri/ /magicCubeUi/index.html;
}
第二个问题,样式冲突问题主要是因为子应用是挂载在主应用的一个元素下(#sub-container),所以子应用会被主应用的全局样式影响,所以需要修改子应用下某些元素的全局样式,例如全局样式所有元素outline: none !imporant,子应用某些样式又需要outline的,就需要指定具体元素的outline,并提高优先级。虽然子应用的配置开启了样式隔离(experimentalStyleIsolation: true会将子应用的样式添加一个qiankun开头的hash,类似scoped),但是element-ui的弹框、下拉框选择项、时间选择等默认都是插入到body里面,导致样式无法生效;经过各种方案尝试,发现最合适的方法是,在子应用内容指定一些类名,然后修改子应用的document.body的appendChild方法,当判断到要插入的元素包含指定类名时,将需要插入的元素插入到调用元素而非body下,方法如下:
// 初始的document.body.appendChild事件
const originFn = document.body.appendChild.bind(document.body);
// 级联的样式如果也append到指定元素下面,会导致样式错乱。
const whiteList = [
'el-select-dropdown',
'el-dialog__wrapper',
'el-dropdown-menu',
'el-message-box__wrapper',
'el-tooltip__popper',
'el-date-picker',
'el-popover',
'el-popper',
];
const blackList = ['el-cascader__dropdown'];
function redirectPopup(container) {
// 子应用中需要挂载到子应用的className。样式class白名单,用子应用的样式。
document.body.appendChild = new Proxy(document.body.appendChild, {
apply: (target, thisArg, argumentsList) => {
const dom = argumentsList[0];
const isInWhiteList = whiteList.some((x) => dom.className.includes(x));
const isInBlackList = blackList.some((x) => dom.className.includes(x));
if (isInWhiteList && container.container && !isInBlackList) {
// 将需要处理的元素,挂载的元素挂载到子应用上,而不是主应用的 body 上
container.container.querySelector('#app').appendChild(dom);
} else {
return target.apply(thisArg, argumentsList);
}
},
});
}
function render(props = {}) {
const { container } = props;
// 每次渲染的时候调用redirectPopup事件
redirectPopup(props);
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 子应用卸载时候,需要还原appendChild方法;
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
document.body.appendChild = originFn;
}
到这里,就勉强能用了!!!