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" · 评论