Skip to content

上手 qiankun:主应用/子应用改造与四种通讯方案

微前端第三课 · qiankun 实战

课程地图

模块内容学习产出
主应用改造安装 qiankun,registerMicroApps 注册子应用(name / activeRule / container / entry)主应用三行代码启动微前端
子应用改造暴露 bootstrap / mount / unmount 三个钩子 + UMD 打包(Vue / React 两套配置)子应用可以被主应用接管
容器踩坑主应用容器 vs 子应用自身目标容器,两种加载方案的容器最佳实践解决"页面出不来 / 样式丢失"
路由前缀用 props 把 activeRule 动态下发给子应用,genRouter(baseURL) 动态建路由主应用改前缀,子应用不用动
沙箱调整experimentalStyleIsolation 样式隔离的原理与局限知道样式为什么被"加前缀"
通讯四方案initGlobalState / Pinia / EventBus / 直接挂 window父子应用双向传值

INFO

本课一句话目标:把前两课手写的"简化版 qiankun"换成真 qiankun,走一遍真实的接入流程——主应用怎么配、子应用怎么改、坑在哪、数据怎么传。

开篇:原理吃透了,该用真框架了

前两节课我们洞悉了微前端的原理:动态加载、路由切换、沙箱、生命周期,甚至手搓了一个"简化版 qiankun"。但实战中我们几乎没机会自己开发一套微前端——一般直接用成熟框架。所以这节课就讲 qiankun(乾坤)这个框架的使用,以及本节实战中总结的一些技巧。

INFO

学到这里你的优势:因为前两课亲手实现过原理,这节课看到的每个 API 你都知道它背后在做什么。 比如 experimentalStyleIsolation,就是第二课我们自己做的"给样式加独一无二父元素前缀"。

微前端天然分为主应用子应用。改造流程分两步:先调整主应用(安装、注册子应用、启动),再调整子应用(暴露生命周期钩子、UMD 打包)。

主应用改造:注册子应用的四个必填配置

2.1 安装 qiankun

本节演示环境里已经装好了,你自己只需要:

js
npm install qiankun   // 主应用安装

然后从 qiankun 里引入注册子应用的方法 registerMicroApps

2.2 注册子应用:数组里的每个对象就是一套规则

调用 registerMicroApps,传入一个数组。数组里每个元素是一个子应用对象,包含它的具体规则。要注册一个子应用,四个配置是必须的

配置作用
name子应用的名字,必须有
activeRule激活规则——地址切换到什么时候显示这个子应用(如 /app1)
container挂载目标——子应用挂到主应用的哪个容器(如主应用里 id 为 sun 的 div)
entry子应用的资源地址,支持两种写法(见下)

2.3 entry 的两种写法

写法一:直接给 JS 和 CSS 地址。把子应用打包后的产物丢到服务器上(就像平常项目打包交给后端上线),把 JS/CSS 地址填进来。地址可以去掉前面的 http://,qiankun 会自动拼接;有多个 JS 就都写进数组:

js
// 主应用 main.js —— 方式一:加载子应用的 JS + CSS
import { registerMicroApps, start } from 'qiankun'

registerMicroApps([
  {
    name: 'app1',                       // ① 子应用名
    activeRule: '/app1',                // ② 激活规则:地址切到 /app1 时显示
    container: '#sun',                  // ③ 挂载目标:主应用里 id 为 sun 的 div
    entry: {                            // ④ 资源地址:JS + CSS 方案
      scripts: ['//localhost:8080/app1/js/app.xxx.js'],   // 可去掉 http://
      styles: ['//localhost:8080/app1/css/app.xxx.css']
    }
  },
  {
    name: 'app2',
    activeRule: '/app2',
    container: '#sun',
    entry: {                            // 多个 JS 就都写在数组里
      scripts: ['//localhost:3001/static/js/bundle.js'],
      styles: ['//localhost:3001/static/css/main.css']
    }
  }
])

写法二:直接给在线地址。如果子应用除了当子应用,也能独立上线、独立访问,那就直接把它的线上页面地址填进来。比如第二个子应用是 React 项目,本地 npm run dev 跑起来后,直接把开发地址填上:

js
// 方式二:在线地址方案(子应用可独立访问)
{
  name: 'app2',
  activeRule: '/app2',
  container: '#sun',
  entry: '//localhost:3001'   // React 子应用的本地开发地址(在线地址)
}

2.4 start 的时机:必须在主应用挂载完成之后

注册子应用只是"登记",还没启动微前端。最后一步是调用 start() 开启。但这里有个很多新手会踩的坑——如果注册完立刻 start:

js
registerMicroApps([...])
start()   // ❌ 有风险:此时主应用可能还没挂载完成,容器 #sun 还没渲染出来

万一主应用挂载要很久,start 时容器还没渲染,子应用就挂不上去。所以实战中保险的做法是在主应用根组件的 onMounted 之后调用 start,确保 id 为 sun 的 div 已经存在:

js
// 主应用 App.vue —— 推荐写法
import { onMounted } from 'vue'
import { start } from 'qiankun'

export default {
  setup() {
    onMounted(() => {
      start()   // ✅ 主应用渲染完成、容器就绪后再启动
    })
  }
}

WARNING

实战注意:不要在主应用模块顶层直接 start()。 你没法确保主应用挂载完成,容器还没渲染出来就启动,子应用一定挂不上去。

子应用改造:生命周期钩子 + UMD 打包

主应用改完了,接下来改子应用。为什么要写生命周期钩子?其实第二课就做过类似操作——为了方便主应用控制子应用的挂载和卸载:子应用给主应用一个 mount 方法,主应用调用它来挂载;再给一个 unmount 方法,主应用调用它来卸载。

3.1 qiankun 要求的三个生命周期钩子

qiankun 要求子应用必须通过 export 暴露三个生命周期钩子,缺一不可:

钩子触发时机
bootstrap只会执行一次(首次进入时),后面再次切换到该应用不再触发
mount每次切进来都会触发
unmount卸载(切走)时触发

Vue 3 子应用示例:

js
// 子应用 src/main.js —— Vue 3 版生命周期钩子
import { createApp } from 'vue'
import App from './App.vue'

let app = null

// bootstrap:只执行一次(首次进入)
export async function bootstrap() {
  console.log('app1 bootstrap')
}

// mount:每次切进来都执行,把 Vue 实例挂到容器
export async function mount(props) {
  app = createApp(App)
  app.mount(props.container)   // 挂载到主应用指定的容器
}

// unmount:切走时执行,销毁实例
export async function unmount() {
  app && app.unmount()
  app = null
}

React 子应用同理:它的启动页面是 index.js,把 bootstrap / mount / unmount 三个方法分别暴露出去即可。

3.2 UMD 打包:qiankun 的硬性要求

配合生命周期钩子方案,必须进行 UMD 打包(qiankun 硬性要求)。UMD 打包和 Vue 还是 React 无关,只取决于你的项目是 Webpack 打包还是 Vite 打包,按对应工具修改打包配置。

  • Vue 2 + Webpack:像改普通 webpack 配置一样,设置 output 的 library 和 libraryTarget 为 umd;
  • Vue 3 + Vite:用 Vite 的 lib 模式(第二课已演示过);
  • React(create-react-app 创建):比较特殊,见下。

React 项目怎么改 UMD 打包(create-react-app 创建的项目)

npx create-react-app 创建的项目没有地方给你改打包配置(就像 Vue 2 + Webpack 一样被隐藏了),需要额外装一个库:@craco/craco

第一步:安装并把启动指令全部替换成 craco:

js
npm install @craco/craco

// package.json —— scripts 全部换成 craco
"scripts": {
  "start": "craco start",
  "build": "craco build",
  "test": "craco test"
}

第二步:写一份 craco.config.js,说明怎么修改 webpack 的打包配置。改成 UMD 核心就两点——打包名字(library)打包目标(libraryTarget: 'umd')

js
// 子应用 craco.config.js —— React 项目改成 UMD 打包
module.exports = {
  webpack: {
    configure(config) {
      config.output.library = 'app2'            // 打包名字:挂到 window 上的名字
      config.output.libraryTarget = 'umd'       // 打包目标:UMD
      // 下面两项可加可不加(可去掉)
      config.output.globalObject = 'window'
      config.output.chunkFilename = '[name].chunk.js'
      return config
    }
  }
}

3.3 一个特殊场景:Vue 3 + Vite 的子应用很难用"开发地址"

Vue 3 项目的开发模式很难改成加载 UMD:就算在 vite.config.js 里把打包模式改成 UMD,npm run dev 跑开发模式时依然不走 UMD(只有 build 打包时才走)。本节也没找到让开发模式走 UMD 的方案。

所以对于 Vue 3 + Vite 子应用,做法是:在 node 服务器上准备一个 HTML,这个 HTML 加载打包后的 JS 和 CSS。在服务器代码里定义一个接口(如 /view-app),访问它就返回这个 HTML——浏览器加载这个 HTML,HTML 再去加载 JS/CSS。这不就是一个"在线地址"了吗?

js
// node 服务器 server.js —— 为 Vue3+Vite 子应用提供"线上地址"
const http = require('http')
const fs = require('fs')

// 打包后的子应用 HTML:内部用 <script> 和 <link> 加载打包后的 JS / CSS
const app1Html = fs.readFileSync('./app1/index.html', 'utf-8')

http.createServer((req, res) => {
  if (req.url.startsWith('/view-app')) {        // 访问这个接口返回 HTML
    res.writeHead(200, { 'Content-Type': 'text/html' })
    res.end(app1Html)
    return
  }
  // ... 其他静态资源(JS / CSS)的路由
}).listen(8080)

事实上,正常项目上线就是这样工作的:打包后的 HTML 交给服务器,浏览器请求时返回 HTML,HTML 再加载 JS/CSS。所以这个方案就是"模拟线上"。

INFO

小结:子应用改造就两件事——① 暴露三个生命周期钩子;② 按项目工具改成 UMD 打包(React 走 @craco/craco,Vue 走对应配置)。

踩坑专题:容器到底怎么填

本节演示到一半,发现页面不出来、报 301、样式丢失——问题就出在容器的填法上。先明确两个容易混淆的概念:

概念是什么
主应用容器注册子应用时填的 container,指向主应用里的某个 div(如 #sun)
子应用自身目标容器子应用挂载时说明的容器(如子应用挂到 id 为 sun 的 div 上)。这个 div 得有人提前准备好

关键是:**这个"子应用自身目标容器"(#sun)到底写在哪?**答案取决于你用的是哪种加载方案。

4.1 方案一(JS + CSS):把子应用目标容器写进"HTML 项"

用 JS+CSS 方案加载子应用时,qiankun 支持 HTML 配置项。最佳实践:先准备一个主应用容器,再在主应用容器里用 HTML 说明子应用自身目标容器

js
// 主应用 index.html —— JS+CSS 方案的最佳实践
// 容器 #sun 里再写一层子应用的挂载目标 #app1
<div id="sun">
  <div id="app1"></div>   <!-- 子应用自身目标容器(HTML 项) -->
</div>
js
// 注册时:container 用主应用容器,子应用目标写在 HTML 里
{
  name: 'app1',
  activeRule: '/app1',
  container: '#sun',
  entry: {
    scripts: ['//localhost:8080/app1/js/app.xxx.js'],
    styles: ['//localhost:8080/app1/css/app.xxx.css'],
    html: '<div id="app1"></div>'   // 子应用挂载目标,放在 HTML 项里
  }
}

4.2 方案二(在线地址):把子应用目标容器直接写进主应用容器里

在线地址方案是直接写一个字符串,没有 HTML 这一项给你写。所以必须把子应用的挂载目标(#sun div)提前写进主应用容器里

js
// 主应用 index.html —— 在线地址方案的最佳实践
// 两个子应用都挂到 #sun,就把 #sun 直接写在主应用容器里
<div id="sun"></div>   <!-- 子应用挂载目标直接写在这里 -->
js
{
  name: 'app2',
  activeRule: '/app2',
  container: '#sun',        // 容器里已经写好子应用的挂载目标 div
  entry: '//localhost:3001'
}

4.3 最常见的坑:两个容器用同一个 ID

很多同学粗略看文档就写——container 用 id 为 sun 的 div,子应用挂载目标也写 id 为 sun。这样一定会出问题:

  • Vue 项目:挂不出来;
  • React 项目:样式丢失。

WARNING

铁律:主应用容器和子应用目标容器一定不能是同一个 ID。 用在线地址方案,就在容器里直接写好子应用目标;用 JS+CSS+HTML 方案,就把子应用目标写在 HTML 项里。 千万不要"container 和挂载目标都用 #sun"。

路由前缀:activeRule 动态下发

开发微前端项目时很多人有个困惑:子应用开发时,不知道主应用给我的 activeRule 是多少,怎么决定自己的路由前缀?

比如主应用给 APP1 定的 activeRule 是 /app1,那么只有地址切到 /app1 时才会显示 APP1,子应用自己跑的时候也得带 /app1 前缀。问题来了:主应用哪天"灵机一动"把 /app1 改成 /appX,子应用这边还是 /app1,就匹配不上、加载不出来了。

5.1 核心思路:主应用通过 props 把 activeRule 传给子应用

qiankun 注册子应用时有一个很核心的配置 props——就像父组件给子组件传消息一样,主应用可以往里面放任何信息,包括 activeRule:

js
// 主应用:注册时通过 props 把 activeRule 传给子应用
{
  name: 'app1',
  activeRule: '/app1',
  container: '#sun',
  entry: { scripts: [...], styles: [...] },
  props: {
    activeRule: '/app1'   // 告诉子应用:主应用给你定的激活前缀是这个
  }
}

5.2 子应用在 mount 阶段接收 props

子应用的 mount 阶段会接收到这些 props。取出来当 baseURL 用。但这里还有个问题:router 不允许动态修改 baseURL——路由已经创建好了,想动态改不行。所以要把路由创建封装成方法:

js
// 子应用 src/router/index.js —— 路由创建封装成 genRouter(baseURL)
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'

// 不马上创建路由,而是封装成方法,接收 baseURL
export default function genRouter(baseURL) {
  return createRouter({
    history: createWebHistory(baseURL),   // 路由前缀由主应用下发的 baseURL 决定
    routes: [
      { path: '/', component: Home }
    ]
  })
}

5.3 main.js:挂载时取出 props 的 activeRule,创建路由

js
// 子应用 src/main.js —— 动态路由 + 生命周期钩子
import { createApp } from 'vue'
import App from './App.vue'
import genRouter from './router'

let app = null
let router = null

export async function bootstrap() {}

export async function mount(props) {
  const activeRule = props.activeRule || '/'    // 主应用通过 props 下发的激活前缀
  router = genRouter(activeRule)                // 创建路由时指定 baseURL
  app = createApp(App)
  app.use(router)
  app.mount(props.container)
}

export async function unmount() {
  app && app.unmount()
  app = null
  router = null
}

INFO

效果:以后主应用想改激活地址,只需要改主应用传给子应用的 props,子应用代码一行都不用动。 这就是"主应用和子应用路由前缀解耦"的标准做法。

沙箱机制:样式隔离怎么开

接入 qiankun 后,如果主应用和子应用都启用了样式,子应用可能把主应用的样式污染掉(演示里主应用的 header 被影响)。qiankun 的沙箱配置里有一项专门处理这个问题:

js
// 主应用 main.js —— 开启第一重样式隔离
start({
  sandbox: {
    experimentalStyleIsolation: true   // 样式隔离:给子应用样式加"前缀"
  }
})

6.1 原理:给样式加"独一无二的父元素选择器"

开启后刷新页面,你会发现主应用的 header 不再被子应用影响了。看浏览器控制台里子应用的样式,自动被加了后缀/前缀——比如 .app-header 变成了 div[data-qiankun="app2"] .app-header

js
/* 隔离前(子应用原始样式) */
.app-header { background: #000 }

/* 隔离后(qiankun 自动处理) */
div[data-qiankun="app2"] .app-header { background: #000 }

这不就是第二课我们自己做的"给样式加一个独一无二的父元素选择器"吗?qiankun 在子应用挂载时给容器加了 data-qiankun="app2" 属性,再把子应用的所有样式选择器加上这个父元素前缀,实现隔离。

6.2 局限:body、router 这类选择器隔离不到

WARNING

这种隔离没办法解决子应用对 body、router 的修改。 比如子应用的 index.css 里写了 body { ... },这种修改不会被隔离——因为 body 不在子应用的挂载范围之内。

如果你要最严格的隔离,连 body、router 的修改都能隔开,理论上要用 shadow DOM(阴影根):qiankun 里对应的配置是 strictIsolation,开启后子应用的 style 标签和整个 HTML 都会放进一个 shadow root 里。但本节提醒:新版本 qiankun 里这个配置有 bug,且即将被移除,不推荐加

INFO

大多数情况配 experimentalStyleIsolation: true 就够用了——因为子应用一般不会去做 body / router 这种全局样式的修改。

父子应用通讯:四种方案

这是 qiankun 里最容易搞蒙、也最复杂的地方。场景是:主应用想给子应用发消息,消息改变时子应用能收到;子应用想修改主应用的数据/状态。本节给出了四种方案,从推荐到不推荐:

7.1 方案一:qiankun 自带全局状态系统(initGlobalState)

**适合简单的数据共享。**主应用通过 initGlobalState 创建全局状态,返回一个 actions,里面有三个方法:

方法作用
onGlobalStateChange(cb, fireImmediately)监听状态改变(任何地方改了都会触发)
setGlobalState(state)设置/修改状态
offGlobalStateChange()移除监听(关闭全局状态)

主应用创建一个全局状态文件:

js
// 主应用 src/global-state.js —— 创建全局状态
import { initGlobalState } from 'qiankun'

const actions = initGlobalState({
  sonName: 'app1',     // 当前子应用的名字
  menuStates: 1        // 主应用的一个状态
})

export default actions

主应用怎么"用"这个状态(别扭点在这里)

没有"获取"方法——这是它设计得别扭的地方。想展示状态,必须配合 Vue 的 ref / reactive:定义响应式数据渲染到页面,再通过 onGlobalStateChange 监听变化,手动把新值赋给响应式数据:

js
// 主应用 App.vue —— 配合 ref/reactive 使用全局状态
import { ref, onMounted } from 'vue'
import actions from './global-state'

const sonName = ref('')
const menuStates = ref(-1)

actions.onGlobalStateChange((newValue, oldValue) => {
  // new 是保留字,参数别叫 new!这里叫 newValue
  console.log('收到全局状态变化', newValue, oldValue)
  sonName.value = newValue.sonName        // 手动赋给响应式数据
  menuStates.value = newValue.menuStates  // 再由响应式数据引发页面更新
})

WARNING

两个易错点: ① 监听回调的参数名不能用 new(保留字,本节排查了半天报错),改叫 newValue; ② 修改状态时 setGlobalState 的 key 必须和初始化时一致——传错 key(比如 menuStates 写成别的)会报 states has not change。

子应用怎么改主应用的状态

把 actions 和 setGlobalState 通过 props 传给子应用:

js
// 主应用:注册时把 actions / setGlobalState 通过 props 传给子应用
import actions from './global-state'

registerMicroApps([
  {
    name: 'app1',
    activeRule: '/app1',
    container: '#sun',
    entry: { scripts: [...], styles: [...] },
    props: {
      activeRule: '/app1',
      actions: actions,                    // 全局状态的 actions
      setGlobalState: actions.setGlobalState   // 或直接给设置方法
    }
  }
])

子应用在 mount 阶段解构接收;但Vue 创建时不允许直接往组件里传 props(和 React 不同),所以最简单的方式是挂到 window 上

js
// 子应用 src/main.js —— mount 阶段接收并挂到 window
export async function mount(props) {
  const { setGlobalState } = props

  // Vue 创建时不能直接传 props 给组件,挂到 window 上让组件随时取
  window.__setGlobalState = setGlobalState

  // 子应用一挂载,马上把主应用的状态改掉(举例)
  setGlobalState({ menuStates: 2 })

  // ... 创建应用挂载
}

// 子应用任意组件里(如 Home.vue 的按钮点击)
function handleClick() {
  window.__setGlobalState({ menuStates: 3 })   // 子应用修改主应用状态
}

INFO

方案一特点:适合简单数据共享。它本身不具备响应式——必须触发监听,再在监听里手动去修改响应式数据,比较绕。 但优点是:主应用和子应用可以跨框架(Vue、React 都能用)。

7.2 方案二:Pinia / Vuex / Redux 全局状态管理

**具有响应式、用起来最爽,但有框架限制。**原理:主应用定义好全局 store,把整个 store 通过 props 传给子应用,子应用直接调用 store 的方法改数据,主应用马上更新,不需要监听、不需要手动赋值

主应用定义 Pinia store:

js
// 主应用 src/stores/global-store.js —— 定义 Pinia 仓库
import { defineStore } from 'pinia'

export const useGlobalStore = defineStore('global', {
  state: () => ({
    sonName: 'app1',
    menuStates: 1
  }),
  actions: {
    setSonName(name) { this.sonName = name },
    setMenuStates(states) { this.menuStates = states }
  }
})
js
// 主应用 main.js —— 注册 Pinia,并把 store 传下去
import { createPinia } from 'pinia'
import { useGlobalStore } from './stores/global-store'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

// 注意:必须在 Pinia 注册之后再调用 useGlobalStore
const globalStore = useGlobalStore()

registerMicroApps([
  {
    name: 'app1',
    activeRule: '/app1',
    container: '#sun',
    entry: { scripts: [...], styles: [...] },
    props: {
      activeRule: '/app1',
      globalStore: globalStore    // 把整个 store 传给子应用
    }
  }
])

子应用从 props 里取出 store 直接调用方法:

js
// 子应用 src/main.js —— 从 props 接收 store,挂 window 供组件使用
export async function mount(props) {
  const { globalStore } = props

  // 一挂载:子应用直接调用主应用 store 的方法改状态
  globalStore.setSonName('app1')
  window.__globalStore = globalStore   // 组件里随时取用
}

// 子应用组件里
function handleClick() {
  window.__globalStore.setMenuStates(3)   // 响应式,主应用页面立即更新
}

WARNING

Pinia 两个易错点: ① 必须在 Pinia 注册(app.use(createPinia()))之后再调用 useGlobalStore,否则报错; ② 不能解构 store——const { sonName } = globalStore 会丢掉响应式(本节演示时习惯 React 的解构写法,结果页面不更新),必须 globalStore.sonName 这样用。

INFO

方案二特点:响应式,主应用一改子应用马上更新,无需监听。 但要求主应用和子应用必须是同一个框架、同一个版本(比如全是 Vue 3,或主 Vue 3 子 React 就跑不了)。 如果你能保证全家桶统一,就用这套。

7.3 方案三:自定义 EventBus 体系

不太推荐。它和方案一本质很像(都没有响应式),完全可以被方案一替代。本质就是自己实现一套观察者模式(监听模式):

js
// 主应用 src/event-bus.js —— 自定义 EventBus(观察者模式)
const eventList = {}

// 注册监听:事件名 + 回调
export function on(eventName, callback) {
  if (!eventList[eventName]) eventList[eventName] = []
  eventList[eventName].push(callback)
}

// 触发监听:从列表里取出对应事件的所有回调执行
export function emit(eventName, ...args) {
  const callbacks = eventList[eventName] || []
  callbacks.forEach((cb) => cb(...args))
}

// 主应用:挂到 window 或通过 props 传给子应用
window.__eventBus = { on, emit }
js
// 子应用使用
// 监听:props.eventBus.on('xxx', callback)
// 触发:props.eventBus.emit('xxx', data)

懂一点设计模式的同学一眼就能看懂。它也无法触发页面更新,和自带方案类似,一般用自带的就好了。

7.4 方案四:直接挂 window(极端情况)

**更不推荐,但极端情况下可以用。**只是临时传一两个数据给子应用,可以主应用把数据挂到 window 上,子应用直接拿;或者反过来。

js
// 主应用:直接挂 window
window.__microAppData = { theme: 'dark' }

// 子应用:直接拿
const theme = window.__microAppData.theme

WARNING

注意:用这种方案必须去掉沙箱(sandbox: false),否则 window 不共享、拿不到。 临时传一两个数据可以用,但不要依赖它做正经业务。

7.5 四种方案怎么选

方案响应式跨框架推荐度适用场景
initGlobalState否(需监听后手动赋值)✅ 可以⭐⭐⭐ 推荐简单数据共享、跨框架项目
Pinia / Vuex / Redux✅ 是❌ 需同框架同版本⭐⭐ 看条件全家桶统一、数据量大的项目
自定义 EventBus✅ 可以⭐ 一般不推荐可被方案一替代
直接挂 window✅ 可以(需去沙箱)⚠极端情况临时传一两个数据

总结:qiankun 能力全景

三节课下来,从原理到手写框架再到真实框架,qiankun 的核心能力和我们手写的简化版一一对应上了:

能力手写版(前两课)qiankun(本课)
子应用注册 / 启动registerApps + startregisterMicroApps + start(onMounted 后调用)
子应用入口js / css 地址entry 支持 JS+CSS、在线地址、HTML 三种形态
生命周期mount / unmountbootstrap(一次)/ mount(每次)/ unmount(卸载)
路由激活pathname 第一段匹配activeRule 规则 + props 动态下发前缀
样式隔离CSS 类名加 #sun 前缀experimentalStyleIsolation(data-qiankun 前缀)/ shadow DOM
JS 沙箱Proxy 代理 windowProxy 沙箱(多实例、快照等),可配置关闭
通讯(未实现)initGlobalState / 状态管理库 / EventBus / window

INFO

学完三节课的完整认知:原理层——微前端是"动态加载 JS + 沙箱 + 生命周期";框架层——qiankun 把这些封装成 registerMicroApps / start / 生命周期钩子 / props 通讯的标准 API。 以后看任何微前端框架(micro-app、无界等)都能秒懂。

术语表 & 附录完整代码

术语表

术语定义
qiankun(乾坤)基于 single-spa 的成熟微前端框架,提供子应用注册、沙箱、样式隔离、通讯等开箱即用能力
activeRule激活规则:地址匹配到该规则时加载对应的子应用
entry子应用的资源入口,支持 JS+CSS 地址、在线地址、HTML 三种形态
生命周期钩子bootstrap(一次)/ mount(每次进入)/ unmount(卸载),由主应用在合适时机调用
UMD 打包库格式打包,把 export 的内容挂到 window[名字] 上,qiankun 硬性要求
主应用容器 / 子应用目标容器注册时的 container(主应用容器)与子应用自身挂载的容器,二者不能是同一个 ID
experimentalStyleIsolationqiankun 的样式隔离:给子应用样式选择器加 data-qiankun 父元素前缀
initGlobalStateqiankun 自带的全局状态系统:onGlobalStateChange / setGlobalState / offGlobalStateChange
props注册子应用时传给子应用的配置,mount 阶段接收,用于传 activeRule、通讯方法等
EventBus基于观察者模式的事件监听体系(on / emit),可做应用间通讯,但可被自带方案替代

附录 A:主应用完整代码(main.js)

js
// ========== 主应用 main.js(合并本课主应用全部内容) ==========
import { createApp } from 'vue'
import { onMounted } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { registerMicroApps, start, initGlobalState } from 'qiankun'
import { useGlobalStore } from './stores/global-store'

// ---- 主应用本身先启动,Pinia 注册后才能取 store ----
const app = createApp(App)
app.use(createPinia())            // 先注册 Pinia
app.mount('#app')

const globalStore = useGlobalStore()   // ✅ 必须在 Pinia 注册之后再调用
const actions = initGlobalState({ sonName: 'app1', menuStates: 1 })

// ---- 注册子应用 ----
registerMicroApps([
  {
    name: 'app1',
    activeRule: '/app1',
    container: '#sun',
    entry: {
      scripts: ['//localhost:8080/app1/js/app.xxx.js'],
      styles: ['//localhost:8080/app1/css/app.xxx.css'],
      html: '<div id="app1"></div>'   // 子应用目标容器写进 HTML 项
    },
    props: {
      activeRule: '/app1',          // 路由前缀动态下发
      actions,                      // 方案一:全局状态
      setGlobalState: actions.setGlobalState,
      globalStore                   // 方案二:Pinia store
    }
  },
  {
    name: 'app2',
    activeRule: '/app2',
    container: '#sun',
    entry: '//localhost:3001',      // 在线地址方案(容器里直接写好 #sun)
    props: { activeRule: '/app2' }
  }
])

// ---- 启动(必须等主应用挂载完成) ----
onMounted(() => {
  start({
    sandbox: {
      experimentalStyleIsolation: true   // 样式隔离
    }
  })
})

附录 B:子应用完整代码(main.js,Vue 3 版)

js
// ========== 子应用 src/main.js(合并本课子应用全部内容) ==========
import { createApp } from 'vue'
import App from './App.vue'
import genRouter from './router'

let app = null
let router = null

export async function bootstrap() {
  // 只会执行一次
}

export async function mount(props) {
  const { activeRule, actions, setGlobalState, globalStore } = props

  // 路由前缀:主应用通过 props 下发
  router = genRouter(activeRule || '/')

  // 通讯方法挂到 window,供组件随时取用(Vue 创建时不能直接传 props 给组件)
  window.__setGlobalState = setGlobalState
  window.__globalStore = globalStore
  window.__actions = actions

  // 子应用一挂载,主动修改主应用状态(演示)
  globalStore && globalStore.setSonName('app1')
  setGlobalState && setGlobalState({ menuStates: 2 })

  app = createApp(App)
  app.use(router)
  app.mount(props.container)
}

export async function unmount() {
  app && app.unmount()
  app = null
  router = null
}

附录 C:React 子应用 UMD 打包(craco.config.js)

js
// ========== React 子应用 craco.config.js(@craco/craco) ==========
// 1. npm install @craco/craco
// 2. scripts 全部换成 craco start / craco build / craco test
module.exports = {
  webpack: {
    configure(config) {
      config.output.library = 'app2'       // 打包名字(挂到 window 上)
      config.output.libraryTarget = 'umd'  // 打包目标:UMD
      config.output.globalObject = 'window'
      return config
    }
  }
}

本博客内容基于 MIT 许可证发布,欢迎合理转载。