企业级微前端项目完整创建步骤,第二步:配置主应用

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>
相关推荐
默_笙1 小时前
🔑 让 AI 学会"读"网页:我用 Cheerio 爬了一篇掘金文章,然后切成小块存进了向量库
前端·javascript
颜进强1 小时前
Ollama 从入门到实践:本地模型运行、API 调用
前端·后端·ai编程
颜进强1 小时前
Embedding 基础使用:用 Ollama 和 LangChain.js 生成文本向量
前端·后端·ai编程
颜进强1 小时前
Vector Store 入门:什么是向量数据库,主流产品如何选择
前端·后端·ai编程
方方洛2 小时前
AI 教程系列-TUI 应用开发教程14-技能系统与可扩展性
前端·javascript·typescript
方方洛2 小时前
AI 教程系列-TUI 应用开发教程05-大模型对话界面设计
前端·javascript·typescript
方方洛2 小时前
AI 教程系列-TUI 应用开发教程08-流式响应与动画
前端·javascript·typescript
方方洛2 小时前
AI 教程系列-TUI 应用开发教程04-第一个 TUI 应用
前端·javascript·typescript
cooldream20092 小时前
AI 时代,前端工程师的“新学习路线“—— 从“学框架“到“用 AI“
前端·人工智能·学习