Appearance
第18章 设计模式与架构决策
"架构决策的本质不是选择最好的方案,而是在当前约束下选择最合理的取舍。微前端十年,教会我们的不是某个框架有多好,而是在隔离与共享之间,永远没有完美的平衡点——只有适合此时此刻的平衡点。"
本章要点
- 识别微前端生态中反复出现的 10 个核心设计模式,理解它们解决的本质问题
- 深入剖析"隔离 vs 共享"这一微前端永恒张力的技术根源与工程权衡
- 从失败案例和被放弃的方案中提炼出比成功经验更有价值的教训
- 展望微前端下一个五年:Web Components 标准化、Server Islands、Edge Rendering
- 理解微前端的终极形态不是"前端的微服务",而是"模块的联邦"
- 作为全书收官章,建立从细节到全景的架构思维——不仅知道"怎么做",更知道"为什么这样做"
写完前面十七章,我们已经从 single-spa 的路由劫持、乾坤的 JS/CSS 沙箱、import-html-entry 的资源解析、Module Federation 的编译时共享、Wujie 的 iframe 增强、Web Components 的原生隔离等各个维度,完成了对微前端架构的全面解剖。如果把前面的章节比作"显微镜"——逐行逐函数地观察每个框架的每一处实现,那么本章我们需要换一种工具:望远镜。
站在足够远的距离回望整个微前端生态,你会发现一个令人惊叹的事实:在那些看似各自为营的框架实现之下,存在着一组反复出现的设计模式。single-spa 在用,乾坤在用,Module Federation 在用,Wujie 也在用——只是表现形式不同。这些模式不是教科书上的学术练习,而是无数工程师在真实生产环境中,面对真实的隔离需求、真实的性能约束、真实的团队协作压力做出的真实选择。
同时,微前端十年的发展史本身就是一部"架构决策史"。每一次技术迭代——从 iframe 到路由分发,从运行时沙箱到编译时联邦——都意味着一次架构哲学的重新审视。那些被放弃的方案和失败的尝试,往往比最终胜出的方案包含更多的智慧。
本章是全书的收官之章。我们将从设计模式、架构张力、失败教训、未来展望四个维度,为你构建一幅微前端架构决策的全景图。
下图展示了微前端生态中 10 个核心设计模式按生命周期阶段的分布:
Facade 模式的关键价值不仅在于简化——更在于解耦。子应用的开发者不需要知道主应用用的是乾坤还是 single-spa,只需要暴露约定的生命周期函数。反过来,主应用也不需要知道子应用用的是 React 还是 Vue。Facade 在两者之间划出了一条清晰的契约边界。
深度洞察:Facade 模式在微前端中的一个微妙风险是"过度封装"。乾坤的
start()函数隐藏了大量底层决策(沙箱类型、CSS 隔离策略、预加载策略),当这些默认决策不适合你的场景时,你需要"穿透" Facade 去修改行为。这就是为什么乾坤后来添加了越来越多的配置项——Facade 的简洁性与灵活性之间,存在天然的张力。
18.1.2 Proxy 模式:JS 沙箱的核心机制
Proxy(代理)模式为另一个对象提供一个替身或占位符,以控制对这个对象的访问。乾坤的 ProxySandbox 是微前端领域最经典的 Proxy 模式应用——它用 ES6 Proxy 拦截子应用对 window 的所有操作,在不修改真实 window 的前提下为每个子应用提供独立的全局环境。
typescript
// 乾坤 ProxySandbox 的核心实现(简化)
class ProxySandbox {
private updatedValueSet = new Set<PropertyKey>();
private fakeWindow: Record<PropertyKey, any>;
proxy: WindowProxy;
constructor(name: string) {
const rawWindow = window;
const fakeWindow = Object.create(null);
this.fakeWindow = fakeWindow;
this.proxy = new Proxy(fakeWindow, {
get(target, prop) {
// 某些属性必须从真实 window 读取
if (prop === 'window' || prop === 'self' || prop === 'globalThis') {
return proxy; // 返回代理本身,形成闭环
}
// 优先从 fakeWindow 读取(子应用的修改)
if (target.hasOwnProperty(prop)) {
return target[prop];
}
// 兜底到真实 window(共享的全局 API)
const value = rawWindow[prop as any];
// 如果是函数,需要绑定正确的 this
if (typeof value === 'function' && !isBoundFunction(value)) {
return value.bind(rawWindow);
}
return value;
},
set(target, prop, value) {
// 所有写操作都发生在 fakeWindow 上
target[prop] = value;
updatedValueSet.add(prop);
return true;
},
has(target, prop) {
return prop in target || prop in rawWindow;
},
});
}
}这段代码体现了 Proxy 模式的精髓:子应用以为自己在操作 window,实际上操作的是一个代理对象。所有读操作先查代理、再查真实对象;所有写操作只发生在代理上。这实现了"读时共享、写时隔离"的效果——本质上是 Copy-on-Write 策略在 JS 全局对象上的应用。
18.1.3 Strategy 模式:可切换的隔离策略
下图展示了 Strategy 模式在微前端隔离策略中的应用,运行时根据配置选择不同的隔离算法:
Strategy(策略)模式定义了一系列算法,把它们一个个封装起来,并且使它们可以互相替换。微前端中的隔离机制天然适合 Strategy 模式——CSS 隔离可以选择 Shadow DOM、Scoped CSS 或运行时前缀;JS 隔离可以选择 Proxy 沙箱、快照沙箱或 iframe 沙箱。
typescript
// 微前端中的 Strategy 模式:CSS 隔离策略
interface CSSIsolationStrategy {
name: string;
apply(appHTML: string, appName: string): string;
revert(appName: string): void;
}
// 策略一:Shadow DOM 隔离
class ShadowDOMStrategy implements CSSIsolationStrategy {
name = 'shadow-dom';
apply(appHTML: string, appName: string): string {
// 将子应用的 DOM 树挂载到 Shadow DOM 中
// 利用浏览器原生的样式隔离能力
const container = document.getElementById(appName);
const shadow = container!.attachShadow({ mode: 'open' });
shadow.innerHTML = appHTML;
return appHTML;
}
revert(appName: string): void {
// Shadow DOM 会随宿主元素一起销毁
}
}
// 策略二:Scoped CSS 前缀
class ScopedCSSStrategy implements CSSIsolationStrategy {
name = 'scoped-css';
apply(appHTML: string, appName: string): string {
// 为所有 CSS 选择器添加子应用特定前缀
// .container { } → div[data-qiankun="order-app"] .container { }
return this.rewriteCSS(appHTML, appName);
}
private rewriteCSS(html: string, scope: string): string {
// 通过正则或 CSS AST 改写选择器
return html.replace(
/([^{}]+)\{/g,
(match, selector) => `div[data-qiankun="${scope}"] ${selector.trim()} {`
);
}
revert(appName: string): void {
// 移除注入的 scoped style 标签
}
}
// 策略三:动态 Style 标签管理
class DynamicStyleStrategy implements CSSIsolationStrategy {
name = 'dynamic-style';
apply(appHTML: string, appName: string): string {
// 子应用激活时添加样式,失活时移除
return appHTML;
}
revert(appName: string): void {
document.querySelectorAll(`style[data-app="${appName}"]`)
.forEach(el => el.remove());
}
}
// 使用:运行时选择策略
function createIsolation(config: AppConfig): CSSIsolationStrategy {
if (config.strictStyleIsolation) return new ShadowDOMStrategy();
if (config.experimentalStyleIsolation) return new ScopedCSSStrategy();
return new DynamicStyleStrategy();
}Strategy 模式在微前端中的价值尤为突出,因为不同的子应用可能需要不同的隔离策略。一个使用 Ant Design 的子应用可能在 Shadow DOM 下样式异常(因为 Ant Design 会动态向 document.head 注入样式),需要回退到 Scoped CSS;而一个完全自包含的子应用则可以享受 Shadow DOM 的完美隔离。Strategy 模式让这种"按需选择"成为可能。
18.1.4 Observer 模式:跨应用事件通信
Observer(观察者)模式定义了一种一对多的依赖关系,当一个对象的状态变化时,所有依赖它的对象都会收到通知。微前端中的跨应用通信几乎都建立在 Observer 模式之上——无论是全局事件总线、自定义事件还是共享状态管理。
typescript
// 微前端事件总线的典型实现
class MicroFrontendEventBus {
private events = new Map<string, Set<Function>>();
// 发布事件
emit(eventName: string, data?: any): void {
const handlers = this.events.get(eventName);
if (handlers) {
handlers.forEach(handler => {
try {
handler(data);
} catch (error) {
console.error(
`[EventBus] Handler error for "${eventName}":`, error
);
}
});
}
}
// 订阅事件
on(eventName: string, handler: Function): () => void {
if (!this.events.has(eventName)) {
this.events.set(eventName, new Set());
}
this.events.get(eventName)!.add(handler);
// 返回取消订阅函数——关键的内存管理细节
return () => {
this.events.get(eventName)?.delete(handler);
};
}
// 子应用卸载时的清理
offAll(appName: string): void {
// 移除该应用注册的所有事件处理器
// 防止已卸载的子应用继续响应事件(内存泄漏)
}
}
// 乾坤的 initGlobalState 本质上就是这个模式
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({ user: null, theme: 'light' });
// 子应用 A:设置用户信息
actions.setGlobalState({ user: { name: '杨艺韬', role: 'admin' } });
// 子应用 B:响应用户信息变化
actions.onGlobalStateChange((state, prev) => {
console.log('全局状态变更:', state, prev);
// 更新本地 UI
});深度洞察:Observer 模式在微前端通信中的最大风险不是技术实现,而是治理缺失。当 10 个子应用通过全局事件总线通信时,"谁发了什么事件"、"谁在监听什么事件"、"事件的数据格式是什么"——如果这些没有统一的契约管理,事件总线会迅速退化为"全局变量 2.0"。成熟的微前端团队会为事件通信建立 TypeScript 类型定义和 Schema 校验,就像后端微服务需要 API 契约一样。
18.1.5 Mediator 模式:主应用作为协调者
Mediator(中介者)模式用一个中介对象来封装一系列对象的交互。在微前端中,主应用不仅是 Facade(对外的统一界面),还是 Mediator(对内的协调中心)——它管理子应用之间的生命周期协调、资源冲突解决和状态同步。
typescript
// 主应用作为 Mediator 的协调逻辑
class AppMediator {
private activeApp: MicroApp | null = null;
private apps = new Map<string, MicroApp>();
async switchApp(targetName: string): Promise<void> {
const target = this.apps.get(targetName);
if (!target) throw new Error(`App "${targetName}" not registered`);
// 1. 协调当前应用的卸载
if (this.activeApp) {
// 通知其他子应用:某个应用即将卸载
this.broadcast('app:before-unmount', {
name: this.activeApp.name,
});
await this.activeApp.unmount();
// 清理该应用的副作用
this.activeApp.sandbox?.deactivate();
this.broadcast('app:after-unmount', {
name: this.activeApp.name,
});
}
// 2. 协调目标应用的挂载
this.broadcast('app:before-mount', { name: targetName });
target.sandbox?.activate();
await target.mount();
this.activeApp = target;
this.broadcast('app:after-mount', { name: targetName });
}
private broadcast(event: string, data: any): void {
this.apps.forEach(app => {
app.eventHandler?.(event, data);
});
}
}Mediator 模式的价值在于避免子应用之间的直接依赖。如果子应用 A 需要在子应用 B 卸载后才能安全挂载(比如它们操作同一个 DOM 容器),这个协调逻辑应该在主应用中完成,而不是让 A 直接感知 B 的存在。