一、qiankun使用场景

  1. 简介:qiankun是在single-spa的基础上实现的,可以保证各个项目独立使用,也可以集成使用。各系统之间不受技术栈的限制,集成使用也能保证各样式和全局变量的隔离。

      模块的插拔式使用,当公司项目集是一个大系统下包含多个子系统或者模块时,可以采用这种方式动态部署各个系统。

      亦或者是老项目技术升级和重构,可以通过qiankun按模块进行改造,避免对整个系统产生较大的影响。

      功能和iframe类似,但是由于iframe数据通信难度较大,且有安全和SEO的问题,所以iframe使用体验不佳。

  2. 原理逻辑:

    a. 需要在各个子应用的基础上新增一个主应用,通过主应用监听路由变化。

    b. 当有路由切换时就会触发上述监听函数从而去匹配在主应用中注册的各个子应用路径(activeRule)是否匹配。

    c. 匹配到子应用后就会加载子应用的资源到对应的容器当中去。

二、实现样例

  本样例使用的是Node 16的版本,主应用采用Vue3框架,两个子应用分别使用Vue2和Vue3框架。qiankun版本是2.10.16。

  1. 搭建主应用,利用脚手架创建一个qiankun-main的主应用,同时安装qiankun组件(qiankun只需要在主应用安装,子应用不需要),其中代码中标注重点的内容是配置qiankun的关键步骤

     1.1 打开vue.config.js文件,添加跨域处理,避免跳转时出现跨域问题

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    port: 8085,
    headers: {            // 重点1: 允许跨域访问子应用页面
        'Access-Control-Allow-Origin': '*',
    }
  }
})

vue.config.js

     1.2 主应用中设置子应用接收容器

<template>
  <div class="app">
    <p><router-link to="/">点击跳转到父页面</router-link></p>
    <button @click="login">登陆</button>
    <p><router-link to="/vue3">跳转到Vue3子应用</router-link></p>
    <p><router-link to="/vue2">跳转到Vue2子应用</router-link></p>
    <router-view />
    <div id="vue3"></div>    <!-- 重点2:子应用容器 id -->
    <div id="vue2"></div> <!-- 重点2:子应用容器 id -->
  </div>
</template>

<script>
import actions from '@/shared/actions';

export default {
  name: 'App',
  components: {
  },
  mounted() {
    actions.onGlobalStateChange((state, prevState) => {
      // state: 变更后的状态; prevState: 变更前的状态
      console.log('主应用观察者:token值改为', prevState.token);
      console.log("主应用观察者:登录状态发生改变,改变后的 token 的值为 ", state.token);
    });
  },
  methods: {
    login() {
      console.log('进入登陆事件');
      setTimeout(() => {
        const token = 'token_' + Math.floor(Math.random() * 100000);
        //登陆后随机生成token并设置
        actions.setGlobalState({ token });
        this.$router.push("/vue3");
      }, 300);
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

App.vue

    1.3 在src根目录下新增public-path文件;同时改造路由,设置返回的base地址

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

public-path.js

import {
  createRouter,
  createWebHashHistory
} from 'vue-router'
import '../public-path' // 重点3: 引入public-path文件
const router = createRouter({
  base: window.__POWERED_BY_QIANKUN__ ? '/vue3' : '/', // 重点4:qiankun进入子应用时,返回true
  history: createWebHashHistory(), // 重点5
  routes: [{
          path: '/',
          redirect: '/child'
      },
      {
          path: '/child',
          component: () => import('@/components/child')
      }
  ]
})
export default router

router/index.js

    1.4 注册和引入子应用

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun'

createApp(App).use(router).mount('#app')

registerMicroApps([
  {
    name: "vue3 app",
    entry: "//localhost:8086", // 重点8:对应重点6
    container: '#vue3',         // 重点9:对应重点2
    activeRule: '/#/vue3',        // 重点10:对应重点4
    props: {
        appContent: '我是主应用传给vue的值'
    }
  },
  {
    name: "vue2 app",
    entry: "//localhost:8087", // 重点8:对应重点6
    container: '#vue2',         // 重点9:对应重点2
    activeRule: '/#/vue2',        // 重点10:对应重点4
    props: {
        appContent: '我是主应用传给Vue2的值'
    }
  }
])
setDefaultMountApp("/")  // 重点11:启动默认的子模块
// 启动
start()

main.js

  2. 搭建子应用1, 同样利用脚手架创建一个qiankun-vue3-child,项目使用Vue3作为基础框架

    2.1 同样在src目录下创建public-path.js文件

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}

public-path.js

    2.2 改造router/index.js文件, 确认在qiankun模式下的路由基础路径

import {
  createRouter,
  createWebHashHistory
} from 'vue-router'
import '../public-path' // 重点3: 引入public-path文件
const router = createRouter({
  base: window.__POWERED_BY_QIANKUN__ ? '/vue3' : '/', // 重点4:qiankun进入子应用时,返回true
  history: createWebHashHistory(), // 重点5
  routes: [
  ]
})
export default router

router/index.js

    2.3 修改入口函数main.js,导入钩子函数

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import actions from './micros/actions'

let instance = null

function render(props = {}) {
  // qiankun模式下实现父子应用之间通信
  if (props) {
    actions.setActions(props);
  }

  const { container } = props
  // 为了避免根id#app与其他DOM冲突,需要限制查找范围
  instance = createApp(App).use(router).mount(container ? container.querySelector('#child-app') : '#child-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)
  render(props)
}
export async function unmount() {
  if (instance) {
    console.log(instance, instance.unmount);
    // instance.unmount();
    instance = null
  }
}

// createApp(App).use(router).mount('#child-app')

main.js

    2.4 修改打包配置文件vue.config.js,设置服务端口以及打包模式

const { defineConfig } = require('@vue/cli-service')
const { name } = require('./package');
module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    port: 8086,            // 重点6
    headers: {            // 重点7:同重点1,允许子应用跨域
        'Access-Control-Allow-Origin': '*',
    },
  },
  // 自定义webpack配置
  configureWebpack: {
      output: {
          library: `${name}-[name]`,
          libraryTarget: 'umd',        // 把子应用打包成 umd 库格式
          // jsonpFunction: `webpackJsonp_${name}`,
      },
  },
})

vue.config.js

  3. 搭建子应用2,步骤与第2步类似,只是使用Vue2作为基础框架

三、功能演示

 四、常见问题

  1. 子应用部署在同一个服务器同一个端口的不同路径下如何配置?

    基本和部署在不同服务器的类似,只是将注册子应用的entry的服务器端口号换成某个路径,同时将打包的publicPath改为该路径

// 主应用入口文件中注册子应用
registerMicroApps([
  {
    name: "vue3_app",
    entry: "/entry_vue3", // 对应之前的 //localhost:8086
    container: '#vue3',    
    activeRule: '/#/vue3',     
    props: {
        appContent: '我是主应用传给vue的值'
    }
  }
])

// 子应用的 router/indexedDB.js
const router = createRouter({
  base: window.__POWERED_BY_QIANKUN__ ? '/vue3' : '/entry_vue3', // 设置路由路径
  history: createWebHashHistory(),
  routes: [
  ]
})

// 打包文件vue.config.js中添加默认路径
module.exports = defineConfig({
  publicPath: devFlag ? '/' : '/entry_vue3',
  transpileDependencies: true,
  devServer: {
    port: 8087,            // 重点6
    headers: {            // 重点7:同重点1,允许子应用跨域
        'Access-Control-Allow-Origin': '*',
    },
  },
  // 自定义webpack配置  重点12
  configureWebpack: {
      output: {
          library: `${name}-[name]`,
          libraryTarget: 'umd',        // 把子应用打包成 umd 库格式
          // jsonpFunction: `webpackJsonp_${name}`,
      },
  },
})

同服务器同端口部署配置

  2. 主子应用之间通信?

    2.1 使用qiankun框架提供的 initGlobalState 实现的,主要有下面三个函数:

      onGlobalStateChange(callback, Immediately)在当前应用监听全局状态变化;

      setGlobalState(state)按照一级属性进行状态设置,微应用只能修改一级属性;

      offGlobalStateChange()移除当前的状态监听,微应用在unmount时默认调用;

    2.2 使用方式,效果可见上面的案列图中对token的打印信息

      a. 主应用的src目录下新增shared/actions.js文件。

import { initGlobalState } from "qiankun";

const initialState = {
  token: 'no token'
};
const actions = initGlobalState(initialState);

export default actions;

actions.js

      b. 比如在主应用的App.vue中使用并且实现登陆后生成token以及跳转到vue3子应用

import actions from '@/shared/actions';

export default {
  name: 'App',
  components: {
  },
  mounted() {
    actions.onGlobalStateChange((state, prevState) => {
      // state: 变更后的状态; prevState: 变更前的状态
      console.log('主应用观察者:token值改为', prevState.token);
      console.log("主应用观察者:登录状态发生改变,改变后的 token 的值为 ", state.token);
    });
  },
  methods: {
    login() {
      console.log('进入登陆事件');
      setTimeout(() => {
        const token = 'token_' + Math.floor(Math.random() * 100000);
        //登陆后随机生成token并设置
        actions.setGlobalState({ token });
        this.$router.push("/vue3");
      }, 300);
    }
  }
}

App.vue

      c. 子应用中使用时首先在根目录下创建一个micros/actions.js文件

function emptyAction() {
  // 确保单独部署时不会报错
  console.warn('当前无可执行的Action');
}

class Actions {
  // 默认设置空Action
  actions = {
    onGlobalStateChange: emptyAction,
    setGlobalState: emptyAction
  }

  // 设置Actions
  setActions(actions) {
    this.actions = actions;
  }

  // 映射
  onGlobalStateChange(...args) {
    return this.actions.onGlobalStateChange(...args);
  }

  // 映射
  setGlobalState(...args) {
    return this.actions.setGlobalState(...args);
  }
}

const actions = new Actions();
export default actions;

actions.js

      d. 子应用的APP.vue页面中监听主应用中数据的变化以及子应用主动修改数据观察主应用是否能接收到

import actions from '@/micros/actions.js';

export default {
  name: 'App',
  components: {
  },
  mounted() {
    actions.onGlobalStateChange(state => {
      console.log('子应用Vue的观察函数:', state);
    }, true)
  },
  methods: {
    changeToken() {
      actions.setGlobalState({ token: 'Vue3_' + Math.floor(Math.random() * 100000) })
    }
  }
}

App.vue

  3. 各个应用之间如何提取一些公共的资源或者模块?

    可以将公共模块提取成一个公共组件发布到npm,然后由各个应用按需安装。

  4. 各个系统如何做到只登陆一次?

    可以参考单点登陆实现,简单逻辑就是比如:

    a. 有一个地址sso.com做为控制中心,然后a.com、b.com为子模块。

    b. 当访问a.com时无权限时路由会携带参数“a.com”自动跳转到登陆页面,输入用户名密码信息后,经过sso.com验证通过生成ticket并返回给页面同时跳转到a.com并下发ticket。

    c. a.com请求获取到ticket后访问sso.com的服务器进行验证是否有效,有效则允许登陆,这样就完成了一次登陆。

    d. 如果在已登录的状态下跳转到b.com,则省略第二步的登陆验证直接将ticket携带到b.com,然后再访问sso.com进行有消息验证。

 

注意:主应用注册的activeRule为/vue3时跳转到子应用不生效可能是因为浏览器路由跳转时自动加上/#/,所以在activeRule也需要修改为/#/vue3才可以跳转