文章

微前端概念与价值深度解析

微前端概念与价值深度解析

一句话概括

微前端是一种将前端应用分解为更小、更简单的独立模块,每个模块可由不同团队使用不同技术栈独立开发、测试和部署的架构模式。它与微服务的核心思想一脉相承——解决中大型前端项目中代码耦合、部署阻塞和技术栈锁定三大痛点。本文从概念辨析、价值论证、技术方案对比到落地适配策略,全面解析微前端的本质与选择框架。

背景与意义

如果说微服务解决了后端单体应用的噩梦,那么微前端就是为前端单体应用量身定制的解药。当你的项目发展到以下阶段之一,微前端将成为一个不得不考虑的架构选择:

  • 团队规模:超过 3 个团队维护一个前端应用
  • 代码规模:单仓库超过 100 万行代码,打包时间超过 10 分钟
  • 协作冲突:每周因为代码合并冲突浪费 20% 的开发时间
  • 发布流程:每次发布需要协调 5 个以上团队冻结代码

一线大厂的面试中,微前端几乎成了中高阶候选人的必问主题。常见的递进式问题链为:”你们为什么用微前端?→ 对比过哪些方案?→ 遇到过哪些坑?→ 如何在子应用间共享状态?→ 有没有考虑过不用微前端?” 这是一个从”用没用过”到”想明白了没有”的典型考察链路。

概念与定义

微前端 (Micro Frontends)

将前端应用分解为多个独立子应用,每个子应用拥有自己的路由、状态管理和构建流程,由独立团队全权负责。主应用(基座/容器)负责子应用的加载、卸载和通信协调。

技术栈无关 (Tech Stack Agnostic)

子应用可以使用与主应用完全不同的技术栈。Team A 用 React 18,Team B 用 Vue 3,Team C 用 Svelte——它们可以存在于同一个页面中。

独立部署 (Independent Deployment)

每个子应用可以独立构建、测试、上线,不需要等待其他团队的发布窗口。这是微前端最核心的工程价值。

增量升级 (Incremental Migration)

允许团队逐步将遗留系统(如老旧的 jQuery/AngularJS 项目)迁移到新框架,而不需要大爆炸式(Big Bang)重写。

沙箱隔离 (Sandbox Isolation)

子应用之间的全局变量、样式、DOM 不会相互污染。这是微前端最难做好的技术点。

核心知识点拆解

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
// 单体应用 vs 微前端的构建配置对比

// === 单体应用:一个 package.json,一个构建命令 ===
// package.json (单体)
{
  "name": "monolith-app",
  "scripts": {
    "build": "webpack --config webpack.prod.js",  // 构建全部模块
    "deploy": "npm run build && aws s3 sync dist/ s3://app-bucket/"
  },
  "dependencies": {
    "react": "^18.0.0",
    "vue": "^3.0.0",      // 不同技术栈在同一个包里
    "angular": "^15.0.0"   // 构建产物极其庞大
  }
}

// === 微前端:每个子应用独立的构建 ===
// packages/container/package.json
{
  "name": "@app/container",
  "scripts": {
    "build": "webpack --config webpack.container.js",
    "deploy": "npm run build && aws s3 sync dist/ s3://app-container/"
  }
}

// packages/team-product/package.json 
{
  "name": "@team/product",
  "scripts": {
    "build": "vite build",
    "deploy": "npm run build && aws s3 sync dist/ s3://team-product-cdn/"
  },
  "dependencies": {
    "react": "^18.2.0"
  }
}

// packages/team-checkout/package.json
{
  "name": "@team/checkout",
  "scripts": {
    "build": "rspack build",
    "deploy": "npm run build && aws s3 sync dist/ s3://team-checkout-cdn/"
  },
  "dependencies": {
    "vue": "^3.4.0"
  }
}

// 微前端的独立部署 CI/CD 流水线(简化的 GitHub Actions)
// .github/workflows/deploy-product.yml
name: Deploy Product App
on:
  push:
    branches: [main]
    paths:
      - 'packages/team-product/**'   // 只有 product 目录变更才触发
      - 'packages/shared/**'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          sparse-checkout: 'packages/team-product'  // 稀疏检出,加速 CI
      - uses: actions/setup-node@v4
      - run: |
          cd packages/team-product
          npm ci
          npm run build
          npm run test
          npm run deploy
      - run: |
          # 通知基座应用更新子应用资源映射
          curl -X POST https://deploy-api/app.com/update-manifest \
            -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" \
            -d '{"app":"product","version":"1.2.3","url":"https://cdn.team-product.com/1.2.3/"}'

独立部署的价值不仅体现在 CI/CD 上——它对发布节奏的影响更为深远。产品团队可以”今天提交,今天上线”,而不需要等待购物车团队冻结代码。

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
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
102
103
104
105
106
107
// 技术栈无关的核心机制 - 子应用生命周期约定
// 每个子应用必须导出一个微前端协议约定的生命周期对象

// === 子应用 A: React 18 ===
// packages/team-product/src/micro-entry.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';

let root = null;

// 微前端生命周期协议
export async function bootstrap(props) {
  // 初始化阶段:只执行一次
  console.log('[ProductReact] bootstrap', props);
  // 可以在这里做:获取全局配置、建立 WebSocket 连接、加载远程数据
}

export async function mount(props) {
  // 挂载阶段:每次子应用激活时执行
  console.log('[ProductReact] mount', props);
  const { container } = props;
  
  // 在基座提供的容器 DOM 中渲染
  const dom = container.querySelector('#product-root') || container;
  root = createRoot(dom);
  root.render(
    <React.StrictMode>
      <App basename={props.basename} />
    </React.StrictMode>
  );
}

export async function unmount(props) {
  // 卸载阶段:子应用被切换走时执行
  console.log('[ProductReact] unmount', props);
  if (root) {
    root.unmount();
    root = null;
  }
}

// === 子应用 B: Vue 3 ===
// packages/team-checkout/src/micro-entry.js
import { createApp, h } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import routes from './routes';

let app = null;
let router = null;

export async function bootstrap(props) {
  console.log('[CheckoutVue] bootstrap', props);
}

export async function mount(props) {
  console.log('[CheckoutVue] mount', props);
  const { container, basename } = props;
  
  // Vue 3 应用创建
  app = createApp(App);
  
  router = createRouter({
    history: createWebHistory(basename),
    routes,
  });
  
  app.use(router);
  app.mount(container.querySelector('#checkout-root') || container);
}

export async function unmount(props) {
  console.log('[CheckoutVue] unmount', props);
  if (app) {
    app.unmount();
    app = null;
    router = null;
  }
}

// === 子应用 C: 纯 JS(无框架) ===
// packages/team-widget/src/micro-entry.js
let container = null;

export async function bootstrap(props) {
  console.log('[WidgetJS] bootstrap');
}

export async function mount(props) {
  console.log('[WidgetJS] mount');
  container = props.container;
  container.innerHTML = `
    <div class="legacy-widget">
      <h3>遗留系统组件</h3>
      <p>这个组件使用纯 JavaScript 构建</p>
      <button onclick="window.legacyHandler()">提交</button>
    </div>
  `;
}

export async function unmount(props) {
  console.log('[WidgetJS] unmount');
  if (container) {
    container.innerHTML = '';
  }
}

可以看到,无论 React、Vue 还是纯 JS,它们导出同样的 bootstrap/mount/unmount 接口。基座应用不需要知道子应用内部使用了什么框架——这就是技术栈无关的本质。

3. 微前端 vs iframe:为什么不用 iframe?

这是每个理解微前端的人都会遇到的第一个问题。iframe 确实能实现”技术栈无关”和”独立部署”,但它在用户体验层面有致命缺陷:

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
// iframe 的问题演示
function iframeVsMicroFrontend() {
  // 1. URL 不同步 - 刷新 iframe 会回到起始页
  const iframe = document.createElement('iframe');
  iframe.src = 'https://subapp.example.com/products';
  // 用户点击内部链接到 /products/123
  // 用户刷新页面 → iframe 回到 /products(丢失了 /123)
  
  // 2. 弹窗和全屏的局限
  // iframe 内的 dialog 无法超出 iframe 边界
  // iframe 内的 Fullscreen API 需要额外权限
  
  // 3. 数据/状态共享困难
  // 只能通过 postMessage 通信,双方都要处理序列化/反序列化
  // 如果子应用需要全局状态(如用户登录态),每次都要 postMessage 请求
  
  // 4. 性能开销
  // 每个 iframe 都是一个完整的浏览器上下文
  // 包括独立的 JavaScript 引擎实例
  // 内存开销比微前端方案大 2-3 倍
  
  // 5. SEO 不友好
  // iframe 内的内容不能被搜索引擎正确索引
  // 对于 SSR 场景完全不适用
  
  // 6. 无障碍访问 (A11Y)
  // 屏幕阅读器无法跨 iframe 导航
  // 键盘导航 Tab 顺序混乱
}

// 微前端则是浏览器主文档的一部分
// 子应用共享同一个 DOM 树和浏览上下文
// 对比:
// ✅ URL 同步(路由由基座统一管理)
// ✅ 弹窗可以覆盖全局
// ✅ 状态共享(内存中的 JavaScript 对象)
// ✅ 更好的性能(无独立上下文开销)
// ✅ 更好的 SEO(服务端渲染可能性)
// ✅ 更好的无障碍支持

4. 增量升级:最实用的微前端场景

很多团队引入微前端并不是为了”多团队并行开发”,而是为了平滑迁移老旧系统。增量升级模式在技术上非常有价值:

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
// 增量升级架构:主应用作为路由网关
// 
// /  (首页)         → 主应用(React 18)
// /legacy/*         → 旧系统(AngularJS - 正在被逐步替换)
// /new/products/*   → 新系统(Vue 3 - 新功能)
// /migrated/*       → 已迁移模块(React 18 - 已完成)

// 基座应用路由配置
const appRoutes = {
  '/': {
    entry: 'https://cdn.main-app.com/latest/entry.js',
    container: '#main-content',
    activeRule: (location) => location.pathname === '/',
  },
  '/legacy/*': {
    entry: 'https://cdn.legacy-app.com/1.0.0/entry.js',
    container: '#legacy-container',
    activeRule: (location) => location.pathname.startsWith('/legacy'),
    // 旧系统特殊处理:增加过渡包装层
    loader: (code) => wrapLegacyApp(code),
  },
  '/new/products/*': {
    entry: 'https://cdn.new-products.com/latest/entry.js',
    container: '#new-container',
    activeRule: (location) => location.pathname.startsWith('/new/products'),
  },
  '/migrated/*': {
    entry: 'https://cdn.migrated-app.com/latest/entry.js',
    container: '#migrated-container',
    activeRule: (location) => location.pathname.startsWith('/migrated'),
  },
};

// 迁移策略:Strangler Fig Pattern(绞杀者模式)
//
// 阶段 1: 在旧应用中嵌入微前端加载器
//   旧 AngularJS 应用 → 嵌入一个 <div id="micro-container"> 
//   新功能使用 React/Vue 开发,通过微前端方式嵌入
//
// 阶段 2: 逐步替换旧功能
//   将 AngularJS 中某个页面重写为 React 版本
//   通过路由规则将流量导向新应用
//   新旧版本可以共存,用 Feature Flag 控制灰度
//
// 阶段 3: 旧应用退役
//   当所有功能被替换后,下线 AngularJS 应用
//   主应用完全切换到新架构

// 灰度切换代码示例
function shouldUseNewVersion(route) {
  // 基于用户 ID 的灰度
  const userId = getUserHash();
  const grayPercent = getGrayPercent(route); // 从配置中心获取
  
  return userId % 100 < grayPercent;
}

// 路由分发
function routeResolver(location) {
  const route = findMatchingRoute(location.pathname);
  
  if (route.legacyVersion && route.newVersion) {
    // 新旧版本共存期
    if (shouldUseNewVersion(route)) {
      return loadMicroApp(route.newVersion);
    } else {
      return loadMicroApp(route.legacyVersion);
    }
  }
  
  return loadMicroApp(route);
}

5. 微前端的代价

微前端不是银弹——它引入了显著的技术复杂度:

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
// 微前端的工程代价清单
const costAnalysis = {
  // 1. 加载性能:子应用资源可能重复加载
  duplicateDeps: {
    problem: 'React 18 可能被每个子应用都打包一次',
    solution: '通过 Webpack Module Federation 或 externals 共享依赖',
    cost: '增加构建配置复杂度',
  },
  
  // 2. 首屏性能:需要先加载基座,再加载子应用
  waterfallLoading: {
    problem: '基座 HTML → 基座 JS → 子应用 HTML → 子应用 JS 的瀑布流',
    solution: '预加载(prefetch)、SSR 流式渲染、子应用缓存',
    cost: '需要额外的预加载策略',
  },
  
  // 3. 运维复杂度:多个独立部署的应用
  opsComplexity: {
    problem: '从 1 个部署流水线变为 N 个,版本管理难度增大',
    solution: '自动化版本管理、兼容性测试矩阵、端到端回归',
    cost: '需要更成熟的 DevOps 工具链',
  },
  
  // 4. 调试复杂度:多应用联合调试
  debugComplexity: {
    problem: 'Bug 可能涉及基座 + 子应用 A + 子应用 B 的交互',
    solution: '统一的日志追踪系统(Trace ID 串联)',
    cost: '需要额外搭建全链路日志系统',
  },
  
  // 5. 视觉一致性
  uiConsistency: {
    problem: '不同团队使用不同的 UI 组件库,视觉风格难以统一',
    solution: '共享 Design Token + Web Component 基础组件',
    cost: '需要治理 Design System',
  },
};

实战案例:基座应用完整实现

以下是一个完整的微前端基座应用,实现子应用的注册、加载、生命周期管理和切换:

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
// MicroFrontendHost - 微前端基座应用完整实现
class MicroFrontendHost {
  constructor(options = {}) {
    this.apps = new Map();
    this.currentApp = null;
    this.loadingPromises = new Map();
    this.cacheEnabled = options.cacheEnabled !== false;
    this.defaultContainer = options.container || '#app';
    this.loadTimeout = options.loadTimeout || 10000;
    this.loadingComponent = options.loadingComponent || null;
    this.errorComponent = options.errorComponent || null;
    this.hooks = {
      beforeMount: [],
      afterMount: [],
      beforeUnmount: [],
      afterUnmount: [],
      onError: [],
    };
  }

  // 注册子应用
  registerApp(config) {
    const { name, entry, container, activeRule, props } = config;

    if (this.apps.has(name)) {
      console.warn(`[MF] 子应用 "${name}" 已注册,将被覆盖`);
    }

    this.apps.set(name, {
      name,
      entry,
      container: container || this.defaultContainer,
      activeRule: typeof activeRule === 'function' 
        ? activeRule 
        : (location) => location.pathname.startsWith(activeRule),
      props: props || {},
      status: 'registered', // registered → loading → mounted → unmounting → unmounted
      bootstrapPromise: null,
      appInstance: null,
    });

    console.log(`[MF] 已注册子应用: ${name} (${entry})`);
    return this;
  }

  // 批量注册
  registerApps(apps) {
    apps.forEach(app => this.registerApp(app));
    return this;
  }

  // 启动基座
  start() {
    console.log('[MF] 微前端基座启动');
    this.setupRouting();
    this.routeToCurrentLocation();
    
    // 监听路由变化
    window.addEventListener('popstate', () => {
      this.routeToCurrentLocation();
    });

    // 劫持 pushState 和 replaceState
    const patchHistoryMethod = (method) => {
      const original = history[method];
      history[method] = (...args) => {
        original.apply(history, args);
        this.routeToCurrentLocation();
      };
    };
    patchHistoryMethod('pushState');
    patchHistoryMethod('replaceState');

    return this;
  }

  // 路由分发
  async routeToCurrentLocation() {
    const location = window.location;
    const matchedApp = this.findMatchingApp(location);

    if (!matchedApp) {
      console.log('[MF] 未找到匹配的子应用');
      this.unmountCurrentApp();
      return;
    }

    if (this.currentApp?.name === matchedApp.name) {
      return; // 当前子应用不变
    }

    await this.switchToApp(matchedApp);
  }

  findMatchingApp(location) {
    for (const [name, app] of this.apps) {
      if (app.activeRule(location)) {
        return app;
      }
    }
    return null;
  }

  // 切换到目标子应用
  async switchToApp(targetApp) {
    console.log(`[MF] 切换子应用: ${this.currentApp?.name || 'none'}${targetApp.name}`);
    
    this.runHooks('beforeUnmount', this.currentApp);
    await this.unmountCurrentApp();
    this.runHooks('afterUnmount', this.currentApp);
    
    this.runHooks('beforeMount', targetApp);
    await this.mountApp(targetApp);
    this.runHooks('afterMount', targetApp);
    
    this.currentApp = targetApp;
  }

  // 卸载当前子应用
  async unmountCurrentApp() {
    if (!this.currentApp) return;
    
    const app = this.currentApp;
    
    if (app.status === 'mounted' && app.appInstance?.unmount) {
      try {
        const props = this.getProps(app);
        await Promise.race([
          app.appInstance.unmount(props),
          this.timeoutPromise('unmount', 5000),
        ]);
        app.status = 'unmounted';
        console.log(`[MF] 子应用 "${app.name}" 已卸载`);
        
        // 清空容器
        this.cleanContainer(app);
      } catch (error) {
        console.error(`[MF] 卸载子应用 "${app.name}" 失败:`, error);
        this.runHooks('onError', { app, phase: 'unmount', error });
      }
    }
    
    this.currentApp = null;
  }

  // 挂载子应用
  async mountApp(app) {
    try {
      // 1. 加载子应用入口
      if (app.status === 'registered' || !app.appInstance) {
        app.status = 'loading';
        const instance = await this.loadApp(app);
        app.appInstance = instance;
      }
      
      // 2. 准备好容器 DOM
      this.prepareContainer(app);
      
      // 3. 调用 mount
      app.status = 'mounted';
      const props = this.getProps(app);
      
      await Promise.race([
        app.appInstance.mount(props),
        this.timeoutPromise('mount', this.loadTimeout),
      ]);
      
      console.log(`[MF] 子应用 "${app.name}" 已挂载`);
    } catch (error) {
      app.status = 'error';
      console.error(`[MF] 挂载子应用 "${app.name}" 失败:`, error);
      this.runHooks('onError', { app, phase: 'mount', error });
    }
  }

  // 加载子应用入口 JS
  async loadApp(app) {
    // 检查缓存
    if (this.cacheEnabled && this.loadingPromises.has(app.name)) {
      return this.loadingPromises.get(app.name);
    }
    
    const loadPromise = this.fetchAndExecuteEntry(app);
    this.loadingPromises.set(app.name, loadPromise);
    
    try {
      const instance = await loadPromise;
      return instance;
    } finally {
      this.loadingPromises.delete(app.name);
    }
  }

  async fetchAndExecuteEntry(app) {
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        reject(new Error(`加载子应用 "${app.name}" 超时`));
      }, this.loadTimeout);

      // 动态创建 script 标签加载子应用
      const script = document.createElement('script');
      script.src = app.entry;
      script.crossOrigin = 'anonymous';
      script.setAttribute('data-app', app.name);
      
      script.onload = () => {
        clearTimeout(timeout);
        // 子应用入口文件应该将生命周期挂载到 window.__MICRO_APPS__[name]
        const appExports = window.__MICRO_APPS__?.[app.name];
        if (appExports) {
          resolve(appExports);
        } else {
          reject(new Error(`子应用 "${app.name}" 未导出微前端生命周期`));
        }
      };
      
      script.onerror = () => {
        clearTimeout(timeout);
        reject(new Error(`子应用 "${app.name}" 入口加载失败: ${app.entry}`));
      };
      
      document.body.appendChild(script);
    });
  }

  // 准备容器 DOM
  prepareContainer(app) {
    const container = document.querySelector(app.container);
    if (!container) {
      console.warn(`[MF] 容器 ${app.container} 不存在,创建中`);
      const el = document.createElement('div');
      el.id = app.container.replace('#', '');
      document.querySelector(this.defaultContainer)?.appendChild(el);
    } else {
      container.innerHTML = '';
    }
  }

  cleanContainer(app) {
    const container = document.querySelector(app.container);
    if (container) {
      container.innerHTML = '';
    }
  }

  // 组装传递给子应用的 props
  getProps(app) {
    return {
      name: app.name,
      container: document.querySelector(app.container),
      basename: this.getBasename(app),
      globalState: this.getGlobalState(),
      onGlobalStateChange: this.onGlobalStateChange,
      setGlobalState: this.setGlobalState,
      ...app.props,
    };
  }

  getBasename(app) {
    // 从 activeRule 推导 basename
    // 如果是字符串规则如 '/app/product',basename 就是 '/app/product'
    const rule = app.activeRule;
    if (typeof rule !== 'function') {
      return '/';
    }
    // 从注册时的规则推断
    return '/';
  }

  getGlobalState() {
    return this.globalState || {};
  }

  onGlobalStateChange(callback, fireImmediately) {
    this._globalStateCallback = callback;
    if (fireImmediately) {
      callback(this.globalState, 'init');
    }
  }

  setGlobalState(state) {
    this.globalState = { ...this.globalState, ...state };
    if (this._globalStateCallback) {
      this._globalStateCallback(this.globalState, 'set');
    }
  }

  // 路由设置
  setupRouting() {
    // 捕获所有 <a> 链接点击,阻止默认行为
    document.addEventListener('click', (e) => {
      const link = e.target.closest('a');
      if (!link || link.target === '_blank' || link.getAttribute('rel') === 'external') return;
      if (link.hostname === window.location.hostname) {
        e.preventDefault();
        history.pushState(null, '', link.href);
      }
    });
  }

  timeoutPromise(phase, ms) {
    return new Promise((_, reject) => {
      setTimeout(() => {
        reject(new Error(`[MF] ${phase} 操作超时 (${ms}ms)`));
      }, ms);
    });
  }

  on(event, handler) {
    if (this.hooks[event]) {
      this.hooks[event].push(handler);
    }
    return this;
  }

  runHooks(event, data) {
    for (const handler of this.hooks[event] || []) {
      try {
        handler(data);
      } catch (e) {
        console.error(`[MF] Hook ${event} 异常:`, e);
      }
    }
  }
}

// 使用示例
const host = new MicroFrontendHost({
  container: '#app',
  cacheEnabled: true,
  loadTimeout: 15000,
});

host.registerApps([
  {
    name: 'home',
    entry: 'https://cdn.example.com/home/latest/entry.js',
    container: '#main-content',
    activeRule: (location) => location.pathname === '/',
  },
  {
    name: 'products',
    entry: 'https://cdn.example.com/products/1.2.0/entry.js',
    container: '#main-content',
    activeRule: (location) => location.pathname.startsWith('/products'),
  },
  {
    name: 'checkout',
    entry: 'https://cdn.example.com/checkout/latest/entry.js',
    container: '#checkout-container',
    activeRule: (location) => location.pathname.startsWith('/checkout'),
  },
]);

host.start();

底层原理

微前端方案的底层技术分类

所有微前端方案在底层都可以归为三类技术路线:

  1. 路由分发式:通过 HTTP 服务器(Nginx / Traefik)根据 URL 路由到不同静态资源目录。每个子应用部署在独立路径下,浏览器加载不同 HTML。这是最简单的方案,但是页面切换会全量刷新。

  2. 组合式(Client-Side Composition):基座应用在浏览器端动态加载子应用的 JS/CSS,渲染到指定容器中。qiankun、single-spa、Module Federation 都属于此类。

  3. Web Components:将子应用封装为 Custom Elements,通过 Shadow DOM 实现天然隔离。但 Web Components 的状态管理和路由管理比框架方案复杂。

样式隔离的底层挑战

CSS 的”全局性”是微前端最棘手的问题。子应用 B 的 div.title { color: red } 可能会覆盖子应用 A 的同名类。底层解决思路:

  • CSS Scoped(qiankun 方案):在子应用加载后,遍历所有 style 标签,在每条 CSS 规则前加上子应用容器的选择器前缀(如 .app-home)。这通过 StyleSheet.cssRules API 操作。
  • Shadow DOM(Web Component 方案):浏览器原生的样式隔离,子应用的样式完全封闭在 Shadow Root 内,但跨 Shadow DOM 的弹窗(如 dialog)需要特殊处理。
  • CSS Module / CSS-in-JS(构建时方案):依靠构建工具为每个类名生成唯一 hash,如 title_3a4f2b。需要改造子应用的样式系统。

高频面试题解析

面试题 1:微前端与 iframe 的根本区别是什么?什么场景下 iframe 反而是更好的选择?

答案要点: 根本区别是:微前端的子应用运行在主文档的浏览上下文中(共享一个 window/document),iframe 有独立的浏览上下文。iframe 的优势在于最彻底的隔离(样式、脚本、存储完全独立),适合嵌入不可信的第三方内容(如支付页面、广告)。微前端的优势在于更好的用户体验(URL 同步、弹窗、性能)和可访问性。选择 iframe 的场景:嵌入第三方不可信内容、遗留系统完全没有改造可能、不需要考虑 SEO 的后台系统。

面试题 2:如何解决微前端的样式冲突?Shadow DOM 为什么没有成为主流方案?

答案要点: 样式冲突方案对比:1)CSS 选择器前缀——效果一般,无法处理子应用动态插入的 style;2)CSS Module——需要改造子应用构建配置;3)Shadow DOM——最彻底的隔离,但问题在于:弹窗/下拉菜单等挂载到 body 的元素会逃逸 Shadow DOM;全局样式(如 antd 的 body 样式)失效;CSS 变量不能穿透;增加调试难度。因此主流方案采用 CSS Module + 命名空间 + 约定式样式管理。

面试题 3:微前端应用的性能优化有哪些关键策略?

答案要点: 1)公共依赖预加载——使用 Module Federation 的 shared 配置共享 React/Vue 运行时;2)子应用预加载——当用户悬停在导航链接上时,提前下载子应用的 entry JS;3)keep-alive 模式——切换子应用时只隐藏不卸载,减少重渲染;4)资源版本化——子应用资源使用 hash 版本号,配合 CDN 强缓存;5)懒加载——非首屏子应用延迟加载;6)子应用产物瘦身——Tree-shaking + 按需加载。

面试题 4:微前端中子应用之间如何通信才靠谱?

答案要点: 通信方案按耦合度排序:1)URL 参数——最松耦合,适合传递少量数据;2)自定义事件(window.dispatchEvent)——中等耦合,适合事件通知;3)全局状态(共享 Store)——紧耦合,需要约定状态类型;4)基座统一通信(发布/订阅模式)。推荐策略:优先使用 URL 参数,其次是自定义事件,尽量避免跨子应用的共享状态。

面试题 5:你的团队是否真的需要微前端?什么情况下不应该用微前端?

答案要点: 不适合的场景:1)团队只有 1-2 个前端开发者——微前端增加的管理成本远大于收益;2)应用复杂度不高(页面少于 20 个)——单体应用更简单高效;3)没有独立部署需求——如果所有功能需要同时发布,微前端的核心优势不存在;4)对 SEO 和首屏性能有极苛刻要求——微前端的加载瀑布流无法避免。实际上,80% 的团队不需要微前端。

总结与扩展

知识体系图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
微前端
├── 核心价值
│   ├── 独立部署(消除发布耦合)
│   ├── 技术栈无关(团队自主选型)
│   ├── 增量升级(渐进式迁移)
│   └── 团队自治(独立开发测试上线)
├── 技术方案
│   ├── 路由分发(Nginx 层转发)
│   ├── 组合式(Client-Side Composition)
│   │   ├── single-spa(通用路由层)
│   │   ├── qiankun(阿里系,沙箱完善)
│   │   └── Module Federation(Webpack 5 原生)
│   └── Web Components(Shadow DOM 隔离)
├── 核心挑战
│   ├── 样式隔离(Shadow DOM / CSS Scoped)
│   ├── JS 沙箱(Proxy 快照 / iframe)
│   ├── 公共依赖(Shared Dependencies)
│   ├── 通信机制(Event / Store / URL)
│   └── 性能开销(多应用加载瀑布流)
└── 决策指南
    ├── 3 个团队 + 10 万行代码 → 考虑微前端
    ├── 配合 DevOps 成熟度评估
    └── 优先考虑 Module Federation(低侵入)

延伸阅读

  1. Micro Frontends 概念原文: Martin Fowler 的博客文章(概念起源)
  2. qiankun 文档: umijs/qiankun — 国内最流行的微前端框架
  3. Webpack Module Federation 示例: webpack.js.org/concepts/module-federation
  4. single-spa 文档: single-spa.js.org — 微前端路由层的祖师爷
  5. ThoughtWorks Technology Radar: 微前端的技术成熟度评级
本文由作者按照 CC BY 4.0 进行授权

© 独行的风. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

本站总访问量 本站访客数 本文阅读量