第11章:微前端与部署
掌握微前端架构设计,实现 CI/CD 自动化部署,建立完整的监控体系
📚 本章目标
通过本章学习,你将掌握:
- Module Federation 微前端架构
- qiankun/wujie 微前端方案
- CI/CD 自动化部署流程
- 性能监控与错误追踪
- 生产环境优化策略
🏗️ 11.1 微前端架构设计
11.1.1 微前端核心概念
微前端是一种将前端应用拆分为多个独立、可部署的小型应用的架构模式。
// 微前端架构图
interface MicroFrontendArchitecture {
// 主应用 (Shell App)
shellApp: {
name: 'shell'
responsibilities: ['路由管理', '应用注册', '共享依赖']
}
// 子应用 (Micro Apps)
microApps: Array<{
name: string
entry: string
container: string
activeRule: string
props?: Record<string, any>
}>
// 共享依赖
sharedDependencies: string[]
// 通信机制
communication: {
props: '父子通信'
customEvents: '自定义事件'
globalState: '全局状态'
postMessage: '跨域通信'
}
}
11.1.2 技术选型对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Module Federation | 原生支持、性能好 | 学习成本高 | 新项目、技术栈统一 |
qiankun | 成熟稳定、生态好 | 侵入性强 | 老项目改造、多技术栈 |
wujie | 无侵入、性能好 | 相对较新 | 对性能要求高的项目 |
single-spa | 灵活度高 | 配置复杂 | 复杂业务场景 |
⚡ 11.2 Module Federation 实战
11.2.1 主应用配置
// webpack.config.js (主应用)
const ModuleFederationPlugin = require('@module-federation/webpack')
module.exports = {
mode: 'development',
devServer: {
port: 3000,
headers: {
'Access-Control-Allow-Origin': '*'
}
},
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
'userApp': 'userApp@http://localhost:3001/remoteEntry.js',
'productApp': 'productApp@http://localhost:3002/remoteEntry.js',
'orderApp': 'orderApp@http://localhost:3003/remoteEntry.js'
},
shared: {
vue: {
singleton: true,
requiredVersion: '^3.0.0'
},
'vue-router': {
singleton: true,
requiredVersion: '^4.0.0'
},
pinia: {
singleton: true,
requiredVersion: '^2.0.0'
}
}
})
]
}
11.2.2 子应用配置
// webpack.config.js (用户管理子应用)
const ModuleFederationPlugin = require('@module-federation/webpack')
module.exports = {
mode: 'development',
devServer: {
port: 3001,
headers: {
'Access-Control-Allow-Origin': '*'
}
},
plugins: [
new ModuleFederationPlugin({
name: 'userApp',
filename: 'remoteEntry.js',
exposes: {
'./UserList': './src/components/UserList.vue',
'./UserForm': './src/components/UserForm.vue',
'./UserStore': './src/stores/userStore.ts'
},
shared: {
vue: {
singleton: true,
requiredVersion: '^3.0.0'
},
'vue-router': {
singleton: true,
requiredVersion: '^4.0.0'
},
pinia: {
singleton: true,
requiredVersion: '^2.0.0'
}
}
})
]
}
11.2.3 动态加载子应用
<!-- 主应用 - 动态加载子应用 -->
<template>
<div class="shell-app">
<nav class="main-nav">
<router-link to="/users">用户管理</router-link>
<router-link to="/products">商品管理</router-link>
<router-link to="/orders">订单管理</router-link>
</nav>
<main class="main-content">
<router-view />
</main>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
// 动态导入子应用组件
const UserList = defineAsyncComponent(() => import('userApp/UserList'))
const UserForm = defineAsyncComponent(() => import('userApp/UserForm'))
const ProductList = defineAsyncComponent(() => import('productApp/ProductList'))
const OrderList = defineAsyncComponent(() => import('orderApp/OrderList'))
// 路由配置
const routes = [
{
path: '/users',
component: UserList
},
{
path: '/users/new',
component: UserForm
},
{
path: '/products',
component: ProductList
},
{
path: '/orders',
component: OrderList
}
]
</script>
11.2.4 子应用间通信
// 共享状态管理
// shared/stores/globalStore.ts
import { defineStore } from 'pinia'
export const useGlobalStore = defineStore('global', {
state: () => ({
user: null as User | null,
theme: 'light' as 'light' | 'dark',
notifications: [] as Notification[]
}),
actions: {
setUser(user: User) {
this.user = user
// 通知其他应用用户信息变更
window.dispatchEvent(new CustomEvent('user-changed', { detail: user }))
},
setTheme(theme: 'light' | 'dark') {
this.theme = theme
document.documentElement.setAttribute('data-theme', theme)
},
addNotification(notification: Notification) {
this.notifications.push(notification)
}
}
})
// 事件总线
class EventBus {
private events: Map<string, Function[]> = new Map()
on(event: string, callback: Function) {
if (!this.events.has(event)) {
this.events.set(event, [])
}
this.events.get(event)!.push(callback)
}
off(event: string, callback: Function) {
const callbacks = this.events.get(event)
if (callbacks) {
const index = callbacks.indexOf(callback)
if (index > -1) {
callbacks.splice(index, 1)
}
}
}
emit(event: string, data?: any) {
const callbacks = this.events.get(event)
if (callbacks) {
callbacks.forEach(callback => callback(data))
}
}
}
export const eventBus = new EventBus()
🚀 11.3 qiankun 微前端方案
11.3.1 主应用配置
// main.ts (主应用)
import { registerMicroApps, start } from 'qiankun'
// 注册微应用
registerMicroApps([
{
name: 'userApp',
entry: '//localhost:3001',
container: '#user-container',
activeRule: '/users',
props: {
data: { theme: 'light' }
}
},
{
name: 'productApp',
entry: '//localhost:3002',
container: '#product-container',
activeRule: '/products'
},
{
name: 'orderApp',
entry: '//localhost:3003',
container: '#order-container',
activeRule: '/orders'
}
])
// 启动微前端
start({
sandbox: {
strictStyleIsolation: true,
experimentalStyleIsolation: true
},
prefetch: 'all',
singular: false
})
11.3.2 子应用配置
// main.ts (子应用)
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
let app: any = null
function render(props: any = {}) {
const { container } = props
app = createApp(App)
app.use(router)
app.use(createPinia())
app.mount(container ? container.querySelector('#app') : '#app')
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
// 微前端生命周期
export async function bootstrap() {
console.log('子应用启动')
}
export async function mount(props: any) {
console.log('子应用挂载', props)
render(props)
}
export async function unmount() {
console.log('子应用卸载')
if (app) {
app.unmount()
app = null
}
}
11.3.3 样式隔离
/* 主应用样式 */
.shell-app {
font-family: 'Arial', sans-serif;
}
/* 子应用样式隔离 */
#user-container {
/* 使用 CSS 变量实现主题共享 */
--primary-color: #1890ff;
--text-color: #333;
}
/* 全局样式重置 */
#user-container * {
box-sizing: border-box;
}
🔄 11.4 CI/CD 自动化部署
11.4.1 GitHub Actions 配置
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm test:coverage
- name: Run E2E tests
run: pnpm test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build application
run: pnpm build
env:
NODE_ENV: production
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build-files
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-files
path: dist/
- name: Deploy to Vercel
uses: amondnet/vercel-action@v20
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.ORG_ID }}
vercel-project-id: ${{ secrets.PROJECT_ID }}
working-directory: ./
11.4.2 Docker 配置
# Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# 复制依赖文件
COPY package*.json pnpm-lock.yaml ./
# 安装依赖
RUN npm install -g pnpm
RUN pnpm install --frozen-lockfile
# 复制源代码
COPY . .
# 构建应用
RUN pnpm build
# 生产环境
FROM nginx:alpine
# 复制构建文件
COPY /app/dist /usr/share/nginx/html
# 复制 nginx 配置
COPY nginx.conf /etc/nginx/nginx.conf
# 暴露端口
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# 缓存配置
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA 路由支持
location / {
try_files $uri $uri/ /index.html;
}
# API 代理
location /api/ {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
11.4.3 环境配置
// config/environments.ts
interface Environment {
apiBaseUrl: string
cdnUrl: string
sentryDsn: string
analyticsId: string
}
const environments: Record<string, Environment> = {
development: {
apiBaseUrl: 'http://localhost:3000/api',
cdnUrl: 'http://localhost:3000',
sentryDsn: '',
analyticsId: ''
},
staging: {
apiBaseUrl: 'https://api-staging.example.com',
cdnUrl: 'https://cdn-staging.example.com',
sentryDsn: 'https://staging-sentry.example.com',
analyticsId: 'GA-STAGING-ID'
},
production: {
apiBaseUrl: 'https://api.example.com',
cdnUrl: 'https://cdn.example.com',
sentryDsn: 'https://sentry.example.com',
analyticsId: 'GA-PRODUCTION-ID'
}
}
export const getEnvironment = (): Environment => {
const env = process.env.NODE_ENV || 'development'
return environments[env]
}
📊 11.5 性能监控与错误追踪
11.5.1 Sentry 错误监控
// plugins/sentry.client.ts
import * as Sentry from '@sentry/vue'
import { BrowserTracing } from '@sentry/tracing'
export default defineNuxtPlugin(() => {
Sentry.init({
app: nuxtApp.vueApp,
dsn: 'YOUR_SENTRY_DSN',
integrations: [
new BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(router)
})
],
tracesSampleRate: 1.0,
environment: process.env.NODE_ENV
})
})
11.5.2 Web Vitals 监控
// composables/useWebVitals.ts
export const useWebVitals = () => {
const reportVital = (name: string, value: number, id: string) => {
// 发送到分析服务
if (typeof window !== 'undefined' && 'gtag' in window) {
gtag('event', name, {
event_category: 'Web Vitals',
value: Math.round(name === 'CLS' ? value * 1000 : value),
event_label: id,
non_interaction: true
})
}
}
const measureWebVitals = () => {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(reportVital)
getFID(reportVital)
getFCP(reportVital)
getLCP(reportVital)
getTTFB(reportVital)
})
}
return { measureWebVitals }
}
11.5.3 自定义性能监控
// utils/performance.ts
class PerformanceMonitor {
private metrics: Map<string, number> = new Map()
// 开始计时
startTiming(name: string) {
this.metrics.set(name, performance.now())
}
// 结束计时
endTiming(name: string) {
const startTime = this.metrics.get(name)
if (startTime) {
const duration = performance.now() - startTime
this.reportMetric(name, duration)
this.metrics.delete(name)
}
}
// 报告指标
private reportMetric(name: string, value: number) {
// 发送到监控服务
fetch('/api/metrics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
value,
timestamp: Date.now(),
url: window.location.href
})
}).catch(console.error)
}
// 监控资源加载
monitorResourceLoading() {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'resource') {
this.reportMetric(`resource.${entry.name}`, entry.duration)
}
})
})
observer.observe({ entryTypes: ['resource'] })
}
}
export const performanceMonitor = new PerformanceMonitor()
🎯 11.6 生产环境优化
11.6.1 代码分割优化
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// 第三方库
vendor: ['vue', 'vue-router', 'pinia'],
// UI 组件库
ui: ['element-plus', '@element-plus/icons-vue'],
// 工具库
utils: ['lodash-es', 'dayjs', 'axios'],
// 图表库
charts: ['echarts', 'chart.js']
}
}
}
}
})
11.6.2 缓存策略
// utils/cache.ts
class CacheManager {
private cache = new Map<string, { data: any; expiry: number }>()
set(key: string, data: any, ttl: number = 300000) { // 5分钟默认
this.cache.set(key, {
data,
expiry: Date.now() + ttl
})
}
get(key: string) {
const item = this.cache.get(key)
if (!item) return null
if (Date.now() > item.expiry) {
this.cache.delete(key)
return null
}
return item.data
}
clear() {
this.cache.clear()
}
}
export const cacheManager = new CacheManager()
11.6.3 预加载策略
// utils/preload.ts
export const preloadResources = () => {
// 预加载关键资源
const criticalResources = [
'/fonts/main.woff2',
'/images/hero.jpg',
'/api/user/profile'
]
criticalResources.forEach(resource => {
if (resource.endsWith('.woff2')) {
const link = document.createElement('link')
link.rel = 'preload'
link.href = resource
link.as = 'font'
link.type = 'font/woff2'
link.crossOrigin = 'anonymous'
document.head.appendChild(link)
} else if (resource.startsWith('/api/')) {
fetch(resource, { method: 'HEAD' })
} else {
const link = document.createElement('link')
link.rel = 'preload'
link.href = resource
link.as = 'image'
document.head.appendChild(link)
}
})
}
🎯 本章总结
通过本章学习,你掌握了:
- 微前端架构:Module Federation、qiankun 方案对比
- Module Federation:主应用配置、子应用开发、通信机制
- qiankun 方案:应用注册、生命周期、样式隔离
- CI/CD 流程:GitHub Actions、Docker 部署、环境配置
- 性能监控:Sentry 集成、Web Vitals、自定义监控
- 生产优化:代码分割、缓存策略、预加载
📝 练习题
- 使用 Module Federation 搭建一个微前端应用
- 实现子应用间的状态共享和通信机制
- 配置完整的 CI/CD 部署流程
- 集成错误监控和性能监控系统
- 优化生产环境的加载性能