跳到主要内容

微前端(qiankun)集成

说明:此文档是 jeecgBoot-vue3作为主应用模式,其他项目作为子应用使用。

jeecgboot-vue3 默认已经集成qiankun,详细使用说明参考 qiankun官网,集成步骤如下

1.安装qiankun

在前端项目外层建立qiankun-jeecg 文件夹 把vue3-admin-jeecg拷贝到此文件夹下如下图

进入qiankun-vue3-jeecg/vue3-admin-jeecg 目录下执行如下命令

pnpm add qiankun

2.在主应用中注册微应用

  • 把 src/qiankun/apps.ts 文件注释的代码放开
export const containerId = 'qiankun-content'

/**
*微应用apps
* @name: 微应用名称 - 具有唯一性
* @entry: 微应用入口.必选 - 通过该地址加载微应用,
* @container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
* @activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
*/
//子应用列表
const _apps: Recordable[] = [];
for (const key in import.meta.env) {
if (key.includes('VITE_APP_SUB_')) {
const name = key.split('VITE_APP_SUB_')[1];
const obj = {
name,
entry: import.meta.env[key],
container: '#' + containerId,
activeRule: name,
};
_apps.push(obj);
}
}
export const apps = _apps;
  • 把 src/qiankun/state.ts 文件注释的代码放开
/**
*公共数据
*/
import { initGlobalState } from 'qiankun';
import { store } from '/@/store';
import { router } from '/@/router';
import { getToken } from '/@/utils/auth';
//定义传入子应用的数据
export function getProps() {
return {
data: {
publicPath: '/',
token: getToken(),
store,
router,
},
};
}

/**
* 定义全局状态,并返回通信方法,在主应用使用,微应用通过 props 获取通信方法。
* @param state 主应用穿的公共数据
*/
export function initGlState(info = { userName: 'admin' }) {
// 初始化state
const actions = initGlobalState(info);
// 设置新的值
actions.setGlobalState(info);
// 注册 观察者 函数 - 响应 globalState 变化,在 globalState 发生改变时触发该 观察者 函数。
actions.onGlobalStateChange((newState, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.info('newState', newState);
console.info('prev', prev);
for (const key in newState) {
console.info('onGlobalStateChange', key);
}
});
}
  • 把 src/qiankun/route.ts 文件注释的代码放开
import { router } from "@/router";
import { apps } from './apps';

export const {registerQiankunRouter} = (function () {

let registered = false;

/**
* 注册qiankun路由
*/
function registerQiankunRouter() {
if (!router) {
// 如果路由对象不存在,递归调用,直到路由对象可用
setTimeout(() => registerQiankunRouter(), 1);
} else {
registerQiankunRouterNow();
}
}

function registerQiankunRouterNow() {
if (registered) {
return;
}
registered = true;
const checkQiankunRoute = (path: string) => apps.some(app => path.startsWith('/' + app.name));
// 添加路由守卫
// 路由守卫,判断是否是qiankun子应用路由
router.beforeEach(async (to, from, next) => {
const isQiankunRoute = checkQiankunRoute(to.path);
if (isQiankunRoute) {
// 如果是qiankun子应用路由,设置meta属性
to.meta.isQiankunRoute = true;
} else {
// 如果不是qiankun子应用路由,清除meta属性
delete to.meta.isQiankunRoute;
}
next();
});
}


return {
registerQiankunRouter,
}
})();
  • 把 src/qiankun/index.ts 文件注释的代码放开
/**
* qiankun配置
*/
import {
start,
registerMicroApps,
runAfterFirstMounted,
addGlobalUncaughtErrorHandler
} from 'qiankun';
import { apps, containerId } from './apps';
import { getProps, initGlState } from './state';
import { registerQiankunRouter } from './route';

registerQiankunRouter();

/**
* 重构apps
*/
function filterApps() {
apps.forEach((item) => {
//主应用需要传递给微应用的数据。
// @ts-ignore
item.props = getProps();
//微应用触发的路由规则
// @ts-ignore
item.activeRule = genActiveRule('/' + item.activeRule);
});
return apps;
}

/**
* 路由监听
* @param {*} routerPrefix 前缀
*/
function genActiveRule(routerPrefix) {
return (location) => location.pathname.startsWith(routerPrefix);
}

let retryCount = 0;

/**
* 微应用注册
*/
function registerApps() {
const container = document.querySelector('#' + containerId);
if (!container) {
// 如果容器不存在,递归尝试注册应用,最多尝试10次,每次间隔500毫秒
if (retryCount < 10) {
retryCount++;
setTimeout(() => registerApps(), 500);
}
} else {
registerAppsNow();
}
}

registerApps['containerId'] = containerId;

function registerAppsNow() {
if (window.qiankunStarted) {
return;
}
window.qiankunStarted = true;
const _apps = filterApps();
// @ts-ignore
registerMicroApps(_apps, {
beforeLoad: [
// @ts-ignore
(loadApp) => {
console.log('[qiankun] before load', loadApp);
},
],
beforeMount: [
// @ts-ignore
(mountApp) => {
console.log('[qiankun] before mount', mountApp);
},
],
afterMount: [
// @ts-ignore
(mountApp) => {
console.log('[qiankun] after mount', mountApp);
},
],
beforeUnmount: [
// @ts-ignore
(unloadApp) => {
console.log('[qiankun] before unmount', unloadApp);
},
],
afterUnmount: [
// @ts-ignore
(unloadApp) => {
console.log('[qiankun] after unmount', unloadApp);
},
],
});
// 设置默认子应用,与 genActiveRule中的参数保持一致
// setDefaultMountApp();
// 第一个微应用 mount 后需要调用的方法,比如开启一些监控或者埋点脚本。
runAfterFirstMounted(() => console.log('开启监控'));
// 添加全局的未捕获异常处理器。
addGlobalUncaughtErrorHandler((event) => console.log(event));
// 定义全局状态
initGlState();
//启动qiankun
start({});
}

export default registerApps;
  • 放开 src/layouts/default/content/index.vue 文件中 注册 qiankun 注释的代码引入qiankun注册文件
<div :id="qiankunDivId" class="app-view-box" v-if="openQiankun && qiankunDivId"></div>
import registerApps from '/@/qiankun';
// 注册 qiankun
if (openQiankun) {
qiankunDivId.value = registerApps?.containerId;
registerApps();
}
  • 设置全局控制开关

.env 文件中qiankun全局控制开关设置true

VITE_GLOB_APP_OPEN_QIANKUN=true
  • 设置子应用入口地址

.env.development 文件中设置开发环境的子应用入口地址。(路由地址和 VITE_APP_SUB_ 后面保持一致,如下面qiankun-app)

#微前端qiankun应用,命名必须以VITE_APP_SUB_开头,qiankun-app为子应用的项目名称,也是子应用的路由父路径
VITE_APP_SUB_qiankun-app = '//localhost:8001/qiankun-app'。

.env.production 文件设置生产环境应用入口地址。(路由地址和 VITE_APP_SUB_ 后面保持一致,如下面qiankun-app)

#微前端qiankun应用,命名必须以VITE_APP_SUB_开头,qiankun-app为子应用的项目名称,也是子应用的路由父路径
VITE_APP_SUB_qiankun-app = '线上地址/qiankun-app'
  • src/main.ts 添加 registerApps()
+ import registerApps from '/@/qiankun';

async function bootstrap(props?: MainAppProps) {
+ registerApps();
}

  • 修改打包输出位置

修改vue3-admin-jeecg/build/constant.ts中outputDir内容定义,将打包内容输出到qiankun-vue3-jeecg文件夹下main目录下

export const OUTPUT_DIR = '../dist/main';
  • 添加全局启动打包文件(package.json) 存放位置如下图

文件内容如下

{
"name": "qiankun-jeecg",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"install": "npm-run-all install:* ",
"install:main": "cd ant-design-jeecg-vue && npm install",
"install:sub01": "cd jeecg-app-1 && npm install ",
"start": "npm-run-all start:* ",
"start:main": "cd ant-design-jeecg-vue && start cmd /k npm run serve",
"start:sub01": "cd jeecg-app-1 && start cmd /k npm run serve",
"build": "npm-run-all build:* ",
"build:main": "cd ant-design-jeecg-vue && npm run build",
"build:sub01": "cd jeecg-app-1 && npm run build"
},
"devDependencies": {
"npm-run-all": "^4.1.5"
}
}

3 子应用改造

  • 在子应用src目录下新建public-path.js文件内容如下
//用于修改运行时的 publicPath
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
  • 修改main.js文件
function render(props = {}) {
const {container} = props;
instance = new Vue({
router,
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}

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

/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
common.initGlState(props)
render(props);
}

/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}

/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
common.setCommonData(props)
common.initGlState(props)
}
  • 打包配置修改(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}`,
},
},
};
  • 子应用vue.config.js完整配置参考如下内容
const path = require("path");
const packageName = require("./package.json").name;
const node_env = process.env.NODE_ENV === "production";
// const baseUrl = process.env.VUE_APP_BASE_URL;
const baseUrl = "/";
const resolve = (dir) => path.join(__dirname, dir);
module.exports = {
//打包输入目录
outputDir: `../dist/${packageName}`,
publicPath: node_env ? baseUrl : "/",
assetsDir: "static",
configureWebpack: {
resolve: {
alias: {
"@": resolve("src"),
},
},
output: {
library: `${packageName}-[name]`,
libraryTarget: "umd", // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${packageName}`,
},
},
devServer: {
hot: true,
disableHostCheck: true,
host:'localhost',
port: 8092,
headers: {
"Access-Control-Allow-Origin": "*", // 主应用获取子应用时跨域响应头
},
},
};

效果: