# 微前端架构实践
微前端是一种前端架构模式,它允许多个独立开发和部署的前端应用组合成一个整体提供给用户。本文将基于我在字节跳动的实际项目经验,深入探讨微前端架构的原理、实施策略和最佳实践。
# 为什么需要微前端
在大型组织中,随着业务的增长,前端应用往往会变得越来越庞大和复杂。这带来了一系列挑战:
- 团队协作困难 - 多团队同时开发单体应用导致冲突和协调成本高
- 技术栈限制 - 被锁定在遗留技术栈中,难以采用新技术
- 部署风险高 - 整体应用一起部署,任何问题都可能影响整个系统
- 代码复杂度高 - 单体应用代码量大,难以维护和理解
- 扩展性差 - 难以按需加载功能或实现灵活的业务组合
微前端架构通过将前端应用分解为小型、自包含的应用来解决这些问题。
# 微前端核心原则
成功的微前端架构遵循以下核心原则:
- 团队自治 - 每个微前端由一个团队端到端负责
- 技术栈无关 - 各团队可以选择适合的技术栈
- 隔离性 - 应用之间不共享状态和依赖
- 原生浏览器特性 - 尽可能使用标准平台特性而非自定义API
- 彼此感知 - 微前端之间能够共享必要的上下文和状态
# 微前端实现方式
# 1. 基于路由的微前端集成
最简单的方式是基于路由将不同应用组合在一起:
// 主应用路由配置
const routes = [
// 本地路由
{ path: '/', component: HomePage },
{ path: '/about', component: AboutPage },
// 微前端路由 - 配置重定向到其他应用
{
path: '/app1/*',
beforeEnter() {
window.location.href = 'http://app1.example.com' + window.location.pathname.replace('/app1', '')
}
},
{
path: '/app2/*',
beforeEnter() {
window.location.href = 'http://app2.example.com' + window.location.pathname.replace('/app2', '')
}
}
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
优点:
- 实现简单,无需特殊框架
- 应用间完全隔离
缺点:
- 体验不连贯,每次跳转都是完整页面刷新
- 无法在同一页面组合多个微前端
# 2. iframe集成
使用iframe嵌入不同应用:
<div class="container">
<header>
<!-- 主应用导航 -->
<nav>
<button onclick="loadApp('app1')">应用1</button>
<button onclick="loadApp('app2')">应用2</button>
</nav>
</header>
<main>
<iframe id="micro-frontend-container" src=""></iframe>
</main>
</div>
<script>
function loadApp(appName) {
const container = document.getElementById('micro-frontend-container')
switch(appName) {
case 'app1':
container.src = 'http://app1.example.com'
break
case 'app2':
container.src = 'http://app2.example.com'
break
}
}
// 初始加载
loadApp('app1')
</script>
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
优点:
- 完美的应用隔离
- 简单直接,无需复杂框架
缺点:
- iframe有许多限制(样式隔离、跨iframe通信、性能等)
- 用户体验不佳(历史导航、深度链接等问题)
# 3. Web Components集成
使用Web Components封装微前端:
// 定义微前端组件
class MicroFrontend extends HTMLElement {
connectedCallback() {
const name = this.getAttribute('name')
const host = this.getAttribute('host')
// 加载微前端的脚本
const scriptId = `micro-frontend-script-${name}`
if (document.getElementById(scriptId)) {
this.renderMicroFrontend(name)
return
}
fetch(`${host}/asset-manifest.json`)
.then(res => res.json())
.then(manifest => {
const script = document.createElement('script')
script.id = scriptId
script.src = `${host}${manifest['main.js']}`
script.onload = () => {
this.renderMicroFrontend(name)
}
document.head.appendChild(script)
})
}
renderMicroFrontend(name) {
const mountFunc = window[`render${name}`]
if (mountFunc) {
// 创建挂载点
const mountPoint = document.createElement('div')
this.appendChild(mountPoint)
// 挂载微前端
mountFunc(mountPoint)
// 保存卸载函数
this.unmountFunc = window[`unmount${name}`]
}
}
disconnectedCallback() {
if (this.unmountFunc) {
this.unmountFunc()
}
}
}
// 注册自定义元素
customElements.define('micro-frontend', MicroFrontend)
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
使用示例:
<header>
<!-- 主应用导航 -->
</header>
<main>
<micro-frontend name="App1" host="http://localhost:3001"></micro-frontend>
<micro-frontend name="App2" host="http://localhost:3002"></micro-frontend>
</main>
2
3
4
5
6
7
每个微前端应用需要导出挂载和卸载函数:
// App1的入口文件
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
window.renderApp1 = (containerId) => {
ReactDOM.render(<App />, containerId)
}
window.unmountApp1 = (containerId) => {
if (containerId) {
ReactDOM.unmountComponentAtNode(containerId)
}
}
// 本地开发模式
if (!document.getElementById('micro-frontend-container')) {
ReactDOM.render(<App />, document.getElementById('root'))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
优点:
- 使用原生Web标准
- 良好的隔离性
- 可组合多个微前端在同一页面
缺点:
- 需要微前端导出特定的挂载/卸载函数
- 共享依赖较复杂
# 4. 使用single-spa框架
single-spa (opens new window)是一个专门为微前端设计的JavaScript框架:
// 主应用入口
import { registerApplication, start } from 'single-spa'
// 注册微前端应用
registerApplication({
name: 'app1',
app: () => import('@org/app1'),
activeWhen: (location) => location.pathname.startsWith('/app1')
})
registerApplication({
name: 'app2',
app: () => import('@org/app2'),
activeWhen: ['/app2']
})
registerApplication({
name: 'navbar',
app: () => import('@org/navbar'),
activeWhen: ['/']
})
// 启动single-spa
start()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
每个微前端需要导出特定的生命周期函数:
// React微前端
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { navigateToUrl } from 'single-spa'
let rootElement
// 必须的生命周期函数
export function bootstrap() {
return Promise.resolve()
}
export function mount(props) {
return new Promise((resolve) => {
rootElement = document.createElement('div')
rootElement.id = 'app1-container'
document.body.appendChild(rootElement)
ReactDOM.render(
<App {...props} />,
rootElement,
() => resolve()
)
})
}
export function unmount() {
return new Promise((resolve) => {
ReactDOM.unmountComponentAtNode(rootElement)
document.body.removeChild(rootElement)
resolve()
})
}
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
优点:
- 专门为微前端设计
- 支持多种框架(React, Vue, Angular等)
- 提供丰富的生命周期钩子
缺点:
- 学习曲线较陡
- 配置较复杂
# 5. 使用Module Federation (webpack 5)
Webpack 5引入的Module Federation允许多个构建共享代码和模块:
// webpack.config.js (主应用)
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
name: 'host',
filename: 'remoteEntry.js',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
app2: 'app2@http://localhost:3002/remoteEntry.js'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
}
// webpack.config.js (微前端应用1)
module.exports = {
// ...
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App'
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
}
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
主应用中使用远程模块:
// 主应用
import React, { lazy, Suspense } from 'react'
// 动态导入远程模块
const RemoteApp1 = lazy(() => import('app1/App'))
const RemoteApp2 = lazy(() => import('app2/App'))
function App() {
return (
<div>
<h1>主应用</h1>
<Suspense fallback={<div>加载中...</div>}>
<RemoteApp1 />
</Suspense>
<Suspense fallback={<div>加载中...</div>}>
<RemoteApp2 />
</Suspense>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
优点:
- 原生webpack支持
- 可共享依赖,避免重复加载
- 简化模块共享配置
缺点:
- 仅限webpack 5项目
- 配置较复杂
# 微前端实际案例:字节跳动内部平台
在字节跳动,我们使用微前端架构重构了一个大型的内部数据分析平台。这个平台涉及多个团队,包括数据可视化、报表系统、指标管理等模块。
# 架构设计
我们采用了基于qiankun(基于single-spa的封装)的微前端解决方案:
// 主应用 (基座应用)
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'dashboard',
entry: '//localhost:8081',
container: '#micro-container',
activeRule: '/dashboard',
props: {
shared: sharedState,
eventBus // 传递事件总线给微前端
}
},
{
name: 'reports',
entry: '//localhost:8082',
container: '#micro-container',
activeRule: '/reports',
props: {
shared: sharedState,
eventBus // 传递事件总线给微前端
}
},
{
name: 'metrics',
entry: '//localhost:8083',
container: '#micro-container',
activeRule: '/metrics',
props: {
shared: sharedState,
eventBus // 传递事件总线给微前端
}
}
])
// 创建共享状态
const sharedState = {
user: null,
permissions: [],
selectedDateRange: null,
// ...其他共享状态
}
// 启动微前端
start({
sandbox: true,
singular: true
})
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
# 跨应用通信
我们使用事件总线模式处理微前端之间的通信:
// 主应用中的事件总线
class EventBus {
constructor() {
this.events = {}
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = []
}
this.events[event].push(callback)
}
off(event, callback) {
if (!this.events[event]) return
this.events[event] = this.events[event].filter(cb => cb !== callback)
}
emit(event, data) {
if (!this.events[event]) return
this.events[event].forEach(cb => cb(data))
}
}
// 创建全局事件总线实例
const eventBus = new EventBus()
window.eventBus = eventBus // 暴露给微前端应用使用
// 在主应用中注册共享
registerMicroApps([
{
name: 'dashboard',
// ...
props: {
shared: sharedState,
eventBus // 传递事件总线给微前端
}
},
// 其他微前端应用...
])
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
微前端应用中使用事件总线:
// 微前端内部使用
function setupCommunication() {
// 在全局对象中查找事件总线
const { eventBus } = window.props || {}
if (eventBus) {
// 订阅事件
eventBus.on('dateRangeChanged', (range) => {
console.log('日期范围变更:', range)
// 更新本应用状态
updateDateRange(range)
})
// 组件内发布事件
function handleMetricSelection(metric) {
// 通知其他应用指标已选择
eventBus.emit('metricSelected', metric)
}
return () => {
// 清理订阅
eventBus.off('dateRangeChanged')
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 数据共享与状态同步
除了事件通信外,我们还实现了基于React Context的跨应用状态共享:
// 主应用中创建全局状态管理
import { create } from 'zustand'
const useGlobalStore = create((set) => ({
user: null,
theme: 'light',
dateRange: { start: null, end: null },
setUser: (user) => set({ user }),
setTheme: (theme) => set({ theme }),
setDateRange: (dateRange) => set({ dateRange })
}))
// 将store实例注入微前端
registerMicroApps([
{
name: 'dashboard',
// ...
props: {
getGlobalState: () => useGlobalStore.getState(),
useGlobalStore // 传递store hook
}
}
])
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在React微前端中使用:
// 微前端应用中
import React, { useEffect, useState } from 'react'
function DashboardApp() {
// 从props获取全局状态
const { useGlobalStore } = window.props
// 使用来自主应用的store
const { dateRange, setDateRange } = useGlobalStore(state => ({
dateRange: state.dateRange,
setDateRange: state.setDateRange
}))
// 使用全局状态
useEffect(() => {
console.log('日期范围更新:', dateRange)
// 加载基于日期范围的数据...
}, [dateRange])
// UI渲染...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 性能优化策略
在字节跳动的微前端实践中,我们采用了多种性能优化策略:
# 1. 预加载微前端
import { registerMicroApps, start, prefetchApps } from 'qiankun'
// 注册应用
registerMicroApps([...])
// 启动qiankun
start()
// 预加载其他微应用静态资源
prefetchApps([
{ name: 'reports' },
{ name: 'metrics' }
])
2
3
4
5
6
7
8
9
10
11
12
13
# 2. 共享运行时依赖
我们通过Webpack Module Federation共享公共依赖,减少重复加载:
// 微前端webpack配置
new ModuleFederationPlugin({
name: 'dashboard',
filename: 'remoteEntry.js',
exposes: {
'./App': './src/App'
},
shared: {
react: {
singleton: true,
requiredVersion: '^17.0.0'
},
'react-dom': {
singleton: true,
requiredVersion: '^17.0.0'
},
antd: {
singleton: true,
requiredVersion: '^4.16.0'
},
lodash: {
singleton: true,
requiredVersion: '^4.17.0'
}
}
})
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
# 3. 懒加载微前端
根据用户导航行为动态加载微前端应用:
// 主应用路由配置
const routes = [
{
path: '/',
component: Home
},
{
path: '/dashboard/*',
component: MicroAppLoader,
props: { name: 'dashboard' }
},
{
path: '/reports/*',
component: MicroAppLoader,
props: { name: 'reports' }
}
]
// 微前端加载器组件
function MicroAppLoader({ name }) {
const containerRef = useRef(null)
useEffect(() => {
// 动态加载微前端
loadMicroApp({
name,
entry: microApps[name].entry,
container: containerRef.current,
props: { /* ... */ }
})
}, [name])
return <div ref={containerRef}></div>
}
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
# 遇到的挑战与解决方案
在实施微前端架构过程中,我们遇到了一些挑战:
# 1. 样式隔离
CSS冲突是微前端中的常见问题。我们采用了Shadow DOM和CSS模块化方案:
// qiankun配置
start({
sandbox: {
strictStyleIsolation: true, // 使用Shadow DOM隔离样式
experimentalStyleIsolation: true // 或使用运行时样式转换
}
})
2
3
4
5
6
7
对于不支持Shadow DOM的场景,我们使用CSS命名空间:
/* 微前端中的CSS */
.app-dashboard .button {
/* 应用特定样式 */
}
/* 或使用CSS Modules */
.button {
/* 编译后会添加唯一哈希 */
}
2
3
4
5
6
7
8
9
# 2. 跨应用导航
为了保持一致的导航体验,我们实现了统一的路由管理:
// 主应用中提供的路由服务
const routerService = {
navigateTo: (path, params = {}) => {
const url = new URL(path, window.location.origin)
// 添加查询参数
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value)
})
// 使用主应用路由导航
history.pushState(null, '', url.toString())
// 触发路由变化事件
window.dispatchEvent(new PopStateEvent('popstate'))
}
}
// 通过props传递给微前端
registerMicroApps([
{
name: 'dashboard',
// ...
props: {
routerService
}
}
])
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
# 3. 权限管理
跨应用的统一权限管理是另一个挑战:
// 主应用中的权限服务
const permissionService = {
// 检查当前用户是否有特定权限
hasPermission: (permissionKey) => {
const { user } = useGlobalStore.getState()
return user?.permissions?.includes(permissionKey) || false
},
// 获取当前用户所有权限
getUserPermissions: () => {
const { user } = useGlobalStore.getState()
return user?.permissions || []
}
}
// 在微前端中使用
function ProtectedFeature({ permissionKey, children }) {
const { permissionService } = window.props
if (!permissionService || permissionService.hasPermission(permissionKey)) {
return children
}
return <AccessDenied />
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 微前端架构的利弊分析
基于在字节跳动的实践经验,微前端架构的主要优缺点如下:
# 优势
- 团队自主性 - 不同团队可以独立开发、测试和部署
- 技术栈灵活性 - 允许不同应用使用不同技术栈
- 增量升级 - 可以逐步现代化遗留系统
- 独立部署 - 降低部署风险和依赖
- 按需加载 - 提高首屏加载性能
# 挑战
- 初始搭建复杂 - 需要额外的架构设计和基础设施
- 调试难度增加 - 分布式架构使调试更复杂
- 一致性维护 - 需要额外工作保持用户体验一致性
- 性能开销 - 可能引入额外的运行时开销
- 学习曲线 - 团队需要学习新的架构模式
# 最佳实践
基于我们在字节跳动的项目经验,总结以下微前端最佳实践:
- 渐进式采用 - 从一个微前端开始,逐步扩展
- 设计清晰的边界 - 微前端之间应有明确的职责划分
- 构建共享组件库 - 确保视觉和交互一致性
- 统一认证和授权 - 实现无缝的用户体验
- 自动化部署流程 - 每个微前端应有独立的CI/CD流程
- 监控和性能追踪 - 实施跨应用的监控系统
- 文档和示例 - 为开发团队提供清晰的指导
# 何时选择微前端
微前端并非适用于所有场景。以下情况可能适合采用微前端:
- 大型应用需要由多团队并行开发
- 需要逐步现代化遗留系统
- 需要组合来自不同来源的功能
- 应用太大,需要拆分以提高可维护性
不建议在以下情况使用微前端:
- 小型应用或团队规模小
- 应用较简单且不需要频繁更新
- 技术栈统一且没有遗留系统集成需求
# 结语
微前端架构为大型前端应用开发提供了一种模块化方法,特别适合多团队协作的场景。在字节跳动,我们通过微前端成功地将一个庞大的单体应用拆分为多个独立可维护的系统,大大提高了开发效率和系统稳定性。
然而,微前端也带来了额外的复杂性和挑战。在决定采用此架构前,团队应仔细评估项目需求和团队能力,选择最适合的实现方案。