Frontend · #micro-frontend#qiankun#module-federation

微前端架构方案对比与实践

2025.07.02 7 min 2.6k 字
// 目录 · contents

引言

随着前端应用规模的膨胀,单体前端应用面临着构建慢、团队协作困难、技术栈升级阻力大等问题。微前端(Micro Frontend)借鉴了微服务的思想,将大型前端应用拆分为多个独立开发、独立部署的子应用。本文将系统对比 iframe、Web Components、qiankun/single-spa、Module Federation 等主流方案,深入探讨应用间通信、依赖共享和生产实践。

微前端核心概念

graph TB
    A[主应用 / 基座 Container] --> B[子应用 A<br/>React App]
    A --> C[子应用 B<br/>Vue App]
    A --> D[子应用 C<br/>Angular App]

    A --> E[路由分发]
    A --> F[应用注册]
    A --> G[生命周期管理]
    A --> H[应用间通信]

    subgraph "独立开发 & 部署"
        B
        C
        D
    end

    style A fill:#2c3e50,color:#fff
    style B fill:#61dafb,color:#000
    style C fill:#42b883,color:#fff
    style D fill:#dd1b16,color:#fff

微前端的核心原则

  1. 技术栈无关:每个子应用可以使用不同的框架
  2. 独立开发部署:各子应用有独立的代码仓库和 CI/CD
  3. 增量升级:逐步迁移,无需大范围重写
  4. 运行时隔离:子应用之间的样式和 JS 互不干扰

方案一:iframe

最简单的隔离方案——用 iframe 嵌入子应用:

1
2
3
4
5
6
7
8
9
10
11
<!-- Main app -->
<div id="app">
<nav>Main Navigation</nav>
<div id="micro-app-container">
<iframe
src="https://sub-app.example.com/dashboard"
style="width: 100%; height: calc(100vh - 60px); border: none;"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
></iframe>
</div>
</div>
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
// Cross-iframe communication via postMessage
// Parent -> Child
const iframe = document.querySelector('#micro-app-container iframe');
iframe.contentWindow.postMessage({
type: 'USER_INFO',
payload: { userId: '123', token: 'abc' },
}, 'https://sub-app.example.com');

// Child -> Parent
window.parent.postMessage({
type: 'NAVIGATE',
payload: { path: '/settings' },
}, 'https://main-app.example.com');

// Listening for messages with origin validation
window.addEventListener('message', (event) => {
if (event.origin !== 'https://sub-app.example.com') return;
const { type, payload } = event.data;
switch (type) {
case 'NAVIGATE':
router.push(payload.path);
break;
case 'RESIZE':
iframe.style.height = `${payload.height}px`;
break;
}
});
优点 缺点
天然隔离(JS/CSS/DOM) URL 不同步,无法使用浏览器前进后退
实现简单 性能差(额外的浏览器上下文)
安全性好(sandbox) 弹窗无法突破 iframe 边界
SEO 不友好

方案二:Web Components

利用 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Define micro-app as Web Component
class MicroApp extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}

static get observedAttributes() {
return ['app-name', 'base-url'];
}

async connectedCallback() {
const appName = this.getAttribute('app-name');
const baseUrl = this.getAttribute('base-url');

// Load sub-app resources
const manifest = await fetch(`${baseUrl}/asset-manifest.json`).then(r => r.json());

// Create isolated DOM using safe DOM API methods
const style = document.createElement('style');
style.textContent = ':host { display: block; }';
this.shadowRoot.appendChild(style);

const container = document.createElement('div');
container.id = `${appName}-root`;
this.shadowRoot.appendChild(container);

// Load and execute sub-app scripts
const script = document.createElement('script');
script.src = `${baseUrl}/${manifest['main.js']}`;
this.shadowRoot.appendChild(script);
}

disconnectedCallback() {
// Cleanup: unmount sub-app
this.dispatchEvent(new CustomEvent('micro-app-unmount'));
}
}

customElements.define('micro-app', MicroApp);

// Usage in HTML
// <micro-app app-name="dashboard" base-url="https://dashboard.example.com"></micro-app>

方案三:qiankun (基于 single-spa)

qiankun 是蚂蚁金服开源的微前端框架,基于 single-spa 并提供了开箱即用的 JS 沙箱和样式隔离:

sequenceDiagram
    participant M as 主应用
    participant Q as qiankun
    participant S as 子应用

    M->>Q: registerMicroApps(apps)
    M->>Q: start()
    Q->>Q: 监听路由变化
    Q->>S: 加载子应用 entry HTML
    S-->>Q: 返回 HTML/JS/CSS
    Q->>Q: 解析 HTML, 创建 JS 沙箱
    Q->>S: 调用 bootstrap()
    Q->>S: 调用 mount(props)
    S->>S: 渲染到指定容器
    Note over Q,S: 路由切换
    Q->>S: 调用 unmount()
    Q->>S: 加载新子应用...

主应用配置

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
// main-app/src/micro-apps.js
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun';

registerMicroApps([
{
name: 'dashboard',
entry: '//localhost:8081', // Sub-app dev server
container: '#micro-app-container',
activeRule: '/dashboard',
props: {
token: getToken(),
onNavigate: (path) => router.push(path),
},
},
{
name: 'settings',
entry: '//localhost:8082',
container: '#micro-app-container',
activeRule: '/settings',
},
{
name: 'legacy-app',
entry: '//localhost:8083',
container: '#micro-app-container',
activeRule: '/legacy',
sandbox: {
strictStyleIsolation: true, // Shadow DOM isolation
},
},
]);

setDefaultMountApp('/dashboard');
start({
sandbox: {
experimentalStyleIsolation: true,
},
prefetch: 'all', // Prefetch all sub-apps after first app mounted
});

子应用改造(React)

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
// sub-app/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

let root = null;

function getRootContainer(container) {
return container
? container.querySelector('#root')
: document.getElementById('root');
}

// qiankun lifecycle exports
export async function bootstrap() {
console.log('Dashboard app bootstrapped');
}

export async function mount(props) {
const { container, token, onNavigate } = props;
root = ReactDOM.createRoot(getRootContainer(container));
root.render(
<App token={token} onNavigate={onNavigate} />
);
}

export async function unmount(props) {
root.unmount();
root = null;
}

// Standalone mode (for independent development)
if (!window.__POWERED_BY_QIANKUN__) {
root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// sub-app webpack config — important for qiankun
// vue.config.js or webpack.config.js
module.exports = {
output: {
library: 'dashboard', // Must match name in registerMicroApps
libraryTarget: 'umd',
chunkLoadingGlobal: `webpackJsonp_dashboard`,
},
devServer: {
headers: {
'Access-Control-Allow-Origin': '*', // Required for cross-origin loading
},
},
};

方案四:Module Federation (Webpack 5)

Module Federation 允许在运行时动态加载远程模块,实现组件级别的微前端:

graph TB
    A[Host App<br/>webpack.config.js] -->|动态导入| B[Remote App A<br/>exposes: ./Button]
    A -->|动态导入| C[Remote App B<br/>exposes: ./Chart]

    subgraph "Shared Dependencies"
        D[react]
        E[react-dom]
    end

    B --> D
    C --> D
    A --> D

    style A fill:#8dd6f9,color:#000
    style B fill:#f5a623,color:#000
    style C fill:#7ed321,color:#000
    style D fill:#e74c3c,color:#fff

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
26
27
28
29
30
31
// host-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
dashboard: 'dashboard@https://dashboard.example.com/remoteEntry.js',
shared_ui: 'shared_ui@https://ui.example.com/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
};

// Usage in host app
const DashboardPage = React.lazy(() => import('dashboard/DashboardPage'));
const SharedButton = React.lazy(() => import('shared_ui/Button'));

function App() {
return (
<Suspense fallback={<Loading />}>
<SharedButton label="Click me" />
<DashboardPage />
</Suspense>
);
}

Remote 应用配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// dashboard-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'dashboard',
filename: 'remoteEntry.js',
exposes: {
'./DashboardPage': './src/pages/DashboardPage',
'./StatCard': './src/components/StatCard',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
// Shared state management
zustand: { singleton: true },
},
}),
],
};

应用间通信策略

graph TB
    A[应用间通信] --> B[Props 传递]
    A --> C[Custom Events]
    A --> D[共享状态]
    A --> E[URL 参数]

    B --> B1[主应用 → 子应用<br/>mount props]
    C --> C1[CustomEvent API<br/>松耦合通信]
    D --> D1[共享 Store<br/>Redux/Zustand]
    E --> E1[路由参数<br/>Query String]

    style A fill:#2c3e50,color:#fff
    style C fill:#e74c3c,color:#fff
    style D fill:#3498db,color:#fff

Custom Events 通信

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
// Event bus using CustomEvent (framework-agnostic)
class MicroAppEventBus {
emit(eventName, detail) {
window.dispatchEvent(
new CustomEvent(`micro-app:${eventName}`, { detail })
);
}

on(eventName, handler) {
const wrappedHandler = (e) => handler(e.detail);
window.addEventListener(`micro-app:${eventName}`, wrappedHandler);
return () => window.removeEventListener(`micro-app:${eventName}`, wrappedHandler);
}
}

const eventBus = new MicroAppEventBus();

// In sub-app A (sender)
eventBus.emit('user-selected', { userId: '123', name: 'Alice' });

// In sub-app B (receiver)
const unsubscribe = eventBus.on('user-selected', (data) => {
console.log('User selected:', data.name);
loadUserDetails(data.userId);
});

// Cleanup on unmount
export async function unmount() {
unsubscribe();
}

共享状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Shared store using Zustand (works across Module Federation)
// shared-store/src/userStore.js
import { createStore } from 'zustand/vanilla';

export const userStore = createStore((set) => ({
user: null,
token: null,
setUser: (user) => set({ user }),
setToken: (token) => set({ token }),
logout: () => set({ user: null, token: null }),
}));

// In any micro-app
import { userStore } from 'shared_store/userStore';
import { useStore } from 'zustand';

function UserInfo() {
const user = useStore(userStore, (state) => state.user);
return user ? <span>{user.name}</span> : <span>未登录</span>;
}

方案对比总结

特性 iframe Web Components qiankun Module Federation
隔离性 最强 较强 (Shadow DOM) 中等 (JS 沙箱) 弱
性能 差 中 中 好
接入成本 低 中 中 高
通信便捷性 差 中 好 好
技术栈无关 是 是 是 受限 (Webpack)
共享依赖 不能 不能 有限 完善
独立部署 是 是 是 是
粒度 页面级 组件级 页面级 组件级

选型建议

flowchart TD
    A[微前端需求] --> B{需要组件级共享?}
    B -->|是| C{使用 Webpack?}
    C -->|是| D[Module Federation]
    C -->|否 / Vite| E[Vite Federation Plugin]
    B -->|否, 页面级| F{需要强隔离?}
    F -->|是| G{接受 iframe 缺陷?}
    G -->|是| H[iframe]
    G -->|否| I[qiankun + strictStyleIsolation]
    F -->|否| J[qiankun / single-spa]

    style D fill:#8dd6f9,color:#000
    style H fill:#95a5a6,color:#fff
    style I fill:#e74c3c,color:#fff
    style J fill:#e74c3c,color:#fff

总结

微前端架构的选择没有银弹,需要根据团队规模、技术栈差异、隔离需求和性能要求综合权衡。对于大型企业级应用,qiankun 提供了较完善的开箱即用方案;对于同技术栈的模块化拆分,Module Federation 在性能和共享能力上更具优势;而对于需要嵌入第三方应用的场景,iframe 仍然是最安全的选择。无论选择哪种方案,都应优先保证子应用的独立可运行性,避免过度耦合。

作者 · authorzt
发布 · date2025-07-02
篇幅 · length2.6k 字 · 7 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论