单体应用改造配置说明文档
微应用不需要额外安装任何其他依赖即可接入
qiankun
。默认阅读本文档的前端开发人员掌握按照 《前端 vue 项目构建流程指导规范》搭建 vue 项目的能力,搭建过程省略,详细步骤请阅读对应文档。
本文档将说明如何改造一个按照文档创建的 vue 项目,集成到 qiankun 环境中,并且可以独立运行。
一. 配置
1. 创建并引入 public-path.js
在src/qiankun
目录中创建 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
在main.js
顶部引入
import "./qiankun/public-path";
2. 在 main.js 封装 render 方法创建 vue 实例
// new Vue({
// store,
// router,
// render: (h) => h(App)
// }).$mount('#app');
let instance = null;
function render(props = {}) {
const { container } = props;
container &&
container.setAttribute(
"style",
"height:100%;overflow-y: auto;background-color: #ffffff;"
);
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
3. 在 main.js 导出相应的生命周期钩子
微应用需要在自己的入口 js (vue 项目在 main.js) 导出 bootstrap
、mount
、unmount
三个生命周期钩子,以供 qiankun 在适当的时机调用。
在 mount 生命周期中通过 props 参数获取到基座应用传递过来的参数
- qiankunEventBus 用于 eventBus 通信
- qiankunStore 用于 vuex 通信
- setGlobalState 用于 改变 gloabalState 的值并触发全局监听
- onGlobalStateChange 用于 注册监听 gloabalState 的监听器
- qiankunCommonStore 用于 接收基座应用的 common 模块并注册到微应用的 vuex 中
// bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
export async function bootstrap() {
console.log("[vue] vue app bootstraped");
}
// 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
export async function mount(props) {
console.log("[vue] props from main framework", props);
Vue.prototype.$qiankunEventBus = props.qiankunEventBus;
Vue.prototype.$qiankunStore = props.qiankunStore;
Vue.prototype.$setGlobalState = props.setGlobalState;
Vue.prototype.$onGlobalStateChange = props.onGlobalStateChange;
// 将基座的common注册的微应用自己的vuex实例上,这样微应用就可以使用自己的vuex实例访问该模块
if (store && store.hasModule) {
if (!store.hasModule("qiankunCommonStore")) {
store.registerModule("qiankunCommonStore", props.qiankunCommonStore);
}
}
store.dispatch("common/setIsPoweredByQiankun", true);
render(props);
}
// 应用每次 切出/卸载 会调用的unmount方法,通常在这里我们会卸载微应用的应用实例
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
}
4. 配置 vue.config.js
除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:
- 允许跨域让基座加载微应用
- 配置打包格式为 umd 打包
const { defineConfig } = require("@vue/cli-service");
// 每个微应用的name必须唯一
const { name } = require("./package.json");
module.exports = defineConfig({
// 部署应用包时的基本 URL
publicPath: `/child/${name}`,
devServer: {
// 推荐固定端口,方便调试(可选)
port: 9082,
// 允许跨域让基座加载微应用
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
// 配置打包格式
output: {
library: `${name}-[name]`,
libraryTarget: "umd",
// webpack5以下使用 jsonpFunction 配置
// jsonpFunction: `webpackJsonp_${name}`
// webpack5及以上使用 chunkLoadingGlobal 配置
chunkLoadingGlobal: `webpackJsonp_${name}`,
},
},
});
5. 配置 Vuex 的 common 模块
common 模块添加 isPoweredByQiankun 用于判断当前是否处于 qiankun 环境中
function initState () {
return {
...
// 是否处于乾坤环境
isPoweredByQiankun: false
};
}
const state = initState(),
mutations = {
...
/**
* @description 设置当前是否处于乾坤环境
* @return {void}
* @example
* this.$store.commit('common/setIsPoweredByQiankun')
*/
setIsPoweredByQiankun (state, payload) {
state.isPoweredByQiankun = payload;
}
},
actions = {
...
/**
* @description 设置当前是否处于乾坤环境
* @return {void}
* @example
* this.$store.dispatch('common/setIsPoweredByQiankun')
*/
setIsPoweredByQiankun ({ commit }, payload) {
commit('setIsPoweredByQiankun', payload);
}
},
getters = {
...
/**
* @description 获取当前是否处于乾坤环境
* @return {boolean}
* @example
* this.$store.getters['common/isPoweredByQiankun']
*/
isPoweredByQiankun (state){
return state.isPoweredByQiankun;
}
};
export default {
namespaced: true,
state,
mutations,
getters,
actions
};
6. 配置路由
修改router/routes.js
每个微应用的路由地址都需要一个不重复的路由前缀,用于让 qiankun 根据当前路由匹配并启动对应的微应用。
const routes = [
{
path: "/login",
name: "Login",
component: () => import("@/views/login.vue"),
},
{
path: "/",
redirect: "/heaven-sub-app2",
},
{
path: "/index",
redirect: "/heaven-sub-app2",
},
{
path: "/heaven-sub-app2",
name: "Index",
component: () => import("@/views/index.vue"),
children: [
{
path: "example-a",
name: "App2ExampleA",
},
{
path: "example-a/1",
name: "App2ExampleA1",
component: () => import("@/views/example-a/example-a-1.vue"),
},
{
path: "/",
name: "Welcome",
component: () => import("@/views/welcome.vue"),
},
{
path: "404",
name: "NotFound",
component: () => import("@/views/404.vue"),
},
],
},
];
export default routes;
修改router/index.js
const beforeEach = function (to, from, next) {
if (["Login"].includes(to.name)) {
next();
} else {
if (store.state.common.token) {
if (["Login", "Index", "Welcome", "NotFound"].includes(to.name)) {
next();
} else {
let hasPermission = store.state.common.menus.find(
(i) => i.resourceUrl === to.path
);
// 如果跳转的路由有权限那么添加到tabs列表里面
if (hasPermission) {
next();
store.dispatch("common/addCurrentTab", {
...hasPermission,
componentsName: to.name,
});
} else {
next({ name: "NotFound" });
// next()
}
}
} else {
next({ name: "Login", replace: true });
}
}
};
router.beforeEach((to, from, next) => {
// 如果处于乾坤环境,那么权限交由基座处理
if (store.state.common.isPoweredByQiankun) {
next();
} else {
beforeEach(to, from, next);
}
});
7. 改造 index.vue
<template>
<div class="index-page flex-col">
<!-- 如果当前处于乾坤环境,那么隐藏header区域 -->
<div v-if="!isPoweredByQiankun" class="index-page-header flex-row-bw">
...
</div>
<!-- 如果当前处于乾坤环境,那么最近class -->
<div
class="index-page-container flex1 flex-row"
:class="{ 'is-powered-by-qiankun': isPoweredByQiankun }"
>
<!-- 如果当前处于乾坤环境,那么隐藏左侧菜单区域 -->
<aside-menu
v-if="!isPoweredByQiankun"
:menus="menus"
:is-collapse="isCollapse"
></aside-menu>
<div class="index-page-content">
<!-- 如果当前处于乾坤环境,那么隐藏上方tabs区域 -->
<Tabs
v-if="!isPoweredByQiankun"
:is-collapse="isCollapse"
:menus="menus"
@change-collapse="handleChangeCollapse"
></Tabs>
<div class="app-container">
<keep-alive :include="cachePages">
<router-view></router-view>
</keep-alive>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
name: 'Index',
...
computed: {
...mapGetters('common', {
// 是否处于乾坤环境
isPoweredByQiankun: 'isPoweredByQiankun'
}),
// 用户信息
userInfo () {
// 如果处于 qiankun 环境中,那么访问 qiankunCommonStore 模块,否则访问 common 模块
return this.isPoweredByQiankun
? this.$store.getters['qiankunCommonStore/getUserInfo']
: this.$store.getters['common/getUserInfo'];
},
// 获取需要缓存的路由
cachePages () {
// 如果处于 qiankun 环境中,那么访问 qiankunCommonStore 模块,否则访问 common 模块
return this.isPoweredByQiankun
? this.$store.getters['qiankunCommonStore/getCachePages']
: this.$store.getters['common/getCachePages'];
}
}
...
};
</script>
<style scoped lang="scss">
... &-container {
...
// 添加如果应用是运行到乾坤环境中的相关样式代码
&.is-powered-by-qiankun {
padding: 0;
& > .index-page-content {
margin-left: 0;
margin-bottom: 0;
.app-container {
height: 100%;
}
}
}
}
</style>
8. 微应用页面如何与主应用通信
- 通过 qiankunEventBus 进行通信,示例:
this.$qiankunEventBus.$emit("logout");
- 通过 setGlobalState 进行通信,示例:
this.$setGlobalState({
// 事件触发来源
eventFrom: "microApp",
// 事件的标识
eventCode: "logout",
// 事件传递的参数
eventData: {
time: new Date().getTime(),
},
});
- 使用 globalState 进行全局状态改变监听,示例:
this.$onGlobalStateChange((state, prev) => {
console.log("微应用监听到全局状态改变", state, prev);
});
- 通过 Vuex 进行通信,示例:
<script>
import { mapGetters } from "vuex";
export default {
computed: {
...mapGetters("common", {
// 是否处于乾坤环境
isPoweredByQiankun: "isPoweredByQiankun",
}),
// 如果处于 qiankun 环境中,那么访问 qiankunCommonStore 模块,否则访问 common 模块
userInfo() {
return this.isPoweredByQiankun
? this.$store.getters["qiankunCommonStore/getUserInfo"]
: this.$store.getters["common/getUserInfo"];
},
},
methods: {
logout() {
// 如果处于 qiankun 环境中,那么访问 qiankunCommonStore 模块,否则访问 common 模块
this.isPoweredByQiankun
? this.$store.dispatch("qiankunCommonStore/logout")
: this.$store.dispatch("common/logout");
},
},
};
</script>
9. 微应用跳转页面
- 跳转当前微应用的其他页面,推荐使用 name,示例:
this.$router.push({
name: "App2ExampleA2",
});
- 跳转其他微应用的页面,需要写完整路径,示例:
this.$router.push({
path: "/heaven-sub-app1/example-a/1",
query: {
from: "App2ExampleA1",
},
});
二. Q&A
- 路由模式应如何选择?
为了方便集成和部署,基座应用以及微应用的路由都要求使用hash
模式。 - 微应用的路由必须要加前缀吗?
是,每个微应用的路由地址都需要一个不重复的路由前缀,用于让 qiankun 根据当前路由匹配并启动对应的微应用。 - 每个页面都要定义 name 吗
是,为了保证当前已经打开的同一个微应用下的页面可以正确的被keep-alive
组件缓存。 - 使用
$setGlobalState
的修改全局数据失败?
微应用中只能修改已存在的一级属性,基座应用不受该限制。 - qiankunActions.onGlobalStateChange 事件监听被覆盖
onGlobalStateChange
只能同时创建一个监听,新创建的事件监听会覆盖上一个事件监听。推荐在 index.vue
、main.js
等仅创建一次监听,根据eventCode
做不同的动作 - class、id 选择器命名有什么注意事项吗?
为了避免影响基座的样式,请勿使用 main-app 开头的 class、id 选择器修改样式。
修改 Element-UI 等组件库的样式时,推荐限制样式作用范围如 .my-table .el-table{} - 集成后报错 Uncaught Error: application ‘heaven-demo-digital’ died in status LOADING_SOURCE_CODE: only one instance of babel-polyfill is allowed
这是因为多个应用的 babel 被重复引入了,临时解决方法如下:
在 main.js 直接引入 import “babel-polyfill”; 改为判断是否存在再引入if (!global._babelPolyfill) {
require('babel-polyfill');
}
如果还不行查看 webpack 配置中是否也引入了 babel