Skip to content
索引

实战

以主应用vue2,微应用vue2,vue3,react为例

主应用配置-vue2

1.下载qiankun

sh
# yarn
yarn add qiankun 
# npm
npm i -s qiankun
# yarn
yarn add qiankun 
# npm
npm i -s qiankun

2.设置main.js

js
import Vue from "vue"
import App from "./App.vue"
import router from "./router"
import store from "./store"

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app")

import { registerMicroApps, runAfterFirstMounted, start } from "qiankun"

/**
 * Step1 注册子应用
 */
registerMicroApps([
  {
    name: "react15", // 微应用的package.json的name
    entry: "//localhost:7102",
    container: "#subapp-viewport",
    activeRule: "/react15"
  },
  {
    name: "vue3", // 微应用的package.json的name
    entry: "//localhost:8080",
    container: "#subapp-viewport",
    activeRule: "/vue3"
  }
])

/** Step2 启动应用 */
start()

/** 第一次加载完成运行 */
runAfterFirstMounted(() => {
  console.log("[qiankun 主应用]加载完毕")
})
import Vue from "vue"
import App from "./App.vue"
import router from "./router"
import store from "./store"

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app")

import { registerMicroApps, runAfterFirstMounted, start } from "qiankun"

/**
 * Step1 注册子应用
 */
registerMicroApps([
  {
    name: "react15", // 微应用的package.json的name
    entry: "//localhost:7102",
    container: "#subapp-viewport",
    activeRule: "/react15"
  },
  {
    name: "vue3", // 微应用的package.json的name
    entry: "//localhost:8080",
    container: "#subapp-viewport",
    activeRule: "/vue3"
  }
])

/** Step2 启动应用 */
start()

/** 第一次加载完成运行 */
runAfterFirstMounted(() => {
  console.log("[qiankun 主应用]加载完毕")
})

这样注册的话,微应用直接跟主应用路由关联,也就是主应用路由切换,则微应用相应切换

流程逻辑:浏览器的url发生变化,qiankun的匹配逻辑触发,activeRule规则匹配到的微应用会被插入到指定的container容器(dom元素)中,当微应用信息注册完之后,同时依次调用微应用暴露出的生命周期钩子

3.设置vue文件

设置微应用加载的dom元素,qiankun微应用的container容器,registerMicroApps注册的container字段的值

vue
<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </nav>
    <router-view />
    <!-- 微应用加载的dom元素 -->
    <main id="subapp-viewport"></main>
  </div>
</template>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

nav {
  padding: 30px;

  a {
    font-weight: bold;
    color: #2c3e50;

    &.router-link-exact-active {
      color: #42b983;
    }
  }
}

/* 设置微应用加载的dom元素的高度 */
#subapp-container {
  height: 500px;
}
</style>
<template>
  <div id="app">
    <nav>
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </nav>
    <router-view />
    <!-- 微应用加载的dom元素 -->
    <main id="subapp-viewport"></main>
  </div>
</template>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

nav {
  padding: 30px;

  a {
    font-weight: bold;
    color: #2c3e50;

    &.router-link-exact-active {
      color: #42b983;
    }
  }
}

/* 设置微应用加载的dom元素的高度 */
#subapp-container {
  height: 500px;
}
</style>

微应用配置-vue3

主应用安装qiankun即可,微应用不需要安装qiankun

1. 导出相应的生命周期钩子

微应用需要在自己的入口 js (通常就是配置的webpack的entry js,使用脚手架的话是main.js) 导出 bootstrapmountunmount 三个生命周期钩子,以供主应用在适当的时机调用

ts
import "./public-path"
import { createApp } from "vue"
import { createRouter, createWebHistory } from "vue-router"
import App from "./App.vue"
import routes from "./router"
import store from "./store"

let router = null
let instance: any = null
let history: any = null

function render(props: any = {}) {
  const { container } = props
  history = createWebHistory((window as any).__POWERED_BY_QIANKUN__ ? "/vue3" : "/")
  router = createRouter({
    history,
    routes
  })

  instance = createApp(App)
  instance.use(router)
  instance.use(store)
  instance.mount(container ? container.querySelector("#app") : "#app")
}

if (!(window as any).__POWERED_BY_QIANKUN__) {
  render()
}

export async function bootstrap() {
  console.log("%c%s", "color: green;", "vue3.0 app bootstraped")
}

function storeTest(props: any) {
  props.onGlobalStateChange && props.onGlobalStateChange((value: any, prev: any) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev), true)
  props.setGlobalState &&
    props.setGlobalState({
      ignore: props.name,
      user: {
        name: props.name
      }
    })
}

export async function mount(props: any) {
  storeTest(props)
  render(props)
  instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange
  instance.config.globalProperties.$setGlobalState = props.setGlobalState
}

export async function unmount() {
  instance.unmount()
  instance._container.innerHTML = ""
  instance = null
  router = null
  history.destroy()
}
import "./public-path"
import { createApp } from "vue"
import { createRouter, createWebHistory } from "vue-router"
import App from "./App.vue"
import routes from "./router"
import store from "./store"

let router = null
let instance: any = null
let history: any = null

function render(props: any = {}) {
  const { container } = props
  history = createWebHistory((window as any).__POWERED_BY_QIANKUN__ ? "/vue3" : "/")
  router = createRouter({
    history,
    routes
  })

  instance = createApp(App)
  instance.use(router)
  instance.use(store)
  instance.mount(container ? container.querySelector("#app") : "#app")
}

if (!(window as any).__POWERED_BY_QIANKUN__) {
  render()
}

export async function bootstrap() {
  console.log("%c%s", "color: green;", "vue3.0 app bootstraped")
}

function storeTest(props: any) {
  props.onGlobalStateChange && props.onGlobalStateChange((value: any, prev: any) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev), true)
  props.setGlobalState &&
    props.setGlobalState({
      ignore: props.name,
      user: {
        name: props.name
      }
    })
}

export async function mount(props: any) {
  storeTest(props)
  render(props)
  instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange
  instance.config.globalProperties.$setGlobalState = props.setGlobalState
}

export async function unmount() {
  instance.unmount()
  instance._container.innerHTML = ""
  instance = null
  router = null
  history.destroy()
}

2.打包配置

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

webpack.config.js(vue脚手架的话则为vue.config.js

js
const { defineConfig } = require("@vue/cli-service")
const { name } = require("./package")
const path = require("path")
function resolve(dir) {
  return path.join(__dirname, dir)
}

module.exports = defineConfig({
  transpileDependencies: true,
  // 自定义webpack配置
  configureWebpack: {
    resolve: {
      alias: {
        "@": resolve("src")
      }
    },
    output: {
      // qiankun框架需要微应用打包成umd格式
      library: `${name}-[name]`,
      libraryTarget: "umd",
      chunkLoadingGlobal: `webpackJsonp_${name}`
    }
  },
  devServer: {
    // 主子应用间访问需要允许跨域
    headers: {
      "Access-Control-Allow-Origin": "*"
    }
  }
})
const { defineConfig } = require("@vue/cli-service")
const { name } = require("./package")
const path = require("path")
function resolve(dir) {
  return path.join(__dirname, dir)
}

module.exports = defineConfig({
  transpileDependencies: true,
  // 自定义webpack配置
  configureWebpack: {
    resolve: {
      alias: {
        "@": resolve("src")
      }
    },
    output: {
      // qiankun框架需要微应用打包成umd格式
      library: `${name}-[name]`,
      libraryTarget: "umd",
      chunkLoadingGlobal: `webpackJsonp_${name}`
    }
  },
  devServer: {
    // 主子应用间访问需要允许跨域
    headers: {
      "Access-Control-Allow-Origin": "*"
    }
  }
})

3.新增public-path.js

src 目录新增 public-path.js

js
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

在入口文件顶层如main.js中导入public-path.js

js
import "./public-path"
import "./public-path"

vue-devtools无法调试子项目

https://github.com/umijs/qiankun/issues/601

同一页面多个微应用

使用loadMicroApp手动加载微应用

js
<template>
  <div id="app">
    <div id="subapp-container"></div>
    <div id="subapp-container1"></div>
  </div>
</template>

<script>
import { loadMicroApp } from "qiankun"

export default {
  mounted() {
    loadMicroApp({
      name: "vuesub", // 微应用的名称,微应用之间必须确保唯一
      entry: "//localhost:9002", // 必选,微应用的入口
      container: "#subapp-container" // 必选,微应用的容器节点的选择器或者 Element 实例
    })
    loadMicroApp({
      name: "secondsub", // 微应用的名称,微应用之间必须确保唯一
      entry: "//localhost:9001", // 必选,微应用的入口
      container: "#subapp-container1" // 必选,微应用的容器节点的选择器或者 Element 实例
    })
  }
}
</script>
<template>
  <div id="app">
    <div id="subapp-container"></div>
    <div id="subapp-container1"></div>
  </div>
</template>

<script>
import { loadMicroApp } from "qiankun"

export default {
  mounted() {
    loadMicroApp({
      name: "vuesub", // 微应用的名称,微应用之间必须确保唯一
      entry: "//localhost:9002", // 必选,微应用的入口
      container: "#subapp-container" // 必选,微应用的容器节点的选择器或者 Element 实例
    })
    loadMicroApp({
      name: "secondsub", // 微应用的名称,微应用之间必须确保唯一
      entry: "//localhost:9001", // 必选,微应用的入口
      container: "#subapp-container1" // 必选,微应用的容器节点的选择器或者 Element 实例
    })
  }
}
</script>

多个微应用间样式隔绝

默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离,当配置为 { strictStyleIsolation: true } 时表示开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响

{ experimentalStyleIsolation: true } 时,qiankun 会改写子应用所添加的样式为所有样式规则增加一个特殊的选择器规则来限定其影响范围

默认直接使用 { strictStyleIsolation: true }会报错,使用{ experimentalStyleIsolation: true }则正常使用,样式隔绝了

js
 loadMicroApp(
      {
        name: "vuesub", // 微应用的名称,微应用之间必须确保唯一
        entry: "//localhost:9002", // 必选,微应用的入口
        container: "#subapp-container" // 必选,微应用的容器节点的选择器或者 Element 实例
      },
      {
        sandbox: {
          experimentalStyleIsolation: true
        }
      }
    )
    loadMicroApp(
      {
        name: "secondsub", // 微应用的名称,微应用之间必须确保唯一
        entry: "//localhost:9001", // 必选,微应用的入口
        container: "#subapp-container1" // 必选,微应用的容器节点的选择器或者 Element 实例
      },
      {
        sandbox: {
          experimentalStyleIsolation: true
        }
      }
    )
 loadMicroApp(
      {
        name: "vuesub", // 微应用的名称,微应用之间必须确保唯一
        entry: "//localhost:9002", // 必选,微应用的入口
        container: "#subapp-container" // 必选,微应用的容器节点的选择器或者 Element 实例
      },
      {
        sandbox: {
          experimentalStyleIsolation: true
        }
      }
    )
    loadMicroApp(
      {
        name: "secondsub", // 微应用的名称,微应用之间必须确保唯一
        entry: "//localhost:9001", // 必选,微应用的入口
        container: "#subapp-container1" // 必选,微应用的容器节点的选择器或者 Element 实例
      },
      {
        sandbox: {
          experimentalStyleIsolation: true
        }
      }
    )

设置主应用启动后默认进入的微应用

js
import { setDefaultMountApp } from 'qiankun'
setDefaultMountApp('/homeApp')
import { setDefaultMountApp } from 'qiankun'
setDefaultMountApp('/homeApp')

一键启动主应用和微应用

根目录安装 npm-run-all,该包可以并行运行多个命令

sh
 npm i -D npm-run-all
 npm init -y
 npm i -D npm-run-all
 npm init -y

package.json添加scripts,--parallel:表示并行运行多个命令

json
  "scripts": {
    "instasll-all": "npm-run-all --parallel install:*",
    "install:master": "cd projects/master & npm i",
    "install:vue-sub": "cd projects/vue-sub & npm i",
    "serve-all": "npm-run-all --parallel serve:*",
    "serve:master": "cd projects/master & npm run serve",
    "serve:vue-sub": "cd projects/vue-sub & npm run serve",
    "serve-:react-sub": "cd projects/react-sub & npm run serve",
    "build-all": "npm-run-all --parallel build:*",
    "build:master": "cd projects/master & npm run build",
    "build:vue-sub": "cd projects/vue-sub & npm run build"
  },
  "scripts": {
    "instasll-all": "npm-run-all --parallel install:*",
    "install:master": "cd projects/master & npm i",
    "install:vue-sub": "cd projects/vue-sub & npm i",
    "serve-all": "npm-run-all --parallel serve:*",
    "serve:master": "cd projects/master & npm run serve",
    "serve:vue-sub": "cd projects/vue-sub & npm run serve",
    "serve-:react-sub": "cd projects/react-sub & npm run serve",
    "build-all": "npm-run-all --parallel build:*",
    "build:master": "cd projects/master & npm run build",
    "build:vue-sub": "cd projects/vue-sub & npm run build"
  },

常用的参数

sh
--parallel: 并行运行多个命令,例如:npm-run-all --parallel lint build
--serial: 多个命令按排列顺序执行,例如:npm-run-all --serial clean lint build:**
--continue-on-error: 是否忽略错误,添加此参数 npm-run-all 会自动退出出错的命令,继续运行正常的
--race: 添加此参数之后,只要有一个命令运行出错,那么 npm-run-all 就会结束掉全部的命令
--parallel: 并行运行多个命令,例如:npm-run-all --parallel lint build
--serial: 多个命令按排列顺序执行,例如:npm-run-all --serial clean lint build:**
--continue-on-error: 是否忽略错误,添加此参数 npm-run-all 会自动退出出错的命令,继续运行正常的
--race: 添加此参数之后,只要有一个命令运行出错,那么 npm-run-all 就会结束掉全部的命令

主子应用间通信

initGlobalState

定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法

主应用通过initGlobalState传递值,并监听值的变化

js
const state = {
  token: "1111-2222-3333"
}
// 初始化state
const actions = initGlobalState(state)

// 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback
actions.onGlobalStateChange((state, prev) => {
  // state变更后的状态  prev变更前的状态
  console.log(state, prev)
})
actions.setGlobalState(state) // 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
actions.offGlobalStateChange() // 移除当前应用的状态监听,微应用umount时会默认调用
const state = {
  token: "1111-2222-3333"
}
// 初始化state
const actions = initGlobalState(state)

// 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback
actions.onGlobalStateChange((state, prev) => {
  // state变更后的状态  prev变更前的状态
  console.log(state, prev)
})
actions.setGlobalState(state) // 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
actions.offGlobalStateChange() // 移除当前应用的状态监听,微应用umount时会默认调用

子应用通过mount生命周期里的props接收

ts
function storeTest(props: any) {
  props.onGlobalStateChange &&
    props.onGlobalStateChange((value: any, prev: any) => {
      console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev)
    }, true)
  props.setGlobalState &&
    props.setGlobalState({
      ignore: props.name,
      user: {
        name: props.name
      }
    })
}

export async function mount(props: any) {
  storeTest(props)
  render(props)
  instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange
  instance.config.globalProperties.$setGlobalState = props.setGlobalState
}
function storeTest(props: any) {
  props.onGlobalStateChange &&
    props.onGlobalStateChange((value: any, prev: any) => {
      console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev)
    }, true)
  props.setGlobalState &&
    props.setGlobalState({
      ignore: props.name,
      user: {
        name: props.name
      }
    })
}

export async function mount(props: any) {
  storeTest(props)
  render(props)
  instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange
  instance.config.globalProperties.$setGlobalState = props.setGlobalState
}

loadMicroApp

主应用

js
 loadMicroApp(
      {
        name: "vuesub", // 微应用的名称,微应用之间必须确保唯一
        entry: "//localhost:9002", // 必选,微应用的入口
        container: "#subapp-container", // 必选,微应用的容器节点的选择器或者 Element 实例
        props: {
          hello: "world"
        }
      },
      {
        sandbox: {
          experimentalStyleIsolation: true
        }
      }
    )
 loadMicroApp(
      {
        name: "vuesub", // 微应用的名称,微应用之间必须确保唯一
        entry: "//localhost:9002", // 必选,微应用的入口
        container: "#subapp-container", // 必选,微应用的容器节点的选择器或者 Element 实例
        props: {
          hello: "world"
        }
      },
      {
        sandbox: {
          experimentalStyleIsolation: true
        }
      }
    )

子应用通过mount生命周期里的props接收

js
export const mount = async props => {
  console.log("第一个vue子应用加载")
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log("[微应用获取的全局值]", state, prev)
    // 第二个参数为true,表示立即触发
  }, true)
  // 打印props,发现多了一个hello属性,值为world
  console.log("[微应用props]", props)
  render(props)
}
export const mount = async props => {
  console.log("第一个vue子应用加载")
  props.onGlobalStateChange((state, prev) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log("[微应用获取的全局值]", state, prev)
    // 第二个参数为true,表示立即触发
  }, true)
  // 打印props,发现多了一个hello属性,值为world
  console.log("[微应用props]", props)
  render(props)
}

子应用向主应用

子应用向主应用发送数据(子应用中修改数据,可以在主应用中监听到)

子应用render函数详解

props.container是子应用的根dom

所以container ? container.querySelector("#app") : "#app")的作用是如果子应用的根dom存在,那么基于根dom,用querySelector,这样就限制了查询dom的区间,避免子应用根dom相同的话出现冲突

js
function render(props = {}) {
  const { container } = props
  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount(container ? container.querySelector("#app") : "#app")
}

if (!window.__POWERED_BY_QIANKUN__) {
  render()
}
function render(props = {}) {
  const { container } = props
  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount(container ? container.querySelector("#app") : "#app")
}

if (!window.__POWERED_BY_QIANKUN__) {
  render()
}

子项目跳转主项目,主项目css未加载

https://github.com/umijs/qiankun/issues/578#

Released under the MIT License.