单体应用改造配置说明文档

微应用不需要额外安装任何其他依赖即可接入 qiankun

默认阅读本文档的前端开发人员掌握按照 《前端 vue 项目构建流程指导规范》搭建 vue 项目的能力,搭建过程省略,详细步骤请阅读对应文档。

本文档将说明如何改造一个按照文档创建的 vue 项目,集成到 qiankun 环境中,并且可以独立运行。

一. 配置

1. 创建并引入 public-path.js

src/qiankun目录中创建 public-path.js

  1. if (window.__POWERED_BY_QIANKUN__) {
  2. // eslint-disable-next-line
  3. __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  4. }

main.js顶部引入

  1. import "./qiankun/public-path";

2. 在 main.js 封装 render 方法创建 vue 实例

  1. // new Vue({
  2. // store,
  3. // router,
  4. // render: (h) => h(App)
  5. // }).$mount('#app');
  6. let instance = null;
  7. function render(props = {}) {
  8. const { container } = props;
  9. container &&
  10. container.setAttribute(
  11. "style",
  12. "height:100%;overflow-y: auto;background-color: #ffffff;"
  13. );
  14. instance = new Vue({
  15. router,
  16. store,
  17. render: (h) => h(App),
  18. }).$mount(container ? container.querySelector("#app") : "#app");
  19. }
  20. // 独立运行时
  21. if (!window.__POWERED_BY_QIANKUN__) {
  22. render();
  23. }

3. 在 main.js 导出相应的生命周期钩子

微应用需要在自己的入口 js (vue 项目在 main.js) 导出 bootstrapmountunmount 三个生命周期钩子,以供 qiankun 在适当的时机调用。

在 mount 生命周期中通过 props 参数获取到基座应用传递过来的参数

  1. qiankunEventBus 用于 eventBus 通信
  2. qiankunStore 用于 vuex 通信
  3. setGlobalState 用于 改变 gloabalState 的值并触发全局监听
  4. onGlobalStateChange 用于 注册监听 gloabalState 的监听器
  5. qiankunCommonStore 用于 接收基座应用的 common 模块并注册到微应用的 vuex 中
  1. // bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
  2. export async function bootstrap() {
  3. console.log("[vue] vue app bootstraped");
  4. }
  5. // 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
  6. export async function mount(props) {
  7. console.log("[vue] props from main framework", props);
  8. Vue.prototype.$qiankunEventBus = props.qiankunEventBus;
  9. Vue.prototype.$qiankunStore = props.qiankunStore;
  10. Vue.prototype.$setGlobalState = props.setGlobalState;
  11. Vue.prototype.$onGlobalStateChange = props.onGlobalStateChange;
  12. // 将基座的common注册的微应用自己的vuex实例上,这样微应用就可以使用自己的vuex实例访问该模块
  13. if (store && store.hasModule) {
  14. if (!store.hasModule("qiankunCommonStore")) {
  15. store.registerModule("qiankunCommonStore", props.qiankunCommonStore);
  16. }
  17. }
  18. store.dispatch("common/setIsPoweredByQiankun", true);
  19. render(props);
  20. }
  21. // 应用每次 切出/卸载 会调用的unmount方法,通常在这里我们会卸载微应用的应用实例
  22. export async function unmount() {
  23. instance.$destroy();
  24. instance.$el.innerHTML = "";
  25. instance = null;
  26. }

4. 配置 vue.config.js

除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:

  1. 允许跨域让基座加载微应用
  2. 配置打包格式为 umd 打包
  1. const { defineConfig } = require("@vue/cli-service");
  2. // 每个微应用的name必须唯一
  3. const { name } = require("./package.json");
  4. module.exports = defineConfig({
  5. // 部署应用包时的基本 URL
  6. publicPath: `/child/${name}`,
  7. devServer: {
  8. // 推荐固定端口,方便调试(可选)
  9. port: 9082,
  10. // 允许跨域让基座加载微应用
  11. headers: {
  12. "Access-Control-Allow-Origin": "*",
  13. },
  14. },
  15. configureWebpack: {
  16. // 配置打包格式
  17. output: {
  18. library: `${name}-[name]`,
  19. libraryTarget: "umd",
  20. // webpack5以下使用 jsonpFunction 配置
  21. // jsonpFunction: `webpackJsonp_${name}`
  22. // webpack5及以上使用 chunkLoadingGlobal 配置
  23. chunkLoadingGlobal: `webpackJsonp_${name}`,
  24. },
  25. },
  26. });

5. 配置 Vuex 的 common 模块

common 模块添加 isPoweredByQiankun 用于判断当前是否处于 qiankun 环境中

  1. function initState () {
  2. return {
  3. ...
  4. // 是否处于乾坤环境
  5. isPoweredByQiankun: false
  6. };
  7. }
  8. const state = initState(),
  9. mutations = {
  10. ...
  11. /**
  12. * @description 设置当前是否处于乾坤环境
  13. * @return {void}
  14. * @example
  15. * this.$store.commit('common/setIsPoweredByQiankun')
  16. */
  17. setIsPoweredByQiankun (state, payload) {
  18. state.isPoweredByQiankun = payload;
  19. }
  20. },
  21. actions = {
  22. ...
  23. /**
  24. * @description 设置当前是否处于乾坤环境
  25. * @return {void}
  26. * @example
  27. * this.$store.dispatch('common/setIsPoweredByQiankun')
  28. */
  29. setIsPoweredByQiankun ({ commit }, payload) {
  30. commit('setIsPoweredByQiankun', payload);
  31. }
  32. },
  33. getters = {
  34. ...
  35. /**
  36. * @description 获取当前是否处于乾坤环境
  37. * @return {boolean}
  38. * @example
  39. * this.$store.getters['common/isPoweredByQiankun']
  40. */
  41. isPoweredByQiankun (state){
  42. return state.isPoweredByQiankun;
  43. }
  44. };
  45. export default {
  46. namespaced: true,
  47. state,
  48. mutations,
  49. getters,
  50. actions
  51. };

6. 配置路由

修改router/routes.js

每个微应用的路由地址都需要一个不重复的路由前缀,用于让 qiankun 根据当前路由匹配并启动对应的微应用。

  1. const routes = [
  2. {
  3. path: "/login",
  4. name: "Login",
  5. component: () => import("@/views/login.vue"),
  6. },
  7. {
  8. path: "/",
  9. redirect: "/heaven-sub-app2",
  10. },
  11. {
  12. path: "/index",
  13. redirect: "/heaven-sub-app2",
  14. },
  15. {
  16. path: "/heaven-sub-app2",
  17. name: "Index",
  18. component: () => import("@/views/index.vue"),
  19. children: [
  20. {
  21. path: "example-a",
  22. name: "App2ExampleA",
  23. },
  24. {
  25. path: "example-a/1",
  26. name: "App2ExampleA1",
  27. component: () => import("@/views/example-a/example-a-1.vue"),
  28. },
  29. {
  30. path: "/",
  31. name: "Welcome",
  32. component: () => import("@/views/welcome.vue"),
  33. },
  34. {
  35. path: "404",
  36. name: "NotFound",
  37. component: () => import("@/views/404.vue"),
  38. },
  39. ],
  40. },
  41. ];
  42. export default routes;

修改router/index.js

  1. const beforeEach = function (to, from, next) {
  2. if (["Login"].includes(to.name)) {
  3. next();
  4. } else {
  5. if (store.state.common.token) {
  6. if (["Login", "Index", "Welcome", "NotFound"].includes(to.name)) {
  7. next();
  8. } else {
  9. let hasPermission = store.state.common.menus.find(
  10. (i) => i.resourceUrl === to.path
  11. );
  12. // 如果跳转的路由有权限那么添加到tabs列表里面
  13. if (hasPermission) {
  14. next();
  15. store.dispatch("common/addCurrentTab", {
  16. ...hasPermission,
  17. componentsName: to.name,
  18. });
  19. } else {
  20. next({ name: "NotFound" });
  21. // next()
  22. }
  23. }
  24. } else {
  25. next({ name: "Login", replace: true });
  26. }
  27. }
  28. };
  29. router.beforeEach((to, from, next) => {
  30. // 如果处于乾坤环境,那么权限交由基座处理
  31. if (store.state.common.isPoweredByQiankun) {
  32. next();
  33. } else {
  34. beforeEach(to, from, next);
  35. }
  36. });

7. 改造 index.vue

  1. <template>
  2. <div class="index-page flex-col">
  3. <!-- 如果当前处于乾坤环境,那么隐藏header区域 -->
  4. <div v-if="!isPoweredByQiankun" class="index-page-header flex-row-bw">
  5. ...
  6. </div>
  7. <!-- 如果当前处于乾坤环境,那么最近class -->
  8. <div
  9. class="index-page-container flex1 flex-row"
  10. :class="{ 'is-powered-by-qiankun': isPoweredByQiankun }"
  11. >
  12. <!-- 如果当前处于乾坤环境,那么隐藏左侧菜单区域 -->
  13. <aside-menu
  14. v-if="!isPoweredByQiankun"
  15. :menus="menus"
  16. :is-collapse="isCollapse"
  17. ></aside-menu>
  18. <div class="index-page-content">
  19. <!-- 如果当前处于乾坤环境,那么隐藏上方tabs区域 -->
  20. <Tabs
  21. v-if="!isPoweredByQiankun"
  22. :is-collapse="isCollapse"
  23. :menus="menus"
  24. @change-collapse="handleChangeCollapse"
  25. ></Tabs>
  26. <div class="app-container">
  27. <keep-alive :include="cachePages">
  28. <router-view></router-view>
  29. </keep-alive>
  30. </div>
  31. </div>
  32. </div>
  33. </div>
  34. </template>
  35. <script>
  36. import { mapGetters, mapActions } from 'vuex';
  37. export default {
  38. name: 'Index',
  39. ...
  40. computed: {
  41. ...mapGetters('common', {
  42. // 是否处于乾坤环境
  43. isPoweredByQiankun: 'isPoweredByQiankun'
  44. }),
  45. // 用户信息
  46. userInfo () {
  47. // 如果处于 qiankun 环境中,那么访问 qiankunCommonStore 模块,否则访问 common 模块
  48. return this.isPoweredByQiankun
  49. ? this.$store.getters['qiankunCommonStore/getUserInfo']
  50. : this.$store.getters['common/getUserInfo'];
  51. },
  52. // 获取需要缓存的路由
  53. cachePages () {
  54. // 如果处于 qiankun 环境中,那么访问 qiankunCommonStore 模块,否则访问 common 模块
  55. return this.isPoweredByQiankun
  56. ? this.$store.getters['qiankunCommonStore/getCachePages']
  57. : this.$store.getters['common/getCachePages'];
  58. }
  59. }
  60. ...
  61. };
  62. </script>
  63. <style scoped lang="scss">
  64. ... &-container {
  65. ...
  66. // 添加如果应用是运行到乾坤环境中的相关样式代码
  67. &.is-powered-by-qiankun {
  68. padding: 0;
  69. & > .index-page-content {
  70. margin-left: 0;
  71. margin-bottom: 0;
  72. .app-container {
  73. height: 100%;
  74. }
  75. }
  76. }
  77. }
  78. </style>

8. 微应用页面如何与主应用通信

  1. 通过 qiankunEventBus 进行通信,示例:
    1. this.$qiankunEventBus.$emit("logout");
  1. 通过 setGlobalState 进行通信,示例:
    1. this.$setGlobalState({
    2. // 事件触发来源
    3. eventFrom: "microApp",
    4. // 事件的标识
    5. eventCode: "logout",
    6. // 事件传递的参数
    7. eventData: {
    8. time: new Date().getTime(),
    9. },
    10. });
  1. 使用 globalState 进行全局状态改变监听,示例:
    1. this.$onGlobalStateChange((state, prev) => {
    2. console.log("微应用监听到全局状态改变", state, prev);
    3. });
  1. 通过 Vuex 进行通信,示例:
    1. <script>
    2. import { mapGetters } from "vuex";
    3. export default {
    4. computed: {
    5. ...mapGetters("common", {
    6. // 是否处于乾坤环境
    7. isPoweredByQiankun: "isPoweredByQiankun",
    8. }),
    9. // 如果处于 qiankun 环境中,那么访问 qiankunCommonStore 模块,否则访问 common 模块
    10. userInfo() {
    11. return this.isPoweredByQiankun
    12. ? this.$store.getters["qiankunCommonStore/getUserInfo"]
    13. : this.$store.getters["common/getUserInfo"];
    14. },
    15. },
    16. methods: {
    17. logout() {
    18. // 如果处于 qiankun 环境中,那么访问 qiankunCommonStore 模块,否则访问 common 模块
    19. this.isPoweredByQiankun
    20. ? this.$store.dispatch("qiankunCommonStore/logout")
    21. : this.$store.dispatch("common/logout");
    22. },
    23. },
    24. };
    25. </script>

9. 微应用跳转页面

  1. 跳转当前微应用的其他页面,推荐使用 name,示例:
    1. this.$router.push({
    2. name: "App2ExampleA2",
    3. });
  1. 跳转其他微应用的页面,需要写完整路径,示例:
    1. this.$router.push({
    2. path: "/heaven-sub-app1/example-a/1",
    3. query: {
    4. from: "App2ExampleA1",
    5. },
    6. });

二. Q&A

  1. 路由模式应如何选择?
    为了方便集成和部署,基座应用以及微应用的路由都要求使用 hash 模式。
  2. 微应用的路由必须要加前缀吗?
    是,每个微应用的路由地址都需要一个不重复的路由前缀,用于让 qiankun 根据当前路由匹配并启动对应的微应用。
  3. 每个页面都要定义 name 吗
    是,为了保证当前已经打开的同一个微应用下的页面可以正确的被 keep-alive组件缓存。
  4. 使用$setGlobalState的修改全局数据失败?
    微应用中只能修改已存在的一级属性,基座应用不受该限制。
  5. qiankunActions.onGlobalStateChange 事件监听被覆盖
    onGlobalStateChange只能同时创建一个监听,新创建的事件监听会覆盖上一个事件监听。推荐在 index.vuemain.js 等仅创建一次监听,根据eventCode做不同的动作
  6. class、id 选择器命名有什么注意事项吗?
    为了避免影响基座的样式,请勿使用 main-app 开头的 class、id 选择器修改样式。
    修改 Element-UI 等组件库的样式时,推荐限制样式作用范围如 .my-table .el-table{}
  7. 集成后报错 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”; 改为判断是否存在再引入
    1. if (!global._babelPolyfill) {
    2. require('babel-polyfill');
    3. }

    如果还不行查看 webpack 配置中是否也引入了 babel