微前端架构2.0:模块联邦与云前端实践
深入探讨微前端架构的最新实践,包括Module Federation、云前端部署、独立发布等核心技术
深入探讨微前端架构的最新实践,包括Module Federation、云前端部署、独立发布等核心技术
引言 随着前端应用规模的增长,单体应用面临着维护困难、部署低效、技术栈固化等问题。微前端架构通过将应用拆分为多个独立的子应用,实现了应用级别的模块化。本文将全面介绍微前端架构的设计与实现。 一、微前端基础概念 1.1 什么是微前端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 单体应用 vs 微前端 // 单体应用 const MonolithicApp = () => { return ( <div> <Header /> {/* 用户团队维护 */} <Dashboard /> {/* 数据团队维护 */} <Settings /> {/* 配置团队维护 */} <Billing /> {/* 财务团队维护 */} </div> ) } // 微前端架构 const MicroFrontendApp = () => { return ( <div> <MicroApp name="header" /> {/* 独立部署 */} <MicroApp name="dashboard" /> {/* 独立部署 */} <MicroApp name="settings" /> {/* 独立部署 */} <MicroApp name="billing" /> {/* 独立部署 */} </div> ) } 1.2 微前端核心价值 优势 说明 收益 技术栈无关 各子应用可选择不同技术 团队自治、技术灵活性 独立开发部署 子应用独立版本控制 提高发布频率、降低风险 增量升级 逐步迁移旧系统 降低迁移成本 团队自治 独立开发测试部署 提高团队效率 1.3 微前端技术选型对比 方案 实现复杂度 样式隔离 JS隔离 适用场景 qiankun 中 支持 支持 Vue/React/Angular混用 single-spa 高 手动处理 手动处理 高度定制需求 Module Federation 低 需配置 需配置 Webpack 5项目 iframe 低 天然隔离 天然隔离 快速隔离、跨域限制 二、qiankun微前端实战 2.1 主应用配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 // 主应用入口 main.ts import { registerMicroApps, start, initGlobalState } from 'qiankun' // 1. 注册子应用 const apps = [ { name: 'dashboard', entry: '//localhost:7101', container: '#subapp-container', activeRule: '/dashboard', props: { routerBase: '/dashboard', getGlobalState: () => store.getState() } }, { name: 'settings', entry: '//localhost:7102', container: '#subapp-container', activeRule: '/settings', props: { routerBase: '/settings' } }, { name: 'billing', entry: '//localhost:7103', container: '#subapp-container', activeRule: '/billing' } ] // 2. 配置全局状态 const initialState = { user: null, token: null, theme: 'light' } const actions = initGlobalState(initialState) // 3. 监听状态变化 actions.onGlobalStateChange((state, prev) => { console.log('Global state changed:', state, prev) }) // 4. 启动qiankun registerMicroApps(apps, { beforeLoad: [ app => { console.log('Loading app:', app.name) return Promise.resolve() } ], beforeMount: [ app => { console.log('Mounting app:', app.name) return Promise.resolve() } ], afterMount: [ app => { console.log('Mounted app:', app.name) return Promise.resolve() } ], beforeUnmount: [ app => { console.log('Unmounting app:', app.name) return Promise.resolve() } ], afterUnmount: [ app => { console.log('Unmounted app:', app.name) return Promise.resolve() } ] }) start({ sandbox: { strictStyleIsolation: true, // 严格样式隔离 experimentalStyleIsolation: true // 实验性样式隔离 }, prefetch: true, // 预加载子应用资源 singular: false, // 是否单实例 fetch: window.fetch // 自定义fetch方法 }) 2.2 子应用配置(Vue 3) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 // 子应用入口 main.ts import { createApp } from 'vue' import { createRouter, createWebHistory } from 'vue-router' import App from './App.vue' let app: any let router: any let history: any // 渲染函数 function render(props: any = {}) { const { container } = props history = createWebHistory(props.routerBase || '/') router = createRouter({ history, routes: [ { path: '/', component: () => import('./views/Home.vue') }, { path: '/about', component: () => import('./views/About.vue') } ] }) app = createApp(App) app.use(router) app.mount(container ? container.querySelector('#app') : '#app') } // 独立运行 if (!(window as any).__POWERED_BY_QIANKUN__) { render() } // qiankun生命周期 export async function bootstrap() { console.log('Dashboard app bootstrapped') } export async function mount(props: any) { console.log('Dashboard app mounted with props:', props) render(props) // 接收主应用传递的状态 props.onGlobalStateChange && props.onGlobalStateChange((state: any) => { console.log('Global state:', state) }, true) } export async function unmount() { app?.unmount() history?.destroy() app = null router = null history = null } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // vue.config.js const { name } = require('./package.json') module.exports = { devServer: { port: 7101, headers: { 'Access-Control-Allow-Origin': '*' // 允许跨域 } }, configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd', globalObject: 'window' } } } 2.3 应用间通信 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 // 1. 基于props通信(推荐) // 主应用 registerMicroApps([ { name: 'dashboard', entry: '//localhost:7101', container: '#subapp-container', activeRule: '/dashboard', props: { // 传递方法 onNavigate: (to: string) => { console.log('Navigate to:', to) }, // 传递数据 sharedData: { apiEndpoint: 'https://api.example.com' }, // 传递状态管理 getGlobalState: () => store.getState() } } ]) // 子应用接收 export async function mount(props: any) { const { onNavigate, sharedData, getGlobalState } = props // 使用传递的方法 onNavigate('/settings') // 访问共享数据 console.log('API endpoint:', sharedData.apiEndpoint) // 访问全局状态 const globalState = getGlobalState() } // 2. 全局状态管理 // 主应用 import { initGlobalState } from 'qiankun' const initialState = { user: null, theme: 'light', locale: 'zh-CN' } const actions = initGlobalState(initialState) // 主应用更新状态 actions.setGlobalState({ user: { id: 1, name: 'Alice' } }) // 子应用监听状态 export async function mount(props: any) { props.onGlobalStateChange((state: any, prev: any) => { console.log('State changed from', prev, 'to', state) // 响应状态变化 if (state.theme !== prev.theme) { updateTheme(state.theme) } }, true) } // 3. 自定义事件总线 // 创建事件总线 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) } emit(event: string, ...args: any[]) { const callbacks = this.events.get(event) || [] callbacks.forEach(callback => callback(...args)) } off(event: string, callback: Function) { const callbacks = this.events.get(event) || [] const index = callbacks.indexOf(callback) if (index > -1) { callbacks.splice(index, 1) } } } // 挂载到window window.__EVENT_BUS__ = new EventBus() // 使用 window.__EVENT_BUS__.on('user-login', (user) => { console.log('User logged in:', user) }) window.__EVENT_BUS__.emit('user-login', { id: 1, name: 'Alice' }) 三、Module Federation实战 3.1 Webpack配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // 主应用 webpack.config.js const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin') module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'host', remotes: { dashboard: 'dashboard@http://localhost:7101/remoteEntry.js', settings: 'settings@http://localhost:7102/remoteEntry.js' }, shared: { react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^18.0.0' }, vue: { singleton: true, requiredVersion: '^3.0.0' } } }) ] } // 子应用 webpack.config.js module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'dashboard', filename: 'remoteEntry.js', exposes: { './Dashboard': './src/Dashboard.vue', './router': './src/router' }, shared: { vue: { singleton: true, requiredVersion: '^3.0.0' }, 'vue-router': { singleton: true } } }) ] } 3.2 动态加载远程模块 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // 主应用中动态加载 import { defineAsyncComponent } from 'vue' // 方式1:使用defineAsyncComponent const Dashboard = defineAsyncComponent(() => import('dashboard/Dashboard') ) // 方式2:手动加载 async function loadRemoteModule() { const module = await import('dashboard/Dashboard') return module.default } // 方式3:错误边界 const SafeDashboard = defineAsyncComponent({ loader: () => import('dashboard/Dashboard'), loadingComponent: LoadingSpinner, errorComponent: ErrorDisplay, delay: 200, timeout: 5000 }) // 使用 <template> <Dashboard v-if="loaded" /> <Loading v-else /> </template> 四、样式隔离方案 4.1 CSS Modules 1 2 3 4 5 6 7 8 9 10 /* Dashboard.module.css */ .container { padding: 20px; background: #f0f0f0; } .title { font-size: 24px; color: #333; } 1 2 3 4 5 6 7 8 9 10 <!-- Dashboard.vue --> <template> <div :class="$style.container"> <h1 :class="$style.title">Dashboard</h1> </div> </template> <script setup> import styles from './Dashboard.module.css' </script> 4.2 Shadow DOM 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // 使用Shadow DOM实现完全隔离 class ShadowComponent extends HTMLElement { constructor() { super() // 创建Shadow DOM const shadow = this.attachShadow({ mode: 'open' }) // 添加样式 const style = document.createElement('style') style.textContent = ` .container { padding: 20px; background: #f0f0f0; } ` shadow.appendChild(style) // 添加内容 const container = document.createElement('div') container.className = 'container' container.innerHTML = '<h1>Shadow DOM Content</h1>' shadow.appendChild(container) } } customElements.define('shadow-component', ShadowComponent) 4.3 动态CSS隔离 1 2 3 4 5 6 7 8 9 10 11 12 13 // qiankun的样式隔离 start({ sandbox: { strictStyleIsolation: true // 为每个子应用创建Shadow DOM } }) // 或使用实验性CSS作用域 start({ sandbox: { experimentalStyleIsolation: true // 添加作用域选择器 } }) 五、共享依赖管理 5.1 依赖共享配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // webpack.config.js new ModuleFederationPlugin({ shared: { // 单例模式共享 react: { singleton: true, // 只加载一个版本 requiredVersion: '^18.0.0', eager: true // 初始时加载 }, // 版本控制 'lodash': { singleton: false, // 允许多版本 version: '^4.17.0' }, // 自定义共享 'shared-utils': { import: './src/utils', shareScope: 'default' } } }) 5.2 公共组件库 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // 创建共享组件库 // shared-components/src/index.ts export { Button } from './Button' export { Input } from './Input' export { Modal } from './Modal' // 主应用暴露共享组件 // webpack.config.js new ModuleFederationPlugin({ name: 'host', exposes: { './shared': './shared-components/src' } }) // 子应用使用共享组件 // webpack.config.js new ModuleFederationPlugin({ name: 'dashboard', remotes: { host: 'host@http://localhost:3000/remoteEntry.js' }, shared: { vue: { singleton: true } } }) // 子应用中使用 import { Button, Input } from 'host/shared' 六、部署策略 6.1 独立部署 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 主应用部署配置 # .github/workflows/deploy-host.yml name: Deploy Host App on: push: branches: [main] paths: - 'packages/host/**' jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build run: | cd packages/host npm install npm run build - name: Deploy to CDN run: | aws s3 sync packages/host/dist s3://cdn.example.com/host 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 子应用部署配置 # .github/workflows/deploy-dashboard.yml name: Deploy Dashboard App on: push: branches: [main] paths: - 'packages/dashboard/**' jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build run: | cd packages/dashboard npm install npm run build - name: Deploy to CDN run: | aws s3 sync packages/dashboard/dist s3://cdn.example.com/dashboard 6.2 版本管理策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // 应用版本管理 const appVersions = { host: '1.2.0', dashboard: '2.1.0', settings: '1.5.0', billing: '1.0.0' } // A/B测试部署 const abTestConfig = { '/dashboard': { control: '//cdn.example.com/dashboard/2.1.0/remoteEntry.js', experiment: '//cdn.example.com/dashboard/2.2.0/remoteEntry.js', traffic: 0.1 // 10%流量到新版本 } } function getAppEntry(appName: string): string { const config = abTestConfig[`/${appName}`] if (!config) return `//cdn.example.com/${appName}/latest/remoteEntry.js` const traffic = Math.random() return traffic < config.traffic ? config.experiment : config.control } 七、监控与调试 7.1 性能监控 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 // 微前端性能监控 class MicroFrontendMonitor { trackAppLoad(appName: string) { const startTime = performance.now() return { end: () => { const duration = performance.now() - startTime this.sendMetrics({ app: appName, type: 'load', duration }) } } } trackAppError(appName: string, error: Error) { this.sendMetrics({ app: appName, type: 'error', message: error.message, stack: error.stack }) } private sendMetrics(data: any) { fetch('/api/metrics', { method: 'POST', body: JSON.stringify(data) }) } } // 使用qiankun生命周期 const monitor = new MicroFrontendMonitor() registerMicroApps(apps, { beforeMount: [app => monitor.trackAppLoad(app.name)], error: [app => monitor.trackAppError(app.name, app.error)] }) 7.2 错误边界 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <!-- ErrorBoundary.vue --> <template> <div v-if="error" class="error-boundary"> <h2>Something went wrong</h2> <p>{{ error.message }}</p> <button @click="retry">Retry</button> </div> <slot v-else /> </template> <script setup lang="ts"> import { ref, onErrorCaptured } from 'vue' const error = ref<Error | null>(null) onErrorCaptured((err: Error) => { error.value = err // 上报错误 console.error('Micro frontend error:', err) return false // 阻止错误继续传播 }) function retry() { error.value = null } </script> 总结 微前端架构实施要点: ...