第8章 8.3 微前端:qiankun + Vue3

上一章我们用 Electron 打包了一个桌面端 Vue3 应用,体验了一把「桌面端即原生」的快感。但你有没有想过一个问题:一个大型后台系统,动辄几十个模块,如果全塞在一个 Vue 项目里,会怎样? 代码冲突、团队协作困难、每次改代码都要全量发布……今天我们来聊一个解决这个问题的大杀器——微前端

🎯 开场:为什么需要微前端?

想象一下这个场景:

你入职了一家电商公司,公司有一个「中台系统」,包含:
- 用户管理模块(5个人维护)
- 订单管理模块(8个人维护)
- 数据报表模块(3个人维护)
- 库存管理模块(6个人维护)

传统方案:所有人都在同一个 Vue 项目里开发。

问题来了
1. 张三改了一个公共组件,李四不知道,结果王五的页面崩了
2. 订单模块只需要更新,但库存模块也要跟着重新打包发布
3. 某个模块想用 Vue3,另一个模块还在用 Vue2,技术升级像搬家
4. 代码仓库巨大,git clone 要半小时

微前端的出现\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\n\n,就是来解决这个痛点的。它的核心思想是:把一个巨大的应用拆成多个独立的小应用,每个小应用可以独立开发、独立部署、独立运行。

就像一栋大楼:

  • 传统方案:一整栋楼是一个大工程,谁改水管都要整栋楼停水
  • 微前端方案:每户都是独立的小公寓,独立装修、独立改造,不影响其他户

这一章,我们就来用 qiankun 这个微前端框架,把 Vue3 应用拆成「主应用 + 子应用」的可执行方案。

学完本章,你能够:
- 理解微前端的核心概念(为什么拆、怎么拆)
- 搭建一个 qiankun 主应用 + Vue3 子应用的最小闭环
- 实现主应用和子应用之间的状态共享
- 避坑:子应用样式冲突、生命周期管理、预加载优化


🧱 基础:微前端核心概念

什么是微前端?

说白了:微前端就是「把前端项目拆成多个小项目,拆开后还能像原来一样互相调用」的技术。

生活类比:就像大型连锁餐厅(微前端)vs 小作坊单店(单体应用)。

  • 小作坊单店:厨师既要炒菜、又要收银、又要洗碗,老板恨不得员工都是超人
  • 大型连锁餐厅:中央厨房负责备菜,各门店负责销售,后勤部门负责采购——各司其职,效率最大化

qiankun 是什么?

qiankun(乾坤)是蚂蚁金服开源的微前端解决方案,专门解决「多个 Vue/React/Vue 项目如何合并成一个页面」的问题。

为什么选 qiankun?
- ✅ 阿里系开源,社区活跃,文档中文友好
- ✅ 对 Vue3 支持完善
- ✅ 自动处理 CSS 样式隔离(不会 A 应用的按钮样式把 B 应用的按钮盖掉)
- ✅ 只需修改子应用,主应用接入方几乎不改代码

核心概念:主应用 + 子应用

┌─────────────────────────────────────────────┐
│                 主应用 (Main)                │
│  ┌─────────────────────────────────────────┐│
│  │  导航栏 │  侧边栏  │                    ││
│  └─────────────────────────────────────────┘│
│  ┌─────────────────────────────────────────┐│
│  │                                         ││
│  │        <div id="子应用容器"></div>      ││
│  │                                         ││
│  └─────────────────────────────────────────┘│
└─────────────────────────────────────────────┘

子应用可以独立运行:
┌────────────────┐
│   子应用 Vue3   │
│  独立开发部署   │
└────────────────┘

主应用(Main App):负责整体布局、导航、用户登录状态等公共部分,相当于「房东」。

子应用(Sub App):负责具体业务模块,如订单管理、用户管理,相当于「租户」。

每个子应用都可以:
- 独立开发(npm run dev 自己跑)
- 独立部署(上线不需要通知主应用)
- 独立技术栈(可以用 Vue3,也可以用 React)

第一个 qiankun 项目:最小闭环

下面我们来实现一个最小可运行的 qiankun + Vue3 案例。

环境准备

# 创建主应用
npm create vite@latest main-app -- --template vue
cd main-app
npm install qiankun

主应用 main.js 完整代码

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { qiankunPlugin } from '@originjs/vite-plugin-qiankun'

export default defineConfig({
plugins: [
vue(),
qiankunPlugin({
  name: 'main-app'  // 主应用名称
})
],
server: {
port: 5173,
headers: {
  'Access-Control-Allow-Origin': '*'  // 允许跨域,子应用才能嵌入
}
}
})

主应用 src/main.js 完整代码

import { createApp } from 'vue'
import App from './App.vue'
import { registerMicroApps, start } from 'qiankun'
import './style.css'

// 创建 Vue 实例
const app = createApp(App)
app.mount('#app')

// 注册子应用列表
registerMicroApps([
{
name: 'vue3-sub',           // 子应用名称(唯一标识)
entry: '//localhost:5174',  // 子应用入口地址
container: '#subapp-container',  // 子应用挂载的 DOM 节点
activeRule: '/vue3-sub'     // 激活路径(浏览器访问 /vue3-sub 时加载这个子应用)
}
])

// 启动 qiankun
start()

主应用 src/App.vue 完整代码

<template>
<div class="main-app">
<nav class="navbar">
  <h1>🧱 主应用 - 中台系统</h1>
  <div class="nav-links">
    <!-- 点击链接切换子应用 -->
    <router-link to="/vue3-sub">📦 Vue3 子应用</router-link>
    <router-link to="/">返回主页</router-link>
  </div>
</nav>

<div class="content">
  <h2>当前页面内容:</h2>
  <!-- 子应用容器,qiankun 会把子应用渲染到这里 -->
  <div id="subapp-container"></div>
</div>
</div>
</template>

<script setup>
// 路由配置(实际项目用 vue-router)
import { useRouter } from 'vue-router'
const router = useRouter()
</script>

<style>
.main-app {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
.navbar {
background: #2c3e50;
color: white;
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-links a {
color: white;
margin-left: 20px;
text-decoration: none;
}
.content {
padding: 30px;
}
#subapp-container {
min-height: 300px;
border: 2px dashed #42b983;
border-radius: 8px;
padding: 20px;
}
</style>

子应用 sub-app/src/main.js 完整代码

import { createApp } from 'vue'
import App from './App.vue'

/**
* qiankun 要求的生命周期钩子
* 必须导出 render/mount/unmount 三个函数
*/

// 保存 app 实例,用于卸载
let app = null

/**
* 渲染函数 - qiankun 调用它来启动子应用
*/
function render(props = {}) {
const { container } = props
app = createApp(App)
// 如果传入了 container,就挂载到 container 内(qiankun 场景)
// 否则挂载到 #subapp(独立运行场景)
const mountPoint = container ? container.querySelector('#subapp') : '#subapp'
app.mount(mountPoint)
}

/**
* 独立运行时的入口(直接访问子应用页面时走这里)
*/
if (!window.__POWERED_BY_QIANKUN__) {
render()
}

/**
* qiankun 导出三个生命周期钩子
*/
export async function bootstrap() {
console.log('[vue3-sub] 应用初始化')
}

export async function mount(props) {
console.log('[vue3-sub] 应用挂载', props)
render(props)
}

export async function unmount() {
console.log('[vue3-sub] 应用卸载')
app.unmount()
app = null
}

子应用 vite.config.js 完整配置

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { qiankunPlugin } from '@originjs/vite-plugin-qiankun'

export default defineConfig({
plugins: [
vue(),
qiankunPlugin({
  name: 'vue3-sub'  // 必须和主应用 registerMicroApps 中的 name 一致
})
],
server: {
port: 5174  // 子应用用不同端口
}
})

运行步骤

# 终端 1:启动主应用
cd main-app && npm run dev

# 终端 2:启动子应用
cd sub-app && npm run dev

# 浏览器访问 http://localhost:5173

点击「Vue3 子应用」链接,你会看到子应用在主应用的 #subapp-container 里渲染了!

代码解释
- registerMicroApps 告诉主应用「有哪些子应用、叫什么名字、什么路径激活」
- 子应用必须导出 bootstrap/mount/unmount 三个生命周期函数
- entry: '//localhost:5174' 表示子应用在另一个端口独立运行


主应用和子应用之间怎么「说话」?

微前端最大的难点不是「能不能跑」,而是「主应用和子应用之间怎么共享数据」。

qiankun 提供了两种方式:

方式 1:通过 props 传递数据

// 主应用注册子应用时,传递 props
registerMicroApps([
{
name: 'vue3-sub',
entry: '//localhost:5174',
container: '#subapp-container',
activeRule: '/vue3-sub',
// 传递给子应用的数据
props: {
  token: 'abc123',        // 比如登录 token
  globalState: {          // 比如全局状态
    userName: '小明',
    role: 'admin'
  },
  // 传递一个方法,子应用可以调用它通知主应用
  onGlobalStateChange: (state) => {
    console.log('全局状态变化了:', state)
  }
}
}
])

方式 2:通过 initGlobalState 全局状态(推荐)

// 主应用 main.js
import { initGlobalState } from 'qiankun'

// 初始化全局状态
const initialState = {
user: null,
theme: 'light'
}
const actions = initGlobalState(initialState)

// 主应用监听变化
actions.onGlobalStateChange((state, prev) => {
console.log('主应用收到状态变化:', state)
// 可以更新自己的 Vue data
})

// 主应用修改状态
function updateGlobalState() {
actions.setGlobalState({
...actions.getGlobalState(),
user: { name: '小红', age: 25 }
})
}

子应用接收和修改状态:

// 子应用 main.js
export async function mount(props) {
render(props)

// 监听主应用的状态变化
props.onGlobalStateChange((state, prev) => {
console.log('子应用收到状态变化:', state)
}, true)  // true 表示立即触发一次

// 读取当前全局状态
console.log('当前全局状态:', props.getGlobalState())

// 修改全局状态(会通知主应用)
props.setGlobalState({
user: { name: '子应用修改了', age: 30 }
})
}

🔥 实战:3 个递进项目

项目 1:子应用独立运行 + 嵌入运行双模式(5 分钟)

需求:让子应用既能独立访问(http://localhost:5174),又能被主应用嵌入。

完整代码 main.js

import { createApp } from 'vue'
import App from './App.vue'

let app = null

function render(props = {}) {
const { container } = props
app = createApp(App)

// 关键:根据是否有 container 决定挂载点
// 有 container → qiankun 嵌入模式
// 无 container → 独立运行模式
const mountPoint = container 
? container.querySelector('#subapp') 
: '#subapp'

app.mount(mountPoint)
}

// 独立运行入口
if (!window.__POWERED_BY_QIANKUN__) {
render()
}

// 导出 qiankun 生命周期
export async function bootstrap() {
console.log('[vue3-sub] 初始化完成')
}

export async function mount(props) {
console.log('[vue3-sub] 挂载到主应用')
render(props)
}

export async function unmount() {
console.log('[vue3-sub] 卸载')
if (app) {
app.unmount()
app = null
}
}

独立运行时的效果:直接访问 http://localhost:5174,页面正常渲染。

嵌入主应用时的效果:子应用被装进 #subapp-container 里,但功能完全一样。

预期输出

独立运行:console 显示 [vue3-sub] 初始化完成
嵌入运行:console 显示 [vue3-sub] 挂载到主应用

一句话解释window.__POWERED_BY_QIANKUN__ 是 qiankun 注入的标记,帮你判断当前运行环境。


项目 2:带登录状态的微前端系统(15 分钟)

需求:主应用负责登录,子应用读取登录状态,未登录时子应用显示「请先登录」。

主应用 App.vue 完整代码

<template>
<div class="main-app">
<nav class="navbar">
  <h1>🏠 主应用</h1>
  <div class="user-area">
    <span v-if="isLoggedIn">👤 {{ userName }}</span>
    <button v-if="!isLoggedIn" @click="login">登录</button>
    <button v-else @click="logout">退出</button>
  </div>
</nav>

<div class="main-content">
  <aside class="sidebar">
    <h3>📋 功能菜单</h3>
    <button @click="navigate('/sub1')">子应用 1</button>
    <button @click="navigate('/sub2')">子应用 2</button>
  </aside>

  <main class="content-area">
    <h2>内容区域</h2>
    <div id="subapp-container"></div>
  </main>
</div>
</div>
</template>

<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun'

const router = useRouter()
const isLoggedIn = ref(false)
const userName = ref('')

function login() {
isLoggedIn.value = true
userName.value = '小明'
}

function logout() {
isLoggedIn.value = false
userName.value = ''
}

function navigate(path) {
router.push(path)
}

// 注册子应用
registerMicroApps([
{
name: 'sub1',
entry: '//localhost:5174',
container: '#subapp-container',
activeRule: '/sub1',
// 把登录状态传递给子应用
props: {
  isLoggedIn,
  userName
}
},
{
name: 'sub2',
entry: '//localhost:5175',
container: '#subapp-container',
activeRule: '/sub2',
props: {
  isLoggedIn,
  userName
}
}
])

setDefaultMountApp('/sub1')
start()
</script>

<style>
.main-app { min-height: 100vh; }
.navbar {
background: #1a1a1a;
color: white;
padding: 15px 30px;
display: flex;
justify-content: space-between;
}
.user-area button {
margin-left: 10px;
padding: 5px 15px;
cursor: pointer;
}
.main-content { display: flex; }
.sidebar {
width: 200px;
background: #f5f5f5;
padding: 20px;
}
.sidebar button {
display: block;
width: 100%;
margin: 10px 0;
padding: 10px;
cursor: pointer;
}
.content-area { flex: 1; padding: 20px; }
#subapp-container {
border: 2px dashed #ccc;
border-radius: 8px;
min-height: 400px;
}
</style>

子应用 App.vue 完整代码

<template>
<div class="subapp">
<h2>📦 子应用 1 - 用户中心</h2>

<!-- 未登录显示提示 -->
<div v-if="!isLoggedIn" class="not-login">
  <p>⚠️ 请先在主应用登录</p>
</div>

<!-- 已登录显示内容 -->
<div v-else class="user-info">
  <p>欢迎回来,<strong>{{ userName }}</strong>!</p>
  <button @click="changeName">改名字</button>
</div>
</div>
</template>

<script setup>
import { ref } from 'vue'

// 从 props 接收主应用传递的数据
const props = defineProps({
isLoggedIn: Boolean,
userName: String
})

const localName = ref(props.userName || '游客')

function changeName() {
localName.value = '子应用用户-' + Date.now()
}
</script>

<style>
.subapp {
padding: 20px;
font-family: Arial, sans-serif;
}
.not-login {
background: #fff3cd;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.user-info {
background: #d4edda;
padding: 20px;
border-radius: 8px;
}
button {
padding: 8px 16px;
cursor: pointer;
background: #28a745;
color: white;
border: none;
border-radius: 4px;
}
</style>

效果
1. 不登录 → 子应用显示「请先在主应用登录」
2. 登录后 → 子应用显示「欢迎回来,小明!」

一句话解释:主应用把 isLoggedInuserName 通过 props 传给子应用,子应用根据这些数据决定显示什么。


项目 3:综合工具——多子应用状态监控面板(15 分钟)

需求:做一个「微前端状态监控面板」,主应用显示所有子应用的运行状态(在线/离线/加载中),点击可以切换子应用。

完整代码 App.vue

<template>
<div class="monitor-panel">
<header class="header">
  <h1>📊 微前端状态监控面板</h1>
  <div class="time">{{ currentTime }}</div>
</header>

<div class="main-layout">
  <!-- 左侧:子应用卡片列表 -->
  <aside class="sidebar">
    <h3>📦 子应用列表</h3>
    <div 
      v-for="app in subApps" 
      :key="app.name"
      class="app-card"
      :class="app.status"
      @click="switchApp(app)"
    >
      <div class="app-name">{{ app.name }}</div>
      <div class="app-status">
        <span class="dot"></span>
        {{ app.statusText }}
      </div>
    </div>
  </aside>

  <!-- 右侧:子应用容器 -->
  <main class="content">
    <div class="container-header">
      <span>当前子应用:{{ activeApp?.name || '无' }}</span>
      <button @click="reloadActiveApp" :disabled="!activeApp">
        🔄 重新加载
      </button>
    </div>
    <div id="subapp-container"></div>
  </main>
</div>

<!-- 底部:全局状态广播 -->
<footer class="footer">
  <h4>📢 全局消息广播</h4>
  <div class="message-input">
    <input 
      v-model="broadcastMessage" 
      placeholder="输入消息发给所有子应用..."
      @keyup.enter="sendBroadcast"
    />
    <button @click="sendBroadcast">发送</button>
  </div>
  <div class="message-list">
    <div 
      v-for="(msg, index) in messages" 
      :key="index"
      class="message-item"
    >
      <span class="msg-time">{{ msg.time }}</span>
      <span class="msg-content">{{ msg.content }}</span>
    </div>
  </div>
</footer>
</div>
</template>

<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import { registerMicroApps, start, initGlobalState } from 'qiankun'

// 状态定义
const currentTime = ref('')
const broadcastMessage = ref('')
const messages = reactive([])
const activeApp = ref(null)

// 子应用配置
const subApps = reactive([
{ name: 'vue3-sub', entry: '//localhost:5174', status: 'offline', statusText: '离线' },
{ name: 'vue3-dashboard', entry: '//localhost:5175', status: 'offline', statusText: '离线' }
])

// 更新时间的定时器
let timer = null
onMounted(() => {
updateTime()
timer = setInterval(updateTime, 1000)

// 初始化全局状态
const actions = initGlobalState({ count: 0, notifications: [] })

// 注册子应用
registerMicroApps(
subApps.map(app => ({
  name: app.name,
  entry: app.entry,
  container: '#subapp-container',
  activeRule: `/${app.name}`,
  props: {
    onGlobalStateChange: (state) => {
      // 收到全局消息
      if (state.latestBroadcast) {
        messages.unshift({
          time: new Date().toLocaleTimeString(),
          content: `[${app.name}] ${state.latestBroadcast}`
        })
      }
    }
  }
}))
)

start()
})

onUnmounted(() => {
if (timer) clearInterval(timer)
})

function updateTime() {
currentTime.value = new Date().toLocaleString()
}

function switchApp(app) {
activeApp.value = app
// 模拟切换(实际项目用路由)
window.history.pushState(null, '', `/${app.name}`)
}

function reloadActiveApp() {
if (activeApp.value) {
// 触发重新加载逻辑
messages.unshift({
  time: new Date().toLocaleTimeString(),
  content: `[系统] 重新加载 ${activeApp.value.name}`
})
}
}

function sendBroadcast() {
if (!broadcastMessage.value.trim()) return
messages.unshift({
time: new Date().toLocaleTimeString(),
content: `[主应用广播] ${broadcastMessage.value}`
})
broadcastMessage.value = ''
}
</script>

<style>
.monitor-panel {
min-height: 100vh;
background: #f0f2f5;
display: flex;
flex-direction: column;
}
.header {
background: #001529;
color: white;
padding: 15px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.main-layout { display: flex; flex: 1; }
.sidebar {
width: 250px;
background: white;
padding: 20px;
}
.app-card {
background: #fafafa;
border: 2px solid #ddd;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
cursor: pointer;
transition: all 0.3s;
}
.app-card:hover { border-color: #1890ff; }
.app-card.active { border-color: #52c41a; background: #f6ffed; }
.app-card.offline { opacity: 0.6; }
.app-status { font-size: 12px; color: #666; margin-top: 5px; }
.dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: #d9d9d9;
margin-right: 5px;
}
.app-card.online .dot { background: #52c41a; }
.content { flex: 1; padding: 20px; }
.container-header {
background: white;
padding: 15px;
border-radius: 8px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
}
#subapp-container {
background: white;
border-radius: 8px;
min-height: 500px;
border: 1px solid #e8e8e8;
}
.footer {
background: white;
padding: 20px;
border-top: 1px solid #e8e8e8;
}
.message-input { display: flex; gap: 10px; margin: 10px 0; }
.message-input input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.message-list { max-height: 100px; overflow-y: auto; }
.message-item {
padding: 5px 10px;
background: #f5f5f5;
margin: 5px 0;
border-radius: 4px;
font-size: 13px;
}
.msg-time { color: #999; margin-right: 10px; }
</style>

效果
- 左侧显示子应用列表及其状态
- 点击切换子应用
- 底部可以向所有子应用广播消息

一句话解释registerMicroApps 批量注册子应用,initGlobalState 实现全局消息广播。


💪 进阶:常见坑 + 性能小贴士

坑 1:子应用样式把主应用样式盖掉了

❌ 错误做法

/* 子应用 App.vue */
.button {
background: blue;
color: white;
}

/* 主应用也定义了 .button,结果被覆盖了 */

✅ 正确做法:启用 CSS 隔离

qiankun 默认使用 Shadow DOM 隔离样式,如果需要更严格的隔离:

import { registerMicroApps, start } from 'qiankun'

start({
// 配置样式隔离策略
getTemplate: (tpl) => tpl,
// scoped CSS 会自动添加唯一属性
singular: false  // 多实例模式
})

或者在子应用中使用 CSS Modules:

<style module>
.button {
background: blue;
}
</style>

坑 2:子应用加载后白屏

❌ 错误原因
- 子应用 mount 函数没有正确挂载到 container
- entry 路径写错了(少写了端口或协议)

✅ 排查步骤

// 1. 浏览器控制台查看 Network 面板
// 看子应用 JS 是否返回 404

// 2. 检查子应用是否正确导出生命周期
export async function mount(props) {
console.log('mount 收到了:', props)
render(props)  // 必须调用 render
}

// 3. 检查主应用 container 是否存在
// HTML 里必须有 <div id="subapp-container"></div>

坑 3:子应用跳转后无法返回

❌ 错误场景:子应用内部用 router.push('/list') 跳转,但浏览器的后退按钮无法回到主应用。

✅ 正确做法:子应用使用主应用提供的路由实例

// 主应用传递 router 给子应用
props: {
mainRouter: router  // vue-router 实例
}

// 子应用使用主应用的路由
props.mainRouter.push('/somewhere')

坑 4:子应用卸载后定时器还在跑

❌ 错误代码

// 子应用 App.vue
onMounted(() => {
this.timer = setInterval(() => {
console.log('定时器 running')
}, 1000)
})
// unmount 时没有清理!

✅ 正确代码

// 子应用 App.vue
import { onMounted, onUnmounted } from 'vue'

let timer = null

onMounted(() => {
timer = setInterval(() => {
console.log('定时器 running')
}, 1000)
})

onUnmounted(() => {
if (timer) {
clearInterval(timer)
timer = null
}
})


坑 5:子应用静态资源 404

❌ 错误原因:子应用打包后用相对路径,但嵌入主应用后路径解析出错。

✅ 正确配置:子应用 vite.config.js 配置 base

export default defineConfig({
base: '/subapp/',  // 必须是绝对路径,不能是相对路径
plugins: [vue(), qiankunPlugin({ name: 'vue3-sub' })],
server: {
port: 5174
}
})

性能优化:预加载子应用

小贴士:用户访问第一个子应用后,qiankun 可以提前加载其他子应用。

import { registerMicroApps, start, prefetchApps } from 'qiankun'

registerMicroApps([...])

start()

// 空闲时预加载所有子应用
prefetchApps([
{ name: 'vue3-sub', entry: '//localhost:5174' },
{ name: 'vue3-dashboard', entry: '//localhost:5175' }
])

效果:用户点击切换时,子应用几乎是秒开(已经预加载好了)。


调试技巧:qiankun 调试面板

在浏览器控制台输入:

// 查看当前注册的子应用
window.__QIANKUN_DEVTOOLS__

// 查看全局状态
window.__POWERED_BY_QIANKUN__  // true 表示在 qiankun 环境

或者在子应用 main.js 加日志:

export async function mount(props) {
console.log('[子应用] 挂载参数:', props)
render(props)
}

✏️ 练习题

练习 1(2 分钟):切换子应用名称

题目:把主应用里注册的子应用名称从 vue3-sub 改成 my-first-sub

输入:原代码中 registerMicroAppsname 字段

预期输出name: 'my-first-sub'

提示:改两个地方:主应用 registerMicroApps 和子应用 vite.config.jsqiankunPlugin 配置。


练习 2(3 分钟):给子应用加登录判断

题目:在子应用 App.vue<script setup> 中加一个判断:如果 isLoggedIn 为 false,显示「未登录」而不是正常内容。

输入propsisLoggedIn: false

预期输出:页面上显示「未登录,请先登录」

提示:用 v-if 指令。


练习 3(5 分钟):添加第三个子应用

题目:参考项目 2,在主应用 registerMicroApps 中添加第三个子应用配置,名字叫 sub3,入口 //localhost:5176

输入:新的 registerMicroApps 配置数组

预期输出:数组长度为 3

提示:复制现有的子应用配置,改名字和入口地址即可。


练习 4(8 分钟):实现状态同步

题目:子应用点击按钮时,调用 props.setGlobalState 把自己的 count 同步到全局状态。

输入:子应用 App.vue 有一个 count 变量,初始值 0

预期输出:点击按钮后,主应用能通过 props.getGlobalState() 读取到新的 count 值

提示:在 props 中解构出 setGlobalStategetGlobalState


练习 5(5 分钟):分析报错

题目:用户报告子应用加载后显示白屏,控制台报错:

Failed to fetch dynamically imported module: 
//localhost:5174/src/main.js?v=xxx
TypeError: Failed to fetch

请分析原因并修复

预期输出:说出 2-3 个可能原因及对应解决方案

提示
- 检查 Network 面板看具体哪个资源 404
- 检查 CORS 头是否配置
- 检查子应用 vite.config.js 是否配置了正确的 qiankunPlugin


作业:做一个「微前端博客阅读器」

需求描述
做一个「博客阅读器」,主应用是导航框架,包含「技术文章」和「生活随笔」两个子应用,分别展示不同分类的文章列表。

功能点
1. 主应用:顶部导航栏,点击切换分类(技术文章/生活随笔)
2. 子应用 A(技术文章):展示 5 篇技术文章标题列表,点击显示摘要
3. 子应用 B(生活随笔):展示 5 篇生活随笔标题列表,点击显示摘要
4. 主应用显示当前阅读量统计(各子应用阅读数累加)

加分项
1. 使用 props 传递主应用的「主题色」(深色/浅色)
2. 子应用切换时显示 loading 动画

验收标准
- 能跑起来(npm run dev 两个终端)
- 点击导航能切换子应用
- 子应用内容独立完整

提交方式:评论区贴代码或 GitHub 链接


📚 总结 + 资源

本章 3 个核心知识点

  1. 微前端核心思想:把大应用拆成「主应用 + 子应用」,各子应用独立开发、独立部署、独立运行
  2. qiankun 最小闭环:主应用 registerMicroApps + 子应用导出 bootstrap/mount/unmount 生命周期
  3. 状态共享方案:通过 props 传递数据,或用 initGlobalState 实现全局状态

延伸学习资源

  1. qiankun 官方文档——最权威的中文教程
  2. Micro Frontends 概念讲解——Martin Fowler 的经典文章
  3. Vue 3 官方教程——配合 qiankun 使用的前置知识

互动钩子:你在公司遇到过「多人维护同一个大项目」的痛苦吗?有没有被 git merge 冲突折磨过?评论区聊聊你的故事,老粉优先回复!


下一章我们要进入 Tauri 的世界——一个用 Rust 写的比 Electron 更轻量的桌面端框架。Electron 包出来的应用动不动几百 MB,Tauri 能做到多少?别走开,下一章我们一起探索「小而美」的桌面端解决方案。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。