阿里 Qiankun 乾坤微前端学习
✨ 阿里 Qiankun 乾坤微前端学习
此 Demo 以Vue 3作为基座主应用,子应用分别为使用了create-react-app、vue-cli、vite创建的React 18、Vue2、Vue3项目
编写此笔记时所使用的Qiankun版本为2.x
相关文档
主应用构建
新建一个qiankun-demo目录,这里将使用pnpm的monorepo模式管理各项目
初始化项目
mkdir qiankun-demo && cd qiankun-demo
mkdir apps
pnpm init
touch pnpm-workspace.yaml
pnpm add -wD typescript @types/node
touch tsconfig.json
touch .gitignore2
3
4
5
6
7
packages:
- 'apps/*'2
{
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "Node",
"allowJs": true,
"sourceMap": true,
"strict": true,
"noEmit": true,
"declaration": true,
"isolatedModules": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noImplicitAny": true,
"strictNullChecks": false,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"types": ["node"]
},
"exclude": ["node_modules", "dist", "build"]
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
node_modules/
.DS_Store
dist/
build/
.vscode/
.idea
*.iml2
3
4
5
6
7
创建主应用(Vue 3)
cd apps
pnpm create vue main-app --typescript
cd main-app
pnpm install
pnpm add qiankun2
3
4
5
编辑src/main.ts,初始化 qiankun
import { createApp } from 'vue'
import App from './App.vue'
import { registerMicroApps, start } from 'qiankun'
import type { MicroApp as MicroAppType } from 'qiankun'
const app = createApp(App)
app.mount('#app')
// 定义子应用配置
const microApps: { name: string; entry: string; container: string; activeRule: string }[] = [
{
name: 'child-react18',
entry: '//localhost:3100',
container: '#subapp-container',
activeRule: '/child-react18',
},
{
name: 'child-vue2',
entry: '//localhost:3200',
container: '#subapp-container',
activeRule: '/child-vue2',
},
{
name: 'child-vue3',
entry: '//localhost:3300',
container: '#subapp-container',
activeRule: '/child-vue3',
},
]
// 注册子应用
registerMicroApps(microApps, {
beforeLoad: [
(app) => {
console.log('[主应用] before load', app.name)
return Promise.resolve()
},
],
beforeMount: [
(app) => {
console.log('[主应用] before mount', app.name)
return Promise.resolve()
},
],
afterMount: [
(app) => {
console.log('[主应用] after mount', app.name)
return Promise.resolve()
},
],
afterUnmount: [
(app) => {
console.log('[主应用] after unmount', app.name)
return Promise.resolve()
},
],
})
// 启动 qiankun
start({
prefetch: 'all', // 预加载所有子应用
singular: false, // 是否单例模式,false 表示多实例
}) 2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
编辑src/App.vue,添加导航和子应用容器
<template>
<div id="qiankun-demo">
<nav>
<router-link to="/">首页</router-link>
<router-link to="/child-react18">React18子应用</router-link>
<router-link to="/child-vue2">Vue2子应用</router-link>
<router-link to="/child-vue3">Vue3子应用</router-link>
</nav>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
<!-- 子应用容器 -->
<div id="subapp-container"></div>
</div>
</template>
<script setup lang="ts">
// App.vue
</script>
<style>
#qiankun-demo {
font-family: Avenir, Helvetica, Arial, sans-serif;
}
nav {
padding: 20px;
}
nav a {
margin: 0 10px;
text-decoration: none;
color: #42b983;
}
nav a.router-link-active {
font-weight: bold;
color: #2c3e50;
}
#subapp-container {
padding: 20px;
min-height: 400px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
编辑src/router/index.ts,添加子应用路由
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/Home.vue'),
},
{
path: '/child-react18',
name: 'child-react18',
component: () => import('../views/ChildReactApp.vue'),
},
{
path: '/child-vue2',
name: 'child-vue2',
component: () => import('../views/ChildVue2App.vue'),
},
{
path: '/child-vue3',
name: 'child-vue3',
component: () => import('../views/ChildVue3App.vue'),
},
],
})
export default router2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
创建占位组件src/views/ChildReactApp.vue、src/views/ChildVue2App.vue、src/views/ChildVue3App.vue
<!-- ChildReactApp.vue -->
<template>
<div class="child-app-wrapper">
<!-- react18 子应用将挂载在这里 -->
</div>
</template>
<!-- ChildVue2App.vue -->
<template>
<div class="child-app-wrapper">
<!-- vue2 子应用将挂载在这里 -->
</div>
</template>
<!-- ChildVue3App.vue -->
<template>
<div class="child-app-wrapper">
<!-- vue3 子应用将挂载在这里 -->
</div>
</template>
<style scoped>
.child-app-wrapper {
border: 1px dashed #42b983;
padding: 20px;
border-radius: 8px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
编辑vite.config.ts,确保主应用运行在合适端口
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
server: {
port: 8080,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
})2
3
4
5
6
7
8
9
10
11
12
13
子应用构建
子应用① React18
使用create-react-app创建
cd apps
pnpm create react-app child-react18 --template typescript
cd child-react18
pnpm install2
3
4
新建.env文件
BROWSER=none
HOST=localhost
PORT=3100
PUBLIC_URL='/child-react18'2
3
4
编辑src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
let root: ReactDOM.Root | null = null
// qiankun 生命周期函数
export async function bootstrap() {
console.log('[React18] 子应用 bootstrap')
}
export async function mount(props: { container?: HTMLElement }) {
console.log('[React18] 子应用 mount', props)
const container = props.container?.querySelector('#root') || document.getElementById('root')
if (container) {
root = ReactDOM.createRoot(container)
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
)
}
}
export async function unmount() {
console.log('[React18] 子应用 unmount')
root?.unmount()
root = null
}
// 独立运行时直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
const container = document.getElementById('root')
if (container) {
ReactDOM.createRoot(container).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
编辑public/index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="React18 子应用" />
<title>React18 子应用</title>
<!-- 用于独立运行时的标题 -->
<title>React 子应用</title>
</head>
</html>2
3
4
5
6
7
8
9
10
11
12
编辑src/App.tsx
import React, { useState, useEffect } from 'react'
interface Props {
name?: string
}
function App({ name }: Props) {
const [data, setData] = useState<any>(null)
useEffect(() => {
// 监听主应用下发的数据
const handleData = (e: CustomEvent) => {
console.log('[React18] 收到主应用数据:', e.detail)
setData(e.detail)
}
// @ts-ignore
window?.__QIANKUN_PROPS__?.addEventListener?.('data-change', handleData)
return () => {
// @ts-ignore
window?.__QIANKUN_PROPS__?.removeEventListener?.('data-change', handleData)
}
}, [])
const sendToMain = () => {
// @ts-ignore
window.__QIANKUN_UNMOUNT__?.({ type: 'react18-msg', data: '来自 React18 的消息' })
}
return (
<div className="App">
<header>
<h1>子应用① -- React@{React.version}</h1>
<p>收到主应用数据: {JSON.stringify(data)}</p>
<button onClick={sendToMain}>发送数据给主应用</button>
</header>
</div>
)
}
export default App2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
编辑src/index.css添加简单样式
.App {
text-align: center;
padding: 20px;
}
.App header {
background-color: #282c34;
min-height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
}
.App h1 {
font-size: 1.5em;
}
.App button {
padding: 10px 20px;
background-color: #61dafb;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
子应用② Vue2
使用vue-cli创建
cd apps
vue create child-vue2
cd child-vue2
pnpm install2
3
4
新建.env文件
VUE_APP_HOST=localhost
VUE_APP_PORT=32002
编辑vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
devServer: {
host: process.env.VUE_APP_HOST,
port: process.env.VUE_APP_PORT,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
publicPath: '/child-vue2',
})2
3
4
5
6
7
8
9
10
11
12
编辑public/index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Vue2 子应用</title>
</head>
</html>2
3
4
5
6
7
8
9
编辑src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
let instance = null
// qiankun 生命周期函数
export function bootstrap() {
console.log('[Vue2] 子应用 bootstrap')
}
export async function mount(props) {
console.log('[Vue2] 子应用 mount', props)
// 监听主应用下发的数据
if (props.onGlobalStateChange) {
props.onGlobalStateChange((state) => {
console.log('[Vue2] 收到主应用数据:', state)
instance?.$set(instance, 'mainData', state)
}, true)
}
instance = new Vue({
router,
render: (h) => h(App),
}).$mount('#app')
}
export async function unmount() {
console.log('[Vue2] 子应用 unmount')
instance?.$destroy()
instance = null
}
// 独立运行时直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
new Vue({
router,
render: (h) => h(App),
}).$mount('#app')
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
编辑src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../components/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
]
const router = new VueRouter({
mode: 'history',
base: window.__POWERED_BY_QIANKUN__ ? '/child-vue2' : '/',
routes,
})
export default router2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
编辑src/App.vue
<template>
<div id="app">
<h1>子应用② -- Vue@2.6.14</h1>
<p>收到主应用数据: {{ JSON.stringify(mainData) }}</p>
<button @click="sendToMain">发送数据给主应用</button>
<router-view />
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
mainData: null,
}
},
methods: {
sendToMain() {
// @ts-ignore
if (window.__QIANKUN_UNMOUNT__) {
// @ts-ignore
window.__QIANKUN_UNMOUNT__({
type: 'vue2-msg',
data: '来自 Vue2 的消息',
})
}
},
},
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
padding: 20px;
}
button {
padding: 10px 20px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
创建src/components/Home.vue
<template>
<div class="home">
<h2>Vue2 子应用首页</h2>
</div>
</template>
<script>
export default {
name: 'Home',
}
</script>
<style scoped>
.home {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
子应用③ Vue3
使用vite创建
cd apps
pnpm create vue child-vue3
cd child-vue3
pnpm install2
3
4
新建.env文件
VITE_APP_HOST=localhost
VITE_APP_PORT=33002
编辑vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
base: '/child-vue3',
server: {
host: 'localhost',
port: 3300,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
编辑index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3 子应用</title>
</head>
</html>2
3
4
5
6
7
8
9
编辑src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
let app: ReturnType<typeof createApp> | null = null
// qiankun 生命周期函数
export async function bootstrap() {
console.log('[Vue3] 子应用 bootstrap')
}
export async function mount(props: any) {
console.log('[Vue3] 子应用 mount', props)
// 监听主应用下发的数据
if (props.onGlobalStateChange) {
props.onGlobalStateChange(
(state: any) => {
console.log('[Vue3] 收到主应用数据:', state)
// 可以通过 provide/inject 或 store 传递数据
},
true
)
}
app = createApp(App)
app.use(router)
app.mount(props.container ? props.container.querySelector('#app') : '#app')
}
export async function unmount() {
console.log('[Vue3] 子应用 unmount')
app?.unmount()
app = null
}
// 独立运行时直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
createApp(App).use(router).mount('#app')
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
编辑src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../components/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
]
export default createRouter({
history: createWebHistory(
// @ts-ignore
window.__POWERED_BY_QIANKUN__ ? '/child-vue3' : '/'
),
routes,
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
编辑src/App.vue
<template>
<div class="app">
<h1>子应用③ -- Vue@{{ vueVersion }}</h1>
<p>收到主应用数据: {{ JSON.stringify(mainData) }}</p>
<button @click="sendToMain">发送数据给主应用</button>
<router-view />
</div>
</template>
<script setup lang="ts">
import { ref, provide, onMounted, onUnmounted } from 'vue'
import { version as vueVersion } from 'vue'
const mainData = ref<any>(null)
// @ts-ignore
const props = window.__QIANKUN_PROPS__
onMounted(() => {
// 监听主应用下发的数据
if (props?.onGlobalStateChange) {
props.onGlobalStateChange(
(state: any) => {
console.log('[Vue3] 收到主应用数据:', state)
mainData.value = state
},
true
)
}
})
const sendToMain = () => {
// @ts-ignore
if (window.__QIANKUN_UNMOUNT__) {
// @ts-ignore
window.__QIANKUN_UNMOUNT__({
type: 'vue3-msg',
data: '来自 Vue3 的消息',
})
}
}
</script>
<style scoped>
.app {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
padding: 20px;
background-color: #f0f9ff;
border-radius: 8px;
}
button {
padding: 10px 20px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
创建src/components/Home.vue
<template>
<div class="home">
<h2>Vue3 子应用首页</h2>
</div>
</template>
<script setup lang="ts">
// Home component
</script>
<style scoped>
.home {
padding: 20px;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
主应用子应用通信
qiankun 提供了initGlobalState方法用于主应用和子应用之间的通信。
主应用发送数据给子应用
编辑src/main.ts,添加全局状态管理
import { initGlobalState, registerMicroApps, start } from 'qiankun'
import type { Actions } from 'qiankun'
// 初始化全局状态
const initialState = {
user: null,
token: '',
}
// 创建全局状态实例
const actions: Actions = initGlobalState(initialState)
// 监听全局状态变化
actions.onGlobalStateChange((state, prev) => {
console.log('[主应用] 状态变化:', state, '前一个状态:', prev)
})
// 设置全局状态
export function setGlobalState(state: Partial<typeof initialState>) {
actions.setGlobalState(state)
}
// 获取当前全局状态
export function getGlobalState() {
return actions.getGlobalState()
}
// 导出 actions 供其他组件使用
export { actions }2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
在组件中使用
<template>
<div>
<button @click="sendToChild">发送数据给子应用</button>
</div>
</template>
<script setup lang="ts">
import { actions } from '../main'
const sendToChild = () => {
actions.setGlobalState({
user: { name: '张三', age: 18 },
token: 'abc123',
})
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
子应用接收主应用数据
在子应用的 mount 生命周期中接收数据
// Vue3 子应用示例
export async function mount(props) {
console.log('[Vue3] mount', props)
// 监听主应用状态变化
props.onGlobalStateChange?.((state: any, prev: any) => {
console.log('[Vue3] 状态变化:', state, 'prev:', prev)
}, true)
app = createApp(App)
app.mount(props.container?.querySelector('#app') || '#app')
}2
3
4
5
6
7
8
9
10
11
12
子应用发送数据给主应用
子应用通过props获取主应用的方法来发送数据
// 子应用 mount 时获取主应用的方法
export async function mount(props) {
// 发送数据给主应用
props.setGlobalState?.({
type: 'child-msg',
data: '来自子应用的消息',
})
}2
3
4
5
6
7
8
样式隔离
qiankun 提供了两种样式隔离方式
严格样式隔离(strictStyleIsolation)
启用后,子应用的样式会影响主应用,需要通过container选择器包裹
编辑src/main.ts
start({
prefetch: 'all',
singular: false,
sandbox: {
strictStyleIsolation: true, // 严格样式隔离
},
})2
3
4
5
6
7
实验性样式隔离(experimentalStyleIsolation)
启用后,qiankun 会自动给子应用的样式添加前缀
start({
prefetch: 'all',
singular: false,
sandbox: {
experimentalStyleIsolation: true, // 实验性样式隔离
},
})2
3
4
5
6
7
子应用添加命名空间
在子应用的 CSS 中添加统一的类名前缀
.vue2-app {
/* 所有样式都添加 .vue2-app 前缀 */
}
.vue2-app h1 {
color: #42b983;
}2
3
4
5
6
常见问题
1. 子应用静态资源 404
确保子应用的publicPath配置正确
// vue.config.js
module.exports = defineConfig({
publicPath: '/child-vue2',
})2
3
4
// vite.config.ts
export default defineConfig({
base: '/child-vue3',
})2
3
4
2. 子应用路由跳转丢失
确保子应用的base与activeRule一致
// 主应用
{
name: 'child-vue3',
entry: '//localhost:3300',
container: '#subapp-container',
activeRule: '/child-vue3',
}
// 子应用 router
createRouter({
history: createWebHistory('/child-vue3'), // 必须与 activeRule 一致
routes,
})2
3
4
5
6
7
8
9
10
11
12
13
3. 子应用获取不到主应用数据
确保在 mount 生命周期后再调用onGlobalStateChange
export async function mount(props) {
// 正确:在 mount 中监听
props.onGlobalStateChange?.((state) => {
console.log('收到数据:', state)
}, true)
}2
3
4
5
6
4. 子应用全局变量冲突
使用 qiankun 的沙箱机制可以隔离全局变量,如果仍有冲突,可以:
- 使用
imports明确声明需要隔离的模块 - 在子应用中使用
IIFE包裹代码
部署
Nginx 配置
server {
listen 80;
server_name localhost;
# 主应用
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# 子应用 react18
location /child-react18 {
alias /usr/share/nginx/html/child-react18;
index index.html;
try_files $uri $uri/ /child-react18/index.html;
}
# 子应用 vue2
location /child-vue2 {
alias /usr/share/nginx/html/child-vue2;
index index.html;
try_files $uri $uri/ /child-vue2/index.html;
}
# 子应用 vue3
location /child-vue3 {
alias /usr/share/nginx/html/child-vue3;
index index.html;
try_files $uri $uri/ /child-vue3/index.html;
}
# CORS 头
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
add_header Access-Control-Allow-Origin *;
expires 7d;
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Docker 部署
在项目根目录新建Dockerfile
FROM node:20-slim AS builder
WORKDIR /app
COPY . /app
RUN npm install -g pnpm
RUN pnpm install --frozen-lockfile
# 构建主应用
RUN pnpm --filter main-app build
# 构建子应用
RUN pnpm --filter child-react18 build
RUN pnpm --filter child-vue2 build
RUN pnpm --filter child-vue3 build
FROM nginx:alpine
COPY --from=builder /app/apps/main-app/dist /usr/share/nginx/html
COPY --from=builder /app/apps/child-react18/build /usr/share/nginx/html/child-react18
COPY --from=builder /app/apps/child-vue2/dist /usr/share/nginx/html/child-vue2
COPY --from=builder /app/apps/child-vue3/dist /usr/share/nginx/html/child-vue3
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
详见官方文档:https://qiankun.umijs.org/