单体应用改造配置说明文档
微应用不需要额外安装任何其他依赖即可接入
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({// 部署应用包时的基本 URLpublicPath: `/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 --><divclass="index-page-container flex1 flex-row":class="{ 'is-powered-by-qiankun': isPoweredByQiankun }"><!-- 如果当前处于乾坤环境,那么隐藏左侧菜单区域 --><aside-menuv-if="!isPoweredByQiankun":menus="menus":is-collapse="isCollapse"></aside-menu><div class="index-page-content"><!-- 如果当前处于乾坤环境,那么隐藏上方tabs区域 --><Tabsv-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
