📖 文章概述
微前端是将大型前端应用拆分成多个独立、自治的小应用,然后再组合起来的架构模式。本文深入讲解微前端的核心概念、实现方案和实战应用。
🎯 微前端核心概念
什么是微前端?
微前端是一种架构风格,用于在前端应用中独立地交付功能的技术、策略和方法。
传统单体应用 微前端应用
┌─────────────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ │ │ 微应 │ │ 微应 │ │ 微应 │
│ 单一代码库 │ → │ 用 1 │ │ 用 2 │ │ 用 3 │
│ │ └──────┘ └──────┘ └──────┘
│ │ ↓ ↓ ↓
│ 单一部署 │ ┌───────────────────────┐
│ │ │ 容器应用/基座应用 │
└─────────────┘ └───────────────────────┘
微前端的优势与劣势
| 特性 | 优势 | 劣势 |
|---|---|---|
| 独立开发 | 团队自主、技术栈自由 | 重复依赖、包体积增大 |
| 独立部署 | 快速迭代、灰度发布 | 运维复杂度高 |
| 隔离性 | 故障隔离、样式隔离 | 通信成本、调试困难 |
| 可扩展性 | 支持增量开发、动态加载 | 学习曲线陡峭 |
适用场景
✅ 适合:
├─ 大型团队协作项目
├─ 需要快速迭代的应用
├─ 涉及多个技术栈的项目
├─ 需要独立部署的功能模块
└─ 需要灰度更新的关键应用
❌ 不适合:
├─ 小型项目(过度设计)
├─ 对性能有极端要求
├─ 实时性要求很高
└─ 团队规模小(维护成本高)
🚀 微前端实现方案
1. 基于 URL 的方案
// 最简单的微前端方案:根据 URL 路由加载不同应用
// main.js(容器应用)
function loadMicroApp(path) {
const apps = {
'/dashboard': 'https://app1.example.com/dist/index.js',
'/user': 'https://app2.example.com/dist/index.js',
'/settings': 'https://app3.example.com/dist/index.js'
}
const scriptUrl = apps[path]
if (scriptUrl) {
loadScript(scriptUrl)
}
}
function loadScript(src) {
const script = document.createElement('script')
script.src = src
document.body.appendChild(script)
}
// 路由变化时加载对应的微应用
window.addEventListener('hashchange', () => {
const path = window.location.hash.slice(1)
loadMicroApp(path)
})
2. Webpack 5 Module Federation
// webpack.config.js - 容器应用(Host)
module.exports = {
mode: 'development',
entry: './src/index',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js'
},
plugins: [
new ModuleFederationPlugin({
name: 'container',
filename: 'remoteEntry.js',
remotes: {
// 远程应用
app1: 'app1@http://localhost:3001/remoteEntry.js',
app2: 'app2@http://localhost:3002/remoteEntry.js',
app3: 'app3@http://localhost:3003/remoteEntry.js'
},
shared: {
react: { singleton: true, requiredVersion: false },
'react-dom': { singleton: true, requiredVersion: false },
vue: { singleton: true, requiredVersion: false }
}
})
]
}
// webpack.config.js - 微应用(Remote)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App.vue'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
vue: { singleton: true }
}
})
]
}
// 使用远程应用
import React, { Suspense, lazy } from 'react'
const RemoteApp1 = lazy(() => import('app1/App'))
export default function Container() {
return (
<Suspense fallback={<div>Loading...</div>}>
<RemoteApp1 />
</Suspense>
)
}
3. Vite Module Federation(experimental)
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import federation from '@originjs/vite-plugin-federation'
export default defineConfig({
plugins: [
vue(),
federation({
name: 'container',
remotes: {
app1: 'http://localhost:3001/dist/remoteEntry.js',
app2: 'http://localhost:3002/dist/remoteEntry.js'
},
exposes: {
'./App': './src/App.vue'
},
shared: ['vue', 'vue-router']
})
]
})
4. 基于 iframe 的方案
<template>
<div class="micro-container">
<iframe
:src="iframeSrc"
:title="appName"
class="micro-iframe"
@load="handleLoad"
></iframe>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
interface Props {
appUrl: string
appName: string
}
const props = withDefaults(defineProps<Props>(), {})
const iframeSrc = ref(props.appUrl)
const handleLoad = () => {
// iframe 加载完成后的处理
console.log(`${props.appName} 加载完成`)
}
// iframe 通信
const sendMessage = (data: any) => {
const iframe = document.querySelector('iframe') as HTMLIFrameElement
iframe.contentWindow?.postMessage(data, '*')
}
// 监听来自 iframe 的消息
window.addEventListener('message', (event) => {
if (event.origin !== 'http://trusted-site.example.com') return
console.log('收到 iframe 消息:', event.data)
})
</script>
<style scoped>
.micro-container {
width: 100%;
height: 100%;
}
.micro-iframe {
width: 100%;
height: 100%;
border: none;
}
</style>
🔌 qiankun 框架(生产级方案)
5. qiankun 基础设置
// main.ts(容器应用)
import { registerMicroApps, start } from 'qiankun'
// 注册微应用
registerMicroApps([
{
name: '@org/app1',
entry: '//localhost:3001',
container: '#app1-container',
activeRule: '/app1'
},
{
name: '@org/app2',
entry: '//localhost:3002',
container: '#app2-container',
activeRule: '/app2'
},
{
name: '@org/app3',
entry: '//localhost:3003',
container: '#app3-container',
activeRule: '/app3'
}
])
// 全局错误处理
qiankun.onGlobalStateChange((state, prev) => {
console.log('全局状态变化:', state, prev)
})
// 启动应用
start()
// 容器应用的根组件
export default {
template: `
<div id="root">
<nav>
<a href="#/app1">App 1</a>
<a href="#/app2">App 2</a>
<a href="#/app3">App 3</a>
</nav>
<div id="app1-container"></div>
<div id="app2-container"></div>
<div id="app3-container"></div>
</div>
`
}
6. qiankun 微应用接入
// src/main.ts(微应用)
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
let instance: any = null
// 导出必要的生命周期钩子
export async function bootstrap() {
console.log('[vue app] vue app bootstraped')
}
export async function mount(props: any) {
console.log('[vue app] props from container:', props)
instance = createApp(App)
instance.use(router)
instance.mount(props.container || '#app')
}
export async function unmount() {
instance?.unmount()
instance = null
}
// 单独开发时
if (!window.__POWERED_BY_QIANKUN__) {
const app = createApp(App)
app.use(router)
app.mount('#app')
}
7. 全局状态管理
// shared-store.ts(容器应用)
import { initGlobalState } from 'qiankun'
const initialState = {
user: null,
token: '',
theme: 'light'
}
const actions = initGlobalState(initialState)
// 监听全局状态
actions.onGlobalStateChange((state, prev) => {
console.log('全局状态更新:', state)
// 同步状态到本地存储
localStorage.setItem('globalState', JSON.stringify(state))
})
// 设置全局状态
export function setGlobalState(newState: any) {
actions.setGlobalState(newState)
}
export default actions
// 微应用中使用
import { getGlobalState } from 'qiankun'
export async function mount(props: any) {
// 从容器应用获取全局状态
const globalState = props.onGlobalStateChange?.getState?.()
console.log('获取全局状态:', globalState)
// 监听全局状态变化
props.onGlobalStateChange?.((state: any) => {
console.log('全局状态变化:', state)
// 更新应用内的状态
updateAppState(state)
})
// 修改全局状态
props.setGlobalState?.({
user: { id: 1, name: 'John' }
})
}
🛡️ 应用隔离和样式隔离
8. CSS 隔离方案
// 方案 1: CSS Modules
// app1/src/styles/index.module.css
.container {
padding: 20px;
color: #333;
}
// app1/src/App.vue
<style module>
@import './styles/index.module.css';
</style>
// 方案 2: BEM 命名规范
<template>
<div class="app1__container">
<div class="app1__header">Header</div>
<div class="app1__content">Content</div>
</div>
</template>
<style scoped>
.app1__container { /* ... */ }
.app1__header { /* ... */ }
.app1__content { /* ... */ }
</style>
// 方案 3: Shadow DOM(最隔离)
class MicroApp extends HTMLElement {
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
:host {
display: block;
padding: 20px;
}
.container {
color: blue;
}
</style>
<div class="container">
<h1>Isolated App</h1>
</div>
`
}
}
customElements.define('micro-app', MicroApp)
9. JavaScript 隔离(沙箱)
// 简单沙箱实现
class Sandbox {
private proxy: any
private globalContext: any = {}
constructor() {
// 创建代理上下文
this.globalContext = Object.create(null)
// 预设全局对象(白名单)
const whiteList = ['console', 'Math', 'Date', 'JSON']
whiteList.forEach(name => {
this.globalContext[name] = window[name as any]
})
// 使用 Proxy 拦截访问
this.proxy = new Proxy(this.globalContext, {
get: (target, prop) => {
if (prop in target) {
return target[prop]
}
return undefined
},
set: (target, prop, value) => {
target[prop] = value
return true
}
})
}
// 在沙箱中执行代码
execute(code: string) {
try {
const fn = new Function('sandbox', code)
fn(this.proxy)
} catch (error) {
console.error('沙箱执行错误:', error)
}
}
}
// 使用
const sandbox = new Sandbox()
sandbox.execute(`
console.log('运行在沙箱中')
const data = { id: 1 }
// 无法访问 window.location 等危险属性
`)
📡 微应用间通信
10. 事件总线通信
// EventBus.ts(共享库)
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)
// 返回取消订阅函数
return () => {
const callbacks = this.events.get(event)!
const index = callbacks.indexOf(callback)
if (index > -1) callbacks.splice(index, 1)
}
}
// 发送事件
emit(event: string, ...args: any[]) {
const callbacks = this.events.get(event)
if (callbacks) {
callbacks.forEach(cb => cb(...args))
}
}
// 一次性监听
once(event: string, callback: Function) {
const unsubscribe = this.on(event, (...args: any[]) => {
callback(...args)
unsubscribe()
})
}
// 清除事件
off(event: string) {
this.events.delete(event)
}
// 清除所有事件
clear() {
this.events.clear()
}
}
export const eventBus = new EventBus()
// 微应用 1 中发送事件
import { eventBus } from '@shared/event-bus'
export function notifyUserUpdate(user: any) {
eventBus.emit('user-updated', user)
}
// 微应用 2 中监听事件
import { eventBus } from '@shared/event-bus'
onMounted(() => {
eventBus.on('user-updated', (user: any) => {
console.log('用户已更新:', user)
updateUserInfo(user)
})
})
11. 基于 URL 的通信
// 通过 URL 参数传递数据
export function navigateToApp(appName: string, params: any) {
const queryString = new URLSearchParams(params).toString()
window.location.hash = `#/${appName}?${queryString}`
}
// 在微应用中读取参数
import { useRoute } from 'vue-router'
export default {
setup() {
const route = useRoute()
const userId = route.query.userId
const action = route.query.action
return { userId, action }
}
}
🎯 实战:完整微前端应用示例
12. 容器应用完整实现
<!-- Container.vue -->
<template>
<div class="container">
<!-- 导航 -->
<header class="header">
<div class="logo">Micro App Platform</div>
<nav class="nav">
<router-link to="/dashboard">Dashboard</router-link>
<router-link to="/profile">Profile</router-link>
<router-link to="/settings">Settings</router-link>
</nav>
<div class="user-info">{{ globalState.user?.name }}</div>
</header>
<!-- 微应用容器 -->
<main class="main">
<div id="micro-app-container"></div>
</main>
<!-- 全局加载提示 -->
<div v-if="loading" class="loading">
<span>加载中...</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun'
const loading = ref(false)
const globalState = reactive({
user: null,
token: '',
theme: 'light'
})
// 注册微应用
const registerApps = () => {
registerMicroApps([
{
name: '@app/dashboard',
entry: 'http://localhost:3001',
container: '#micro-app-container',
activeRule: '/dashboard',
props: { globalState }
},
{
name: '@app/profile',
entry: 'http://localhost:3002',
container: '#micro-app-container',
activeRule: '/profile',
props: { globalState }
},
{
name: '@app/settings',
entry: 'http://localhost:3003',
container: '#micro-app-container',
activeRule: '/settings',
props: { globalState }
}
], {
beforeLoad: [
async () => {
loading.value = true
}
],
afterMount: [
async () => {
loading.value = false
}
]
})
// 设置默认应用
setDefaultMountApp('@app/dashboard')
}
onMounted(() => {
registerApps()
start()
})
// 导出全局状态更新方法
export function setGlobalState(newState: any) {
Object.assign(globalState, newState)
}
</script>
<style scoped>
.container {
display: flex;
flex-direction: column;
height: 100vh;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #333;
color: white;
}
.nav {
display: flex;
gap: 20px;
}
.nav a {
color: white;
text-decoration: none;
}
.main {
flex: 1;
overflow: auto;
}
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
</style>
📊 性能优化建议
| 优化方案 | 效果 | 实施难度 |
|---|---|---|
| 预加载微应用 | ⭐⭐⭐ | 低 |
| 共享依赖 | ⭐⭐⭐⭐ | 中 |
| 懒加载模块 | ⭐⭐⭐ | 中 |
| CDN 加速 | ⭐⭐⭐ | 低 |
| 增量更新 | ⭐⭐⭐⭐⭐ | 高 |
🐛 常见问题解决
问题 1:样式污染
// ❌ 问题:全局样式相互覆盖
// app1 定义 .title { color: blue }
// app2 定义 .title { color: red }
// ✅ 解决方案:使用 BEM 或 CSS Modules
// app1: .app1__title { color: blue }
// app2: .app2__title { color: red }
// 或使用 Shadow DOM
const shadow = this.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
.title { color: blue } /* 完全隔离 */
</style>
`
问题 2:全局变量冲突
// ❌ 问题:多个应用使用相同的全局变量
window.config = { ... } // app1 设置
window.config = { ... } // app2 覆盖
// ✅ 解决方案:使用命名空间
window.__APP1__ = { config: { ... } }
window.__APP2__ = { config: { ... } }
// 或使用事件总线
eventBus.emit('config-update', newConfig)
问题 3:加载缓慢
// ✅ 优化方案:预加载微应用
const preloadMicroApp = (name: string, entry: string) => {
const script = document.createElement('script')
script.src = `${entry}/remoteEntry.js`
script.async = true
document.head.appendChild(script)
}
// 应用启动时预加载
preloadMicroApp('app1', 'http://localhost:3001')
preloadMicroApp('app2', 'http://localhost:3002')
🎓 最佳实践总结
DO ✅
// 1. 清晰的应用边界
registerMicroApps([
{
name: '@org/app1',
entry: 'http://localhost:3001',
activeRule: '/app1' // 明确的激活规则
}
])
// 2. 共享必要的依赖
shared: {
vue: { singleton: true },
'vue-router': { singleton: true }
}
// 3. 隔离样式和脚本
// 使用 CSS Modules 或 BEM
// 使用沙箱或 Web Components
// 4. 健全的通信机制
eventBus.on('event-name', callback)
// 5. 优雅的错误处理
try {
await loadMicroApp(...)
} catch (error) {
showErrorMessage(error)
}
DON'T ❌
// 1. 不要过度分割应用
// 应该按业务域划分,不要过细
// 2. 不要共享所有依赖
// 会导致版本冲突
// 3. 不要忽视隔离
// 必须实现样式和脚本隔离
// 4. 不要直接操作 DOM
// 应该通过事件总线通信
// 5. 不要让微应用依赖特定顺序加载
// 应该支持独立加载
📚 扩展资源
总结
微前端架构核心要点:
- 正确选型:根据项目规模选择合适方案
- 应用隔离:样式、脚本、上下文的完全隔离
- 有效通信:事件总线、全局状态管理
- 性能优化:预加载、共享依赖、增量更新
- 团队协作:明确的应用边界、规范的通信协议
- 运维成本:监控、日志、灰度部署
掌握微前端,能够构建可扩展、易维护的大型前端应用!