Skip to content

第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 的存在。

基于 VitePress 构建