返回笔记首页

21.7 微前端工程化

主题配置

一、子应用开发规范

1.1 生命周期钩子规范

javascript
// 子应用 main.js 标准模板

import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import App from './App.vue'
import routes from './router'
import './public-path' // 公共路径配置

let instance = null
let router = null
let history = null

/**
 * 渲染函数
 * @param {Object} props - 主应用传递的参数
 */
function render(props = {}) {
  const { container, routerBase } = props

  // 创建路由实例
  history = createWebHistory(window.__POWERED_BY_QIANKUN__ ? routerBase : '/')
  router = createRouter({
    history,
    routes
  })

  // 创建Vue实例
  instance = createApp(App)
  instance.use(router)

  // 挂载到容器
  const mountNode = container
    ? container.querySelector('#app')
    : document.querySelector('#app')
  instance.mount(mountNode)
}

/**
 * 生命周期:应用首次加载时调用,只会调用一次
 */
export async function bootstrap() {
  console.log('[子应用] bootstrap')
  // 这里可以做一些全局初始化工作
  // 比如:
  // - 设置全局错误处理
  // - 初始化监控 SDK
  // - 加载全局配置
}

/**
 * 生命周期:应用每次激活时调用
 * @param {Object} props - 主应用传递的参数
 */
export async function mount(props) {
  console.log('[子应用] mount', props)

  // 保存主应用传递的props
  window.__MAIN_APP_PROPS__ = props

  // 监听全局状态变化
  if (props.onGlobalStateChange) {
    props.onGlobalStateChange((state, prev) => {
      console.log('[子应用] 全局状态变化', state, prev)
      // 可以同步到子应用的 store
    }, true)
  }

  // 渲染应用
  render(props)
}

/**
 * 生命周期:应用每次失活/卸载时调用
 */
export async function unmount() {
  console.log('[子应用] unmount')

  // 销毁Vue实例
  if (instance) {
    instance.unmount()
    instance = null
  }

  // 清理路由
  if (router) {
    router = null
  }

  if (history) {
    history = null
  }

  // 清理全局变量
  window.__MAIN_APP_PROPS__ = null
}

/**
 * 生命周期:主应用 props 更新时调用(可选)
 * @param {Object} props - 更新后的 props
 */
export async function update(props) {
  console.log('[子应用] update', props)
  window.__MAIN_APP_PROPS__ = props
}

/**
 * 独立运行模式
 */
if (!window.__POWERED_BY_QIANKUN__) {
  render()
}

1.2 环境判断规范

javascript
// utils/env.js

/**
 * 判断是否在微前端环境运行
 */
export function isMicroApp() {
  return window.__POWERED_BY_QIANKUN__ ||
         window.__MICRO_APP_ENVIRONMENT__ ||
         window.$wujie
}

/**
 * 获取微前端类型
 */
export function getMicroAppType() {
  if (window.__POWERED_BY_QIANKUN__) return 'qiankun'
  if (window.__MICRO_APP_ENVIRONMENT__) return 'micro-app'
  if (window.$wujie) return 'wujie'
  return 'standalone'
}

/**
 * 获取基础路径
 */
export function getBasePath() {
  if (isMicroApp()) {
    return window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ ||
           window.__MICRO_APP_BASE_ROUTE__ ||
           '/'
  }
  return process.env.BASE_URL || '/'
}

/**
 * 获取 API 基础地址
 */
export function getApiBaseURL() {
  if (isMicroApp()) {
    // 微前端环境,使用主应用提供的 API 地址
    return window.__MAIN_APP_PROPS__?.apiBaseURL || '/api'
  }
  // 独立运行,使用环境变量
  return process.env.VUE_APP_API_BASE_URL || '/api'
}

/**
 * 获取主应用传递的数据
 */
export function getMainAppData() {
  return window.__MAIN_APP_PROPS__ || {}
}

// 使用示例
import { isMicroApp, getApiBaseURL } from '@/utils/env'

if (isMicroApp()) {
  console.log('运行在微前端环境')
}

const api = axios.create({
  baseURL: getApiBaseURL()
})

1.3 导出格式规范

javascript
// vue.config.js

const { name } = require('./package.json')

module.exports = {
  devServer: {
    port: 8081,
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  },

  configureWebpack: {
    output: {
      // 库名称,唯一
      library: `${name}-[name]`,
      // UMD 格式
      libraryTarget: 'umd',
      // webpack jsonp 函数名,唯一
      jsonpFunction: `webpackJsonp_${name}`,
      // 全局变量名
      globalObject: 'window'
    }
  }
}

1.4 publicPath 配置规范

javascript
// public-path.js

// qiankun 环境
if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
// micro-app 环境
else if (window.__MICRO_APP_ENVIRONMENT__) {
  __webpack_public_path__ = window.__MICRO_APP_BASE_ROUTE__
}
// wujie 环境
else if (window.$wujie) {
  __webpack_public_path__ = window.$wujie.props.publicPath || '/'
}
// 独立运行
else {
  __webpack_public_path__ = '/'
}

// main.js 第一行引入
import './public-path'

二、基座应用设计

2.1 统一 Layout

vue
<template>
  <div class="main-app-layout">
    <!-- 顶部导航 -->
    <header class="main-header">
      <div class="logo">
        <img src="@/assets/logo.png" alt="Logo">
        <span>微前端平台</span>
      </div>

      <nav class="main-nav">
        <a
          v-for="menu in menus"
          :key="menu.path"
          :href="menu.path"
          :class="{ active: isActive(menu.path) }"
          @click.prevent="navigate(menu.path)"
        >
          {{ menu.name }}
        </a>
      </nav>

      <div class="user-area">
        <el-dropdown @command="handleCommand">
          <span class="user-info">
            <img :src="userInfo.avatar" class="avatar">
            <span>{{ userInfo.name }}</span>
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item command="profile">个人中心</el-dropdown-item>
              <el-dropdown-item command="settings">设置</el-dropdown-item>
              <el-dropdown-item divided command="logout">退出登录</el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </div>
    </header>

    <!-- 侧边栏(可选) -->
    <aside class="main-aside" v-if="showAside">
      <el-menu
        :default-active="currentPath"
        @select="handleMenuSelect"
      >
        <el-menu-item
          v-for="item in sideMenus"
          :key="item.path"
          :index="item.path"
        >
          <span>{{ item.name }}</span>
        </el-menu-item>
      </el-menu>
    </aside>

    <!-- 主内容区 -->
    <main class="main-content" :class="{ 'has-aside': showAside }">
      <!-- 面包屑 -->
      <div class="breadcrumb">
        <el-breadcrumb separator="/">
          <el-breadcrumb-item
            v-for="item in breadcrumbs"
            :key="item.path"
            :to="{ path: item.path }"
          >
            {{ item.name }}
          </el-breadcrumb-item>
        </el-breadcrumb>
      </div>

      <!-- 子应用容器 -->
      <div id="subapp-container" class="subapp-wrapper"></div>

      <!-- 主应用路由 -->
      <router-view v-if="!isSubApp"></router-view>
    </main>

    <!-- 页脚(可选) -->
    <footer class="main-footer">
      <p>&copy; 2024 微前端平台. All rights reserved.</p>
    </footer>

    <!-- 全局加载提示 -->
    <div v-if="loading" class="global-loading">
      <el-loading text="加载中..." />
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'

const router = useRouter()
const route = useRoute()
const userStore = useUserStore()

const loading = ref(false)
const currentPath = ref(route.path)

// 顶部菜单
const menus = ref([
  { name: '首页', path: '/' },
  { name: '应用1', path: '/app1' },
  { name: '应用2', path: '/app2' },
  { name: '应用3', path: '/app3' }
])

// 侧边栏菜单
const sideMenus = ref([])

// 面包屑
const breadcrumbs = computed(() => {
  const paths = route.path.split('/').filter(Boolean)
  return paths.map((path, index) => ({
    name: path,
    path: '/' + paths.slice(0, index + 1).join('/')
  }))
})

// 用户信息
const userInfo = computed(() => userStore.userInfo)

// 是否显示侧边栏
const showAside = computed(() => {
  return route.path.startsWith('/app')
})

// 是否是子应用路由
const isSubApp = computed(() => {
  return route.path.startsWith('/app')
})

// 导航
const navigate = (path) => {
  router.push(path)
}

// 菜单选择
const handleMenuSelect = (path) => {
  router.push(path)
}

// 判断菜单是否激活
const isActive = (path) => {
  return route.path.startsWith(path)
}

// 用户操作
const handleCommand = (command) => {
  switch (command) {
    case 'profile':
      router.push('/profile')
      break
    case 'settings':
      router.push('/settings')
      break
    case 'logout':
      logout()
      break
  }
}

const logout = () => {
  userStore.logout()
  router.push('/login')
}

// 监听路由变化
watch(() => route.path, (newPath) => {
  currentPath.value = newPath
})
</script>

<style scoped>
.main-app-layout {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

.main-header {
  display: flex;
  align-items: center;
  height: 64px;
  padding: 0 24px;
  background: #001529;
  color: white;
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}

.logo {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 20px;
  font-weight: bold;
}

.logo img {
  height: 32px;
}

.main-nav {
  display: flex;
  gap: 8px;
  flex: 1;
  margin-left: 50px;
}

.main-nav a {
  padding: 8px 20px;
  color: rgba(255, 255, 255, 0.65);
  text-decoration: none;
  border-radius: 4px;
  transition: all 0.3s;
}

.main-nav a:hover {
  background: rgba(255, 255, 255, 0.1);
  color: white;
}

.main-nav a.active {
  background: #1890ff;
  color: white;
}

.user-area {
  display: flex;
  align-items: center;
}

.user-info {
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  padding: 8px 12px;
  border-radius: 4px;
  transition: all 0.3s;
}

.user-info:hover {
  background: rgba(255, 255, 255, 0.1);
}

.avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
}

.main-aside {
  width: 200px;
  background: white;
  box-shadow: 2px 0 8px rgba(0,0,0,0.08);
}

.main-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  background: #f0f2f5;
  overflow: hidden;
}

.main-content.has-aside {
  margin-left: 200px;
}

.breadcrumb {
  padding: 16px 24px;
  background: white;
  border-bottom: 1px solid #e8e8e8;
}

.subapp-wrapper {
  flex: 1;
  overflow: auto;
}

.main-footer {
  padding: 16px 24px;
  text-align: center;
  background: white;
  border-top: 1px solid #e8e8e8;
  color: #666;
  font-size: 14px;
}

.global-loading {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
}
</style>

2.2 导航菜单管理

javascript
// stores/menu.js
import { defineStore } from 'pinia'

export const useMenuStore = defineStore('menu', {
  state: () => ({
    menus: [],
    permissions: []
  }),

  getters: {
    // 根据权限过滤菜单
    visibleMenus() {
      return this.menus.filter(menu => {
        return !menu.permission || this.permissions.includes(menu.permission)
      })
    }
  },

  actions: {
    // 加载菜单配置
    async loadMenus() {
      try {
        const res = await fetch('/api/menus')
        const data = await res.json()

        this.menus = data.menus
        this.permissions = data.permissions
      } catch (error) {
        console.error('加载菜单失败', error)
      }
    },

    // 动态注册子应用菜单
    registerAppMenus(appName, menus) {
      const existingIndex = this.menus.findIndex(m => m.app === appName)

      if (existingIndex > -1) {
        this.menus[existingIndex].children = menus
      } else {
        this.menus.push({
          app: appName,
          children: menus
        })
      }
    }
  }
})

// 使用示例
const menuStore = useMenuStore()

// 主应用启动时加载菜单
await menuStore.loadMenus()

// 子应用注册自己的菜单
menuStore.registerAppMenus('app1', [
  { name: '功能1', path: '/app1/feature1' },
  { name: '功能2', path: '/app1/feature2' }
])

2.3 用户信息共享

javascript
// stores/user.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null,
    token: null,
    permissions: []
  }),

  getters: {
    isLoggedIn() {
      return !!this.token
    },

    hasPermission() {
      return (permission) => {
        return this.permissions.includes(permission)
      }
    }
  },

  actions: {
    async login(username, password) {
      try {
        const res = await fetch('/api/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ username, password })
        })

        const data = await res.json()

        this.userInfo = data.userInfo
        this.token = data.token
        this.permissions = data.permissions

        // 保存到 localStorage
        localStorage.setItem('token', data.token)

        // 同步到全局状态
        if (window.__QIANKUN_ACTIONS__) {
          window.__QIANKUN_ACTIONS__.setGlobalState({
            user: this.userInfo,
            token: this.token
          })
        }

        return true
      } catch (error) {
        console.error('登录失败', error)
        return false
      }
    },

    logout() {
      this.userInfo = null
      this.token = null
      this.permissions = []

      localStorage.removeItem('token')

      // 通知所有子应用
      if (window.__QIANKUN_ACTIONS__) {
        window.__QIANKUN_ACTIONS__.setGlobalState({
          user: null,
          token: null
        })
      }
    },

    async loadUserInfo() {
      const token = localStorage.getItem('token')

      if (!token) return false

      try {
        const res = await fetch('/api/user/info', {
          headers: { Authorization: `Bearer ${token}` }
        })

        const data = await res.json()

        this.userInfo = data.userInfo
        this.token = token
        this.permissions = data.permissions

        return true
      } catch (error) {
        console.error('加载用户信息失败', error)
        this.logout()
        return false
      }
    }
  }
})

2.4 权限控制

javascript
// router/permission.js
import router from './index'
import { useUserStore } from '@/stores/user'

// 白名单路由
const whiteList = ['/', '/login', '/404']

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()

  // 已登录
  if (userStore.isLoggedIn) {
    if (to.path === '/login') {
      next('/')
    } else {
      // 检查权限
      if (to.meta.permission) {
        if (userStore.hasPermission(to.meta.permission)) {
          next()
        } else {
          next('/403')
        }
      } else {
        next()
      }
    }
  }
  // 未登录
  else {
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
    }
  }
})

// 子应用权限检查
export function checkAppPermission(appName) {
  const userStore = useUserStore()
  const permission = `app:${appName}`
  return userStore.hasPermission(permission)
}

2.5 错误边界

vue
<!-- components/ErrorBoundary.vue -->
<template>
  <div v-if="error" class="error-boundary">
    <div class="error-icon">⚠️</div>
    <h2>应用加载失败</h2>
    <p class="error-message">{{ error.message }}</p>
    <div class="error-actions">
      <button @click="retry" class="btn-retry">重试</button>
      <button @click="goHome" class="btn-home">返回首页</button>
    </div>
    <details v-if="showDetails">
      <summary>错误详情</summary>
      <pre>{{ error.stack }}</pre>
    </details>
  </div>
  <slot v-else></slot>
</template>

<script setup>
import { ref, onErrorCaptured } from 'vue'
import { useRouter } from 'vue-router'

const router = useRouter()
const error = ref(null)
const showDetails = ref(process.env.NODE_ENV === 'development')

onErrorCaptured((err, instance, info) => {
  console.error('捕获到错误', err, info)
  error.value = err

  // 上报错误
  reportError(err, info)

  return false
})

const retry = () => {
  error.value = null
  window.location.reload()
}

const goHome = () => {
  error.value = null
  router.push('/')
}

const reportError = (err, info) => {
  // 上报到监控系统
  if (window.__MONITOR__) {
    window.__MONITOR__.captureError(err, {
      component: info,
      type: 'vue-error'
    })
  }
}
</script>

<style scoped>
.error-boundary {
  padding: 60px 20px;
  text-align: center;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.08);
}

.error-icon {
  font-size: 72px;
  margin-bottom: 20px;
}

h2 {
  color: #ff4d4f;
  margin-bottom: 12px;
}

.error-message {
  color: #666;
  margin-bottom: 24px;
}

.error-actions {
  display: flex;
  gap: 12px;
  justify-content: center;
}

.btn-retry,
.btn-home {
  padding: 10px 24px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
  transition: all 0.3s;
}

.btn-retry {
  background: #1890ff;
  color: white;
}

.btn-retry:hover {
  background: #40a9ff;
}

.btn-home {
  background: #f5f5f5;
  color: #666;
}

.btn-home:hover {
  background: #e8e8e8;
}

details {
  margin-top: 24px;
  text-align: left;
  max-width: 600px;
  margin-left: auto;
  margin-right: auto;
}

pre {
  padding: 12px;
  background: #f5f5f5;
  border-radius: 4px;
  overflow-x: auto;
  font-size: 12px;
}
</style>

三、部署与发布

3.1 独立部署

bash
# 部署脚本示例 deploy.sh

#!/bin/bash

APP_NAME=$1
ENV=$2

if [ -z "$APP_NAME" ] || [ -z "$ENV" ]; then
  echo "用法: ./deploy.sh <app-name> <env>"
  echo "示例: ./deploy.sh app1 production"
  exit 1
fi

echo "开始部署 $APP_NAME 到 $ENV 环境..."

# 1. 构建应用
cd $APP_NAME
npm run build:$ENV

# 2. 上传到 CDN/OSS
echo "上传静态资源..."
aws s3 sync dist/ s3://my-bucket/$APP_NAME/$ENV/ --delete

# 3. 更新配置
echo "更新配置..."
curl -X POST https://api.example.com/update-config \
  -H "Content-Type: application/json" \
  -d "{\"app\": \"$APP_NAME\", \"env\": \"$ENV\", \"version\": \"$(git rev-parse --short HEAD)\"}"

# 4. 清理 CDN 缓存
echo "清理 CDN 缓存..."
aws cloudfront create-invalidation \
  --distribution-id E123456789 \
  --paths "/$APP_NAME/$ENV/*"

echo "部署完成!"

3.2 版本管理策略

javascript
// version-manager.js

class VersionManager {
  constructor() {
    this.versions = new Map()
  }

  // 注册应用版本
  register(appName, version, entry) {
    if (!this.versions.has(appName)) {
      this.versions.set(appName, [])
    }

    this.versions.get(appName).push({
      version,
      entry,
      timestamp: Date.now()
    })
  }

  // 获取最新版本
  getLatest(appName) {
    const versions = this.versions.get(appName)
    if (!versions || versions.length === 0) {
      return null
    }

    return versions[versions.length - 1]
  }

  // 获取指定版本
  getVersion(appName, version) {
    const versions = this.versions.get(appName)
    return versions?.find(v => v.version === version)
  }

  // 回滚到上一版本
  rollback(appName) {
    const versions = this.versions.get(appName)
    if (!versions || versions.length < 2) {
      throw new Error('没有可回滚的版本')
    }

    // 移除最新版本
    versions.pop()

    return versions[versions.length - 1]
  }
}

const versionManager = new VersionManager()

// 注册版本
versionManager.register('app1', 'v1.0.0', 'https://cdn.example.com/app1/v1.0.0/')
versionManager.register('app1', 'v1.0.1', 'https://cdn.example.com/app1/v1.0.1/')

// 获取最新版本
const latest = versionManager.getLatest('app1')

// 回滚
const previous = versionManager.rollback('app1')

3.3 灰度发布

javascript
// 灰度发布策略

class GrayReleaseStrategy {
  constructor() {
    this.rules = []
  }

  // 添加灰度规则
  addRule(rule) {
    this.rules.push(rule)
  }

  // 判断用户是否命中灰度
  shouldUseGrayVersion(userId) {
    for (const rule of this.rules) {
      if (rule.type === 'percentage') {
        // 按百分比灰度
        return this.hashUserId(userId) % 100 < rule.value
      }

      if (rule.type === 'whitelist') {
        // 白名单用户
        return rule.users.includes(userId)
      }

      if (rule.type === 'region') {
        // 按地区灰度
        const userRegion = this.getUserRegion(userId)
        return rule.regions.includes(userRegion)
      }
    }

    return false
  }

  hashUserId(userId) {
    let hash = 0
    for (let i = 0; i < userId.length; i++) {
      hash = ((hash << 5) - hash) + userId.charCodeAt(i)
      hash = hash & hash
    }
    return Math.abs(hash)
  }

  getUserRegion(userId) {
    // 获取用户地区
    return 'cn'
  }
}

// 使用示例
const grayStrategy = new GrayReleaseStrategy()

// 10% 用户使用灰度版本
grayStrategy.addRule({
  type: 'percentage',
  value: 10
})

// 白名单用户
grayStrategy.addRule({
  type: 'whitelist',
  users: ['user1', 'user2', 'user3']
})

// 根据用户判断使用哪个版本
const userId = 'user123'
const version = grayStrategy.shouldUseGrayVersion(userId)
  ? 'v1.1.0-beta'
  : 'v1.0.0'

console.log(`用户 ${userId} 使用版本: ${version}`)

3.4 回滚机制

javascript
// rollback.js

class RollbackManager {
  constructor() {
    this.deployHistory = []
    this.maxHistory = 10
  }

  // 记录部署
  recordDeploy(appName, version, entry) {
    this.deployHistory.unshift({
      appName,
      version,
      entry,
      timestamp: Date.now()
    })

    // 只保留最近 10 次部署
    if (this.deployHistory.length > this.maxHistory) {
      this.deployHistory.pop()
    }

    // 持久化
    this.save()
  }

  // 回滚
  rollback(appName, steps = 1) {
    const deployments = this.deployHistory.filter(d => d.appName === appName)

    if (deployments.length < steps + 1) {
      throw new Error(`没有足够的历史版本进行回滚(需要 ${steps + 1} 个,实际 ${deployments.length} 个)`)
    }

    // 返回目标版本
    return deployments[steps]
  }

  // 获取部署历史
  getHistory(appName) {
    if (appName) {
      return this.deployHistory.filter(d => d.appName === appName)
    }
    return this.deployHistory
  }

  // 保存到 localStorage
  save() {
    localStorage.setItem('deploy-history', JSON.stringify(this.deployHistory))
  }

  // 从 localStorage 加载
  load() {
    const data = localStorage.getItem('deploy-history')
    if (data) {
      this.deployHistory = JSON.parse(data)
    }
  }
}

const rollbackManager = new RollbackManager()
rollbackManager.load()

// 记录部署
rollbackManager.recordDeploy('app1', 'v1.0.1', 'https://cdn.example.com/app1/v1.0.1/')

// 回滚到上一版本
try {
  const targetVersion = rollbackManager.rollback('app1', 1)
  console.log('回滚到版本:', targetVersion)

  // 重新加载应用
  window.location.reload()
} catch (error) {
  console.error('回滚失败:', error.message)
}

四、监控与调试

4.1 子应用加载监控

javascript
// monitor/app-loader.js

class AppLoadMonitor {
  constructor() {
    this.metrics = []
  }

  // 开始监控
  start(appName) {
    const startTime = performance.now()

    return {
      appName,
      startTime,
      end: () => {
        const endTime = performance.now()
        const duration = endTime - startTime

        this.metrics.push({
          appName,
          startTime,
          endTime,
          duration,
          timestamp: Date.now()
        })

        // 上报
        this.report({
          appName,
          duration,
          success: true
        })

        console.log(`[Monitor] ${appName} 加载耗时: ${duration.toFixed(2)}ms`)
      },
      error: (error) => {
        const endTime = performance.now()
        const duration = endTime - startTime

        this.report({
          appName,
          duration,
          success: false,
          error: error.message
        })

        console.error(`[Monitor] ${appName} 加载失败:`, error)
      }
    }
  }

  // 上报数据
  report(data) {
    // 发送到监控服务
    if (window.__MONITOR_API__) {
      window.__MONITOR_API__.send('app-load', data)
    }

    // 或使用自己的上报接口
    fetch('/api/monitor/app-load', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    }).catch(err => {
      console.error('上报失败', err)
    })
  }

  // 获取统计信息
  getStats(appName) {
    const appMetrics = this.metrics.filter(m => m.appName === appName)

    if (appMetrics.length === 0) {
      return null
    }

    const durations = appMetrics.map(m => m.duration)

    return {
      appName,
      count: appMetrics.length,
      avg: durations.reduce((a, b) => a + b, 0) / durations.length,
      min: Math.min(...durations),
      max: Math.max(...durations)
    }
  }
}

const monitor = new AppLoadMonitor()

// 使用示例
registerMicroApps(apps, {
  beforeLoad: [(app) => {
    app.__loadMonitor__ = monitor.start(app.name)
  }],
  afterMount: [(app) => {
    app.__loadMonitor__?.end()
  }],
  errorHandler: (error, app) => {
    app.__loadMonitor__?.error(error)
  }
})

4.2 错误捕获上报

javascript
// monitor/error-handler.js

class ErrorHandler {
  constructor() {
    this.errors = []
    this.init()
  }

  init() {
    // 捕获全局错误
    window.addEventListener('error', (event) => {
      this.captureError({
        type: 'error',
        message: event.message,
        filename: event.filename,
        lineno: event.lineno,
        colno: event.colno,
        stack: event.error?.stack
      })
    })

    // 捕获未处理的 Promise 错误
    window.addEventListener('unhandledrejection', (event) => {
      this.captureError({
        type: 'unhandledrejection',
        message: event.reason?.message || event.reason,
        stack: event.reason?.stack
      })
    })

    // 捕获 Vue 错误
    if (window.app) {
      window.app.config.errorHandler = (err, instance, info) => {
        this.captureError({
          type: 'vue-error',
          message: err.message,
          stack: err.stack,
          component: info
        })
      }
    }
  }

  captureError(error) {
    // 添加额外信息
    const enrichedError = {
      ...error,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now()
    }

    this.errors.push(enrichedError)

    // 上报
    this.report(enrichedError)

    console.error('[ErrorHandler]', enrichedError)
  }

  report(error) {
    // 发送到错误监控服务
    fetch('/api/monitor/error', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(error)
    }).catch(err => {
      console.error('错误上报失败', err)
    })
  }

  // 获取错误列表
  getErrors() {
    return this.errors
  }

  // 清空错误
  clear() {
    this.errors = []
  }
}

const errorHandler = new ErrorHandler()
window.__ERROR_HANDLER__ = errorHandler

4.3 性能监控

javascript
// monitor/performance.js

class PerformanceMonitor {
  constructor() {
    this.metrics = {}
  }

  // 记录性能指标
  mark(name) {
    performance.mark(name)
  }

  // 测量性能
  measure(name, startMark, endMark) {
    try {
      performance.measure(name, startMark, endMark)

      const entries = performance.getEntriesByName(name)
      const latest = entries[entries.length - 1]

      this.metrics[name] = latest.duration

      // 上报
      this.report({
        name,
        duration: latest.duration,
        startTime: latest.startTime
      })

      return latest.duration
    } catch (error) {
      console.error('性能测量失败', error)
      return 0
    }
  }

  // 获取页面性能指标
  getPageMetrics() {
    const navigation = performance.getEntriesByType('navigation')[0]

    if (!navigation) return null

    return {
      // DNS 查询时间
      dns: navigation.domainLookupEnd - navigation.domainLookupStart,
      // TCP 连接时间
      tcp: navigation.connectEnd - navigation.connectStart,
      // 请求时间
      request: navigation.responseStart - navigation.requestStart,
      // 响应时间
      response: navigation.responseEnd - navigation.responseStart,
      // DOM 解析时间
      domParse: navigation.domInteractive - navigation.responseEnd,
      // 资源加载时间
      resourceLoad: navigation.loadEventStart - navigation.domContentLoadedEventEnd,
      // 总时间
      total: navigation.loadEventEnd - navigation.fetchStart
    }
  }

  // 获取资源加载情况
  getResourceMetrics() {
    const resources = performance.getEntriesByType('resource')

    return resources.map(resource => ({
      name: resource.name,
      type: resource.initiatorType,
      duration: resource.duration,
      size: resource.transferSize
    }))
  }

  // 上报
  report(data) {
    fetch('/api/monitor/performance', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    }).catch(err => {
      console.error('性能数据上报失败', err)
    })
  }
}

const performanceMonitor = new PerformanceMonitor()

// 使用示例
performanceMonitor.mark('app-load-start')
// ... 加载应用
performanceMonitor.mark('app-load-end')
const duration = performanceMonitor.measure('app-load', 'app-load-start', 'app-load-end')

console.log('应用加载时间:', duration)

4.4 Chrome DevTools 调试技巧

javascript
// 调试工具

class MicroAppDebugger {
  constructor() {
    this.apps = new Map()
    this.logs = []

    // 挂载到全局
    window.__MICRO_APP_DEBUGGER__ = this
  }

  // 注册应用
  registerApp(name, app) {
    this.apps.set(name, app)
    this.log('info', `应用 ${name} 已注册`)
  }

  // 获取应用信息
  getApp(name) {
    return this.apps.get(name)
  }

  // 列出所有应用
  listApps() {
    console.table(
      Array.from(this.apps.entries()).map(([name, app]) => ({
        name,
        status: app.status,
        entry: app.entry
      }))
    )
  }

  // 打印应用状态
  inspect(name) {
    const app = this.apps.get(name)

    if (!app) {
      console.error(`应用 ${name} 不存在`)
      return
    }

    console.group(`应用 ${name} 详情`)
    console.log('状态:', app.status)
    console.log('入口:', app.entry)
    console.log('容器:', app.container)
    console.log('Props:', app.props)
    console.groupEnd()
  }

  // 日志记录
  log(level, message, data) {
    const log = {
      level,
      message,
      data,
      timestamp: new Date().toISOString()
    }

    this.logs.push(log)

    console[level](`[MicroApp] ${message}`, data || '')
  }

  // 获取日志
  getLogs(level) {
    if (level) {
      return this.logs.filter(log => log.level === level)
    }
    return this.logs
  }

  // 清空日志
  clearLogs() {
    this.logs = []
  }
}

const debugger = new MicroAppDebugger()

// Chrome Console 使用
// __MICRO_APP_DEBUGGER__.listApps()
// __MICRO_APP_DEBUGGER__.inspect('app1')
// __MICRO_APP_DEBUGGER__.getLogs()

五、总结

微前端工程化是保证项目长期成功的关键,核心要点:

  1. 开发规范
    • 统一的生命周期实现
    • 环境判断和导出格式
    • publicPath 配置
  2. 基座设计
    • 统一 Layout 和导航
    • 用户信息和权限管理
    • 错误边界和容错处理
  3. 部署发布
    • 独立部署策略
    • 版本管理和灰度发布
    • 回滚机制
  4. 监控调试
    • 加载性能监控
    • 错误捕获上报
    • 调试工具

建立完善的工程化体系,可以大幅提升开发效率和系统稳定性。