2.1 安装依赖
javascript
cd main-app
npm install qiankun
npm install vue-router@4
2.2 创建微前端配置文件 src/micro-app.ts
javascript
/**
* 微前端核心配置文件
* 负责:子应用注册、全局状态管理、跨应用通信
*/
import { registerMicroApps, start } from 'qiankun'
// ==================== 全局状态管理 ====================
/**
* 全局状态接口定义
* 包含用户信息、令牌、消息和计数器
*/
interface GlobalState {
user: { name: string; role: string } | null // 当前用户
token: string // 登录令牌
message: string // 子应用传递的消息
count: number // 计数器,用于演示状态同步
}
/**
* 从 localStorage 恢复状态
* 解决页面刷新后全局状态丢失的问题
*/
const loadState = (): GlobalState => {
try {
// 从 localStorage 读取保存的状态
const saved = localStorage.getItem('__MICRO_APP_GLOBAL_STATE__')
if (saved) {
return JSON.parse(saved)
}
} catch (e) {
console.warn('Failed to load state from localStorage:', e)
}
// 返回默认初始状态
return {
user: null,
token: '',
message: '',
count: 0,
}
}
// 初始化全局状态(优先从 localStorage 恢复)
let globalState: GlobalState = loadState()
// 状态变更回调列表(用于通知所有订阅者)
const callbacks: Array<(state: GlobalState, prev: GlobalState) => void> = []
/**
* 跨应用通信接口
* 提供给子应用调用,实现主应用与子应用、子应用与子应用之间的通信
*/
const actions = {
/**
* 获取当前全局状态
* @returns GlobalState 当前全局状态
*/
getGlobalState: () => globalState,
/**
* 设置全局状态
* @param newState 要更新的状态对象
* @returns boolean 更新是否成功
*/
setGlobalState: (newState: Partial<GlobalState>) => {
// 保存更新前的状态
const prevState = { ...globalState }
// 合并新状态
globalState = { ...globalState, ...newState }
console.log('[主应用] Global state changed:', prevState, '->', globalState)
// 保存到 localStorage,解决页面刷新后状态丢失问题
localStorage.setItem('__MICRO_APP_GLOBAL_STATE__', JSON.stringify(globalState))
// 通知所有订阅状态变更的子应用
callbacks.forEach(callback => callback(globalState, prevState))
return true
},
/**
* 订阅全局状态变更
* @param callback 状态变更时的回调函数
* @returns () => void 取消订阅函数
*/
onGlobalStateChange: (callback: (state: GlobalState, prev: GlobalState) => void) => {
callbacks.push(callback)
// 返回取消订阅函数,方便子应用卸载时清理
return () => {
const index = callbacks.indexOf(callback)
if (index > -1) {
callbacks.splice(index, 1)
}
}
},
/**
* 路由跳转
* 使用 window.location.href 确保路由正确切换
* 注意:使用 history.pushState 可能导致主应用路由不响应
* @param path 目标路径
*/
push: (path: string) => {
window.location.href = path
},
/**
* 主应用通知方法
* 子应用可以调用此方法在主应用中显示通知
* @param message 通知内容
* @param type 通知类型:success/error/info
*/
showNotification: (message: string, type: 'success' | 'error' | 'info' = 'info') => {
console.log(`[主应用通知] [${type}] ${message}`)
// 创建通知元素
const notification = document.createElement('div')
notification.style.cssText = `
position: fixed;
top: 80px;
right: 20px;
padding: 16px 24px;
border-radius: 8px;
color: white;
font-weight: bold;
z-index: 9999;
animation: slideIn 0.3s ease;
background: ${type === 'success' ? '#52c41a' : type === 'error' ? '#ff4d4f' : '#1890ff'};
`
notification.innerHTML = `
<style>@keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } }</style>
${message}
`
document.body.appendChild(notification)
// 3秒后自动移除通知
setTimeout(() => notification.remove(), 3000)
},
/**
* 增加计数器(便捷方法)
*/
incrementCount: () => {
actions.setGlobalState({ count: globalState.count + 1 })
},
/**
* 重置计数器(便捷方法)
*/
resetCount: () => {
actions.setGlobalState({ count: 0 })
},
}
// ==================== 初始化全局状态 ====================
/**
* 只有在 localStorage 没有保存状态时才设置初始状态
* 解决页面刷新后初始状态覆盖已保存状态的问题
*/
const hasSavedState = localStorage.getItem('__MICRO_APP_GLOBAL_STATE__')
if (!hasSavedState) {
actions.setGlobalState({
user: { name: '张三', role: 'admin' },
token: 'xxx-token-xxx',
})
}
// ==================== 挂载通信接口 ====================
/**
* 将 actions 挂载到 window 对象上
* 供子应用直接访问,解决 props 传递失败的问题
*/
;(window as any).__MICRO_APP_ACTIONS__ = actions
// ==================== 注册子应用 ====================
/**
* 注册子应用列表
* 每个子应用需要配置:
* - name: 子应用唯一标识
* - entry: 子应用入口地址(开发时使用本地地址,生产时使用部署地址)
* - container: 子应用挂载容器的选择器
* - activeRule: 子应用激活规则(路由匹配)
* - props: 传递给子应用的参数
*/
registerMicroApps([
{
name: 'vue-app', // 子应用唯一标识
entry: '//localhost:8081/', // 子应用入口地址
container: '#subapp-container', // 子应用挂载容器
activeRule: '/vue-app', // 路由匹配规则
props: { actions }, // 传递给子应用的参数
},
{
name: 'react-app', // 子应用唯一标识
entry: '//localhost:8082/', // 子应用入口地址
container: '#subapp-container', // 子应用挂载容器
activeRule: '/react-app', // 路由匹配规则
props: { actions }, // 传递给子应用的参数
},
])
// 导出 actions 和 start 方法,供 main.ts 使用
export { actions, start }
2.3 创建路由配置 src/router/index.ts
javascript
/**
* 主应用路由配置
* 负责:主应用页面路由、子应用容器路由
*/
import { createRouter, createWebHistory } from 'vue-router'
import { defineComponent, h } from 'vue'
import HomePage from '../components/HomePage.vue'
/**
* 子应用容器组件
* 创建一个包含子应用挂载点的组件
* qiankun 会自动将子应用渲染到 #subapp-container 中
*/
const SubAppContainer = defineComponent({
name: 'SubAppContainer',
render() {
// 返回一个带有 id="subapp-container" 的 div
// qiankun 会在这个容器中挂载子应用
return h('div', { id: 'subapp-container' })
},
})
/**
* 路由配置数组
* 定义主应用的所有路由
*/
const routes = [
{
path: '/', // 根路径
name: 'home', // 路由名称
component: HomePage, // 主应用首页组件
},
{
path: '/vue-app', // Vue子应用路径
name: 'vue-app', // 路由名称
component: SubAppContainer, // 子应用容器组件
},
{
path: '/react-app', // React子应用路径
name: 'react-app', // 路由名称
component: SubAppContainer, // 子应用容器组件
},
]
/**
* 创建路由实例
* 使用 HTML5 History 模式(不带 # 号)
*/
const router = createRouter({
history: createWebHistory(),
routes,
})
/**
* 全局路由守卫
* 用于调试路由变化,排查路由问题
*/
router.beforeEach((to, from, next) => {
console.log('[主应用] Route change:', from.path, '->', to.path)
next()
})
export default router
2.4 创建主应用入口 src/main.ts
javascript
/**
* 主应用入口文件
* 负责:初始化 Vue 应用、配置路由、启动 qiankun
*/
import { createApp } from 'vue'
import './style.css' // 全局样式
import App from './App.vue' // 根组件
import router from './router' // 路由配置
import { start } from './micro-app' // qiankun 配置
// 创建 Vue 应用实例
const app = createApp(App)
// 安装路由插件
app.use(router)
// 挂载到 DOM
app.mount('#app')
/**
* 启动 qiankun 微前端框架
* 配置项说明:
* - sandbox: 沙箱配置
* - strictStyleIsolation: 严格样式隔离(3.0版本后已废弃,建议使用 experimentalStyleIsolation)
*/
start({
sandbox: {
// strictStyleIsolation: true, // 已废弃,会报警告
},
})
2.5 创建主应用根组件 src/App.vue
javascript
<template>
<div class="main-app">
<!-- 主应用导航栏 -->
<header class="header">
<h1>🏢 企业级微前端平台</h1>
<nav>
<!-- 使用 router-link 进行路由跳转 -->
<router-link to="/">首页</router-link>
<router-link to="/vue-app">Vue 子应用</router-link>
<router-link to="/react-app">React 子应用</router-link>
</nav>
</header>
<!-- 路由视图 -->
<!-- 渲染当前路由对应的组件(首页或子应用容器) -->
<main class="main-content">
<router-view></router-view>
</main>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
// 组件挂载时的生命周期钩子
onMounted(() => {
console.log('[主应用] 已挂载')
})
</script>
<style>
/* 主应用全局样式 */
.main-app {
min-height: 100vh;
background: #f5f5f5;
}
.header {
background: #1890ff;
color: white;
padding: 16px 24px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header nav {
display: flex;
gap: 24px;
}
.header a {
color: white;
text-decoration: none;
font-size: 16px;
}
.header a:hover {
text-decoration: underline;
}
.main-content {
padding: 24px;
}
/* 子应用容器样式 */
#subapp-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-height: 600px;
}
</style>
2.6 创建主应用首页组件 src/components/HomePage.vue
javascript
<template>
<div class="home-page">
<h2>🏠 主应用首页</h2>
<p>欢迎来到企业级微前端平台!</p>
<!-- 全局状态展示 -->
<div class="state-panel">
<h3>📊 全局状态</h3>
<div class="state-item">
<span class="label">当前用户:</span>
<span class="value">{{ globalState.user?.name || '未登录' }}</span>
</div>
<div class="state-item">
<span class="label">用户角色:</span>
<span class="value">{{ globalState.user?.role || '普通用户' }}</span>
</div>
<div class="state-item">
<span class="label">计数器:</span>
<span class="value highlight">{{ globalState.count }}</span>
</div>
<div class="state-item">
<span class="label">子应用消息:</span>
<span class="value">{{ globalState.message || '暂无消息' }}</span>
</div>
</div>
<!-- 主应用操作区 -->
<div class="action-panel">
<h3>🎮 主应用操作</h3>
<div class="action-buttons">
<button @click="updateUser">更新用户信息</button>
<button @click="incrementCount">增加计数器</button>
<button @click="resetCount">重置计数器</button>
</div>
</div>
<!-- 子应用入口 -->
<div class="subapp-links">
<h3>🚀 子应用入口</h3>
<div class="subapp-cards">
<router-link to="/vue-app" class="subapp-card">
<div class="icon">🟢</div>
<div class="info">
<h4>Vue 3 子应用</h4>
<p>基于 Vue 3 + Vite 构建</p>
</div>
</router-link>
<router-link to="/react-app" class="subapp-card">
<div class="icon">🟣</div>
<div class="info">
<h4>React 18 子应用</h4>
<p>基于 React 18 + Webpack 构建</p>
</div>
</router-link>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
// 获取主应用的 actions(从 window 对象)
const getActions = () => {
return (window as any).__MICRO_APP_ACTIONS__
}
const actions = getActions()
// 全局状态
const globalState = ref(actions?.getGlobalState?.() || {})
let unsubscribe: (() => void) | undefined
// 更新用户信息
const updateUser = () => {
if (actions?.setGlobalState) {
const users = ['张三', '李四', '王五', '赵六']
const randomUser = users[Math.floor(Math.random() * users.length)]
actions.setGlobalState({ user: { name: randomUser, role: 'admin' } })
actions.showNotification('用户信息已更新', 'success')
}
}
// 增加计数器
const incrementCount = () => {
if (actions?.incrementCount) {
actions.incrementCount()
}
}
// 重置计数器
const resetCount = () => {
if (actions?.resetCount) {
actions.resetCount()
}
}
onMounted(() => {
console.log('[主应用] HomePage 已挂载')
// 订阅全局状态变更
if (actions?.onGlobalStateChange) {
unsubscribe = actions.onGlobalStateChange((state) => {
globalState.value = state
})
}
})
onUnmounted(() => {
console.log('[主应用] HomePage 已卸载')
// 取消订阅,避免内存泄漏
if (unsubscribe) {
unsubscribe()
}
})
</script>
<style scoped>
.home-page {
max-width: 1200px;
margin: 0 auto;
}
.state-panel, .action-panel, .subapp-links {
background: white;
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.state-item {
display: flex;
margin-bottom: 12px;
}
.label {
font-weight: bold;
width: 120px;
}
.value {
color: #1890ff;
}
.value.highlight {
font-size: 18px;
font-weight: bold;
}
.action-buttons {
display: flex;
gap: 12px;
}
.action-buttons button {
padding: 10px 20px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.action-buttons button:hover {
background: #40a9ff;
}
.subapp-cards {
display: flex;
gap: 24px;
}
.subapp-card {
display: flex;
align-items: center;
gap: 16px;
padding: 24px;
background: #f5f5f5;
border-radius: 8px;
text-decoration: none;
color: #333;
flex: 1;
}
.subapp-card:hover {
background: #e6f7ff;
}
.icon {
font-size: 48px;
}
.info h4 {
margin: 0 0 8px 0;
}
.info p {
margin: 0;
color: #666;
}
</style>