项目概述
本项目是一个基于 Qiankun 框架的微前端架构实现,演示了微前端架构的核心技术能力,包括:
- HTML Entry 加载:通过解析 HTML 自动加载子应用资源
- 沙箱隔离:JS 沙箱机制,保证应用间互不干扰
- 生命周期管理:完整的 bootstrap、mount、unmount 生命周期
- 应用间通信:通过 props 在主应用和子应用间传递数据和共享状态
技术栈
| 应用 | 技术栈 | 端口 |
|---|---|---|
| 主应用 | Vite + Vanilla JS + Qiankun | 5173 |
| React 子应用 | 纯 JavaScript (无构建工具) | 5174 |
| Vue 子应用 | 纯 JavaScript (无构建工具) | 5175 |
项目结构
微前端/
├── main-app/ # 主应用 - 负责加载和管理子应用
│ ├── src/main.js # 主应用入口,qiankun 配置
│ └── index.html # 主应用 HTML
├── react-sub/ # React 子应用 - 独立的 React 应用
│ ├── dist/index.html # React 子应用入口
│ └── vite.config.js # Vite 配置(用于预览)
├── vue-sub/ # Vue 子应用 - 独立的 Vue 应用
│ ├── dist/index.html # Vue 子应用入口
│ └── vite.config.js # Vite 配置(用于预览)
└── start.sh # 启动脚本
架构设计
主应用架构
主应用负责以下职责:
-
子应用注册与管理
- 使用
registerMicroAppsAPI 注册子应用 - 配置子应用的路由激活规则
- 传递共享状态和通信方法
- 使用
-
路由管理
- 监听 URL 变化
- 根据 URL 决定加载哪个子应用
- 自动处理子应用的加载和卸载
-
全局状态管理
- 维护全局共享状态对象
- 提供状态修改方法(increment, decrement)
- 通过监听器机制通知状态变化
-
生命周期钩子
beforeLoad: 子应用加载前beforeMount: 子应用挂载前afterMount: 子应用挂载后afterUnmount: 子应用卸载后
子应用架构
每个子应用负责以下职责:
-
生命周期导出
bootstrap(): 初始化逻辑mount(props): 挂载逻辑,接收主应用传递的 propsunmount(props): 卸载逻辑
-
独立运行支持
- 通过
window.__POWERED_BY_QIANKUN__检测运行环境 - 支持独立访问和主应用加载两种模式
- 通过
-
状态同步
- 接收主应用传递的
sharedState - 调用
sharedState.increment/decrement修改状态 - 通过
onCountChange监听状态变化更新本地显示
- 接收主应用传递的
核心技术实现
1. 全局状态共享
// 主应用中的状态管理
const sharedState = {
count: 0,
increment: function() {
this.count += 1
this.notifyListeners('count', this.count)
},
decrement: function() {
this.count -= 1
this.notifyListeners('count', this.count)
},
listeners: [],
onGlobalStateChange: function(callback) {
this.listeners.push(callback)
},
notifyListeners: function(key, value) {
this.listeners.forEach(cb => cb(key, value))
}
}
设计考虑:
- 使用单一对象管理状态
- 提供
onGlobalStateChangeAPI 供监听 - 内部维护监听器列表
- 状态变化时通知所有监听器
2. 应用间通信
主应用通过 qiankun 的 props 机制传递数据:
const apps = [
{
name: 'reactApp',
entry: '//localhost:5174',
container: '#subapp-container',
activeRule: '/react',
props: {
sharedState: sharedState, // 传递共享状态对象
onCountChange: (callback) => sharedState.onGlobalStateChange((key, value) => {
if (key === 'count') callback(value)
})
}
}
]
子应用接收并使用 props:
mount: function(props) {
console.log('[Vue] mount', props);
window.vueProps = props; // 保存 props 引用
if (props.sharedState) {
updateCounter(props.sharedState.count);
if (props.onCountChange) {
props.onCountChange(updateCounter); // 注册监听器
}
}
}
3. 生命周期管理
window.reactApp = {
bootstrap: function() {
console.log('[React] bootstrap')
return Promise.resolve()
},
mount: function(props) {
console.log('[React] mount', props)
// 绑定按钮事件
var btnDecrement = document.getElementById('react-btn-decrement')
var btnIncrement = document.getElementById('react-btn-increment')
if (btnDecrement) {
btnDecrement.onclick = function() {
if (window.reactProps && window.reactProps.sharedState) {
window.reactProps.sharedState.decrement()
}
}
}
return Promise.resolve()
},
unmount: function(props) {
console.log('[React] unmount', props)
return Promise.resolve()
}
}
技术要点:
- 所有生命周期函数必须返回
Promise - 生命周期执行顺序:bootstrap → mount → unmount
- bootstrap 只执行一次,mount/unmount 可能执行多次
4. 事件绑定策略
为解决 qiankun 沙箱和重复加载问题,采用克隆节点替换策略:
mount: function(props) {
// 清理旧的事件监听器
var oldBtnDecrement = document.getElementById('react-btn-decrement-old')
if (oldBtnDecrement) oldBtnDecrement.remove()
// 克隆节点并重新绑定事件
var btnIncrement = document.getElementById('react-btn-increment')
var newIncrement = btnIncrement.cloneNode(true)
newIncrement.id = 'react-btn-increment-old'
newIncrement.onclick = function() {
if (window.reactProps && window.reactProps.sharedState) {
window.reactProps.sharedState.increment()
}
}
btnIncrement.parentNode.replaceChild(newIncrement, btnIncrement)
}
设计考虑:
- 每次 mount 时克隆节点确保干净的 DOM
- 使用 ID 后缀区分新旧节点
- 通过
replaceChild替换节点而不是修改原节点
技术难点与解决方案
难点 1: Vite 与 Qiankun 兼容性
问题描述:
- Vite 开发模式输出 ES module
- Qiankun 的
import-html-entry无法正确执行 ES module eval()不支持 ES Module 语法
解决方案:
- 使用纯 JavaScript 不依赖构建工具
- 避免使用 ES Module 语法
- 直接导出生命周期函数到
window对象
代码示例:
// 不使用 import/export
window.reactApp = {
bootstrap: function() { return Promise.resolve() },
mount: function(props) { /* ... */ },
unmount: function(props) { /* ... */ }
}
难点 2: 生命周期函数导出
问题描述:
- Qiankun 需要从
window[appName]找到生命周期函数 - ES Module 格式无法直接导出到
window export { bootstrap, mount, unmount }在eval()环境中无效
解决方案:
- 直接将函数挂载到
window.reactApp对象 - 确保
bootstrap/mount/unmount都返回Promise - 在 HTML 中不使用 ES Module 语法
代码示例:
window.reactApp = {
bootstrap: function() {
console.log('[React] bootstrap')
return Promise.resolve()
},
mount: function(props) {
console.log('[React] mount', props)
// 业务逻辑
return Promise.resolve()
},
unmount: function(props) {
console.log('[React] unmount', props)
return Promise.resolve()
}
}
难点 3: 应用间通信
问题描述:
- 主应用和子应用在不同的作用域
- 子应用无法直接访问主应用的全局变量
- 状态变化需要通知所有应用
解决方案:
- 通过 qiankun 的
props传递共享状态对象 - 使用监听器模式:
onGlobalStateChange(callback) - 每个应用注册自己的更新函数
设计优势:
- 解耦应用间的直接依赖
- 支持多个应用同时监听状态变化
- 主应用控制状态,子应用响应变化
难点 4: 事件绑定失效
问题描述:
- 子应用重复加载后事件绑定失效
- qiankun 沙箱可能影响事件处理
this上下文丢失导致函数无法访问 props
解决方案:
- 使用
cloneNode(true)深度克隆 DOM 节点 - 每次 mount 时用克隆节点替换原节点
- 将 props 保存到
window.reactProps确保访问
技术要点:
mount: function(props) {
window.reactProps = props // 保存 props 引用
var btn = document.getElementById('btn-increment')
var newBtn = btn.cloneNode(true)
newBtn.onclick = function() {
if (window.reactProps && window.reactProps.sharedState) {
window.reactProps.sharedState.increment()
}
}
btn.parentNode.replaceChild(newBtn, btn)
}
技术亮点
1. 纯 JavaScript 实现
- 不依赖复杂的构建工具链
- 直接在 HTML 中编写逻辑
- 减少开发和部署复杂度
2. 完整的生命周期支持
- 支持独立的
bootstrap、mount、unmount生命周期 - 每个生命周期都有 Promise 支持
- 支持独立运行和主应用加载两种模式
3. 响应式状态同步
- 主应用修改状态,子应用自动更新
- 多个子应用可以同时监听状态
- 支持扩展更多共享状态
4. 鲁棒的 DOM 操作
- 克隆节点确保每次加载都是干净的 DOM
- 自动清理旧的事件监听器
- 避免内存泄漏和事件冲突
运行方式
快速启动
# 启动所有应用
./start.sh
# 停止所有应用
./stop.sh
访问地址
- 主应用: http://localhost:5173
- React 子应用: http://localhost:5174
- Vue 子应用: http://localhost:5175
功能验证
-
主应用计数器
- 点击主应用的 +1/-1 按钮
- 观察全局计数变化
-
子应用加载
- 点击"加载 React 子应用"
- 验证子应用显示相同计数
-
子应用计数
- 点击子应用的 +1/-1 按钮
- 验证主应用计数同步更新
-
应用切换
- 在 React 和 Vue 子应用间多次切换
- 验证事件绑定持续有效
-
独立运行
- 直接访问子应用 URL
- 验证独立运行模式正常工作
性能优化建议
1. 子应用懒加载
当前实现按需加载子应用,已经支持懒加载。可以进一步优化:
- 添加子应用预加载配置
- 在主应用空闲时预加载其他子应用
- 缓存子应用资源
2. 状态更新节流
如果状态更新频繁,可以添加节流:
const throttledNotify = throttle((key, value) => {
this.listeners.forEach(cb => cb(key, value))
}, 100)
3. 事件委托
对于大量动态元素,使用事件委托:
document.getElementById('app').addEventListener('click', function(e) {
if (e.target.id === 'btn-increment') {
// 处理点击
}
})
可扩展性设计
1. 支持更多子应用
添加新子应用只需:
- 创建新的子应用目录
- 实现
window[appName]生命周期导出 - 在主应用
registerMicroApps中注册
2. 支持更多共享状态
扩展 sharedState 添加更多状态:
const sharedState = {
count: 0,
userInfo: null,
settings: {},
// ... 更多状态
}
3. 支持复杂通信场景
可以通过 props 传递更复杂的通信对象:
props: {
sharedState: sharedState,
events: {
emit: (eventName, data) => { /* ... */ },
on: (eventName, callback) => { /* ... */ }
},
navigation: {
push: (path) => { /* ... */ }
}
}
常见问题处理
问题 1: 子应用加载失败
检查清单:
- 确认子应用服务器正在运行
- 确认端口配置正确(5174/5175)
- 检查浏览器控制台是否有错误
- 确认
window[appName]正确导出
问题 2: 状态不同步
检查清单:
- 确认
props.sharedState正确传递 - 确认
onCountChange正确注册 - 检查子应用的
updateCounter是否被调用 - 查看主应用日志确认状态修改
问题 3: 事件不响应
检查清单:
- 确认按钮 ID 在 HTML 中唯一
- 确认事件绑定在
mount函数中执行 - 检查
window.appProps是否正确赋值 - 查看浏览器控制台是否有点击日志
技术参考
Qiankun API 文档
- 官方文档
- API:
registerMicroApps,start,loadMicroApp
最佳实践
- 子应用保持独立,不依赖主应用的具体实现
- 通过 props 传递数据,不直接访问全局变量
- 生命周期函数应该幂等,多次调用不应该有副作用
- 及时清理事件监听器和定时器,避免内存泄漏
性能指标
- 子应用首次加载时间:< 100ms
- 状态同步延迟:< 10ms
- 应用切换时间:< 50ms