Skip to content

第14章 其他前沿方案

"当所有人都在争论哪个框架更好时,真正的变革往往来自一个没人注意到的浏览器标准提案。"

本章要点

  • 深入理解 Garfish(字节跳动)的 Loader/Router/Sandbox 三层架构,以及它对乾坤设计哲学的继承与超越
  • 掌握 Micro App(京东)基于 WebComponent 自定义元素的微前端实现路线及其设计取舍
  • 理解 Import Maps 浏览器原生模块加载规范的工作机制,以及它如何改变微前端的依赖共享范式
  • 把握 Server-Driven UI、Server Islands、边缘计算组合等前沿趋势与微前端的融合方向

前面的章节中,我们花了大量篇幅剖析乾坤、single-spa、Module Federation 和 Wujie——这些是 2026 年微前端领域的"四大天王",覆盖了绝大多数生产场景。但微前端的版图远不止这四个名字。

字节跳动内部孵化的 Garfish,在抖音电商、飞书等超大规模场景中经历了严苛的生产验证;京东的 Micro App 另辟蹊径,用 WebComponent 自定义元素重新定义了子应用的加载与隔离范式;而浏览器原生的 Import Maps 规范,正在悄悄削弱"我们为什么需要一个微前端框架"这个根基性假设。

更远处,Server-Driven UI 和 Server Islands 等服务端驱动的架构模式,正在模糊前端微服务与后端微服务之间的边界。当 CDN 边缘节点可以在 5ms 内完成 HTML 片段的组合,当服务端可以动态决定每个 UI 区域加载哪个版本的哪个组件——传统意义上的"前端微前端"是否还有存在的必要?

这一章,我们不追求面面俱到的 API 文档式罗列,而是抓住每个方案的核心设计决策本质差异点,帮助你在已有的架构认知框架上,快速定位这些方案的坐标。

下图展示了微前端各方案在"隔离强度"和"加载粒度"两个维度上的定位:

14.1 Garfish(字节跳动):乾坤的继承者

14.1.1 从字节的痛点说起

2021 年,字节跳动的前端团队面临一个现实问题:乾坤在中小规模场景下表现优秀,但当子应用数量超过 20 个、页面级别的动态组合需求出现时,乾坤的一些设计假设开始被挑战。

最典型的三个痛点:

  1. 预加载策略过于粗放——乾坤的 prefetch 要么全量预加载,要么不加载,缺乏基于路由优先级的细粒度控制
  2. 路由与应用的绑定过于刚性——一个路由对应一个子应用的模型,在"同一个页面需要组合多个子应用的不同区域"时力不从心
  3. 沙箱性能在高频切换场景下不够理想——飞书等 SaaS 产品的用户可能在一分钟内切换十几次页面,沙箱的创建和销毁开销变得不可忽视

Garfish 就是在这样的背景下诞生的。它的设计目标很明确:保留乾坤"运行时沙箱 + HTML Entry"的核心范式,在此基础上解决大规模、高频次、多区域的工程化问题。

14.1.2 三层架构:Loader、Router、Sandbox

Garfish 的架构可以用三个核心模块来概括。与乾坤将加载、路由、沙箱逻辑耦合在主流程中不同,Garfish 做了更清晰的分层:

┌─────────────────────────────────────────────────┐
│                   Garfish 核心                    │
│                                                   │
│  ┌─────────┐   ┌─────────┐   ┌──────────────┐   │
│  │  Loader  │   │  Router  │   │   Sandbox    │   │
│  │  资源加载 │   │  路由管理 │   │   JS/CSS隔离 │   │
│  │          │   │          │   │              │   │
│  │ ·HTML解析│   │ ·路由劫持 │   │ ·Proxy沙箱  │   │
│  │ ·JS提取  │   │ ·激活规则 │   │ ·快照沙箱   │   │
│  │ ·CSS提取 │   │ ·多实例   │   │ ·样式隔离   │   │
│  │ ·预加载  │   │ ·嵌套路由 │   │ ·副作用收集 │   │
│  └─────────┘   └─────────┘   └──────────────┘   │
│                                                   │
│  ┌─────────────────────────────────────────────┐ │
│  │              Plugin System (插件系统)         │ │
│  │   生命周期钩子 ─ 资源转换 ─ 沙箱扩展         │ │
│  └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘

Loader(资源加载器) 负责获取和解析子应用资源。来看核心流程:

typescript
// Garfish Loader 核心流程(简化自源码)
interface AppInfo {
  name: string;
  entry: string;           // 子应用入口 URL
  activeWhen?: string;     // 路由激活规则
  cache?: boolean;         // 是否启用缓存
}

class Loader {
  private appCache: Map<string, AppCacheItem> = new Map();
  private loadingMap: Map<string, Promise<AppResources>> = new Map();

  async loadApp(appInfo: AppInfo): Promise<AppResources> {
    const { name, entry, cache } = appInfo;

    // 1. 命中缓存:直接返回
    if (cache && this.appCache.has(name)) {
      return this.appCache.get(name)!.resources;
    }

    // 2. 正在加载:复用 Promise,避免重复请求
    if (this.loadingMap.has(name)) {
      return this.loadingMap.get(name)!;
    }

    // 3. 发起加载
    const loadPromise = this.fetchAndParse(entry);
    this.loadingMap.set(name, loadPromise);

    try {
      const resources = await loadPromise;
      if (cache) {
        this.appCache.set(name, {
          resources,
          timestamp: Date.now(),
        });
      }
      return resources;
    } finally {
      this.loadingMap.delete(name);
    }
  }

  private async fetchAndParse(entry: string): Promise<AppResources> {
    // 获取 HTML
    const html = await fetch(entry).then(res => res.text());

    // 解析 HTML,提取 JS 和 CSS 资源
    const { scripts, styles, template } = parseHTML(html, entry);

    // 并行加载所有 JS 和 CSS
    const [jsContents, cssContents] = await Promise.all([
      Promise.all(scripts.map(src => this.fetchScript(src))),
      Promise.all(styles.map(href => this.fetchStyle(href))),
    ]);

    return { template, jsContents, cssContents, scripts, styles };
  }
}

与乾坤的 import-html-entry 相比,Garfish 的 Loader 有两个关键改进:

  1. 去重加载——通过 loadingMap 避免对同一个子应用的并发重复请求。在预加载和用户导航同时触发时,这个细节可以避免双倍的网络开销。
  2. 细粒度缓存控制——支持按应用粒度开关缓存,而不是全局的开或关。对于频繁变更的子应用可以关闭缓存,对于稳定的公共模块则开启缓存。

Router(路由管理器) 是 Garfish 与乾坤差异最大的模块:

typescript
// Garfish Router 的多实例路由匹配
interface RouterConfig {
  // 支持多个子应用同时激活
  apps: Array<{
    name: string;
    activeWhen: string | ((path: string) => boolean);
    // 关键:指定子应用挂载到哪个 DOM 容器
    domGetter: string | (() => HTMLElement);
  }>;
  // 路由拦截策略
  autoRefreshApp?: boolean;
  // 基础路径
  basename?: string;
}

class GarfishRouter {
  private apps: Map<string, RouterAppConfig> = new Map();
  private activeApps: Set<string> = new Set();

  /**
   * 核心方法:根据当前 URL 计算需要激活和销毁的子应用
   * 与乾坤的 "一个路由 = 一个子应用" 不同,
   * Garfish 允许同一路由下激活多个子应用
   */
  async reroute(currentPath: string): Promise<void> {
    const nextActiveApps = new Set<string>();

    // 遍历所有注册的子应用,判断哪些应该被激活
    for (const [name, config] of this.apps) {
      if (this.matchRoute(currentPath, config.activeWhen)) {
        nextActiveApps.add(name);
      }
    }

    // 计算需要卸载的子应用
    const appsToUnmount = [...this.activeApps]
      .filter(name => !nextActiveApps.has(name));

    // 计算需要挂载的子应用
    const appsToMount = [...nextActiveApps]
      .filter(name => !this.activeApps.has(name));

    // 先卸载,再挂载(保证 DOM 清理在前)
    await Promise.all(appsToUnmount.map(name => this.unmountApp(name)));
    await Promise.all(appsToMount.map(name => this.mountApp(name)));

    this.activeApps = nextActiveApps;
  }

  private matchRoute(
    path: string,
    rule: string | ((path: string) => boolean)
  ): boolean {
    if (typeof rule === 'function') return rule(path);
    // 支持通配符和前缀匹配
    return path.startsWith(rule);
  }
}

下图展示了 Garfish 与乾坤在路由模型上的关键差异:

多实例激活是 Garfish 路由设计的核心竞争力。在飞书这样的产品中,一个页面的左侧导航、中间内容区、右侧面板可能分别由三个不同的子应用提供。乾坤的"一个路由绑定一个子应用"模型处理这种场景需要大量的 workaround,而 Garfish 从路由层面原生支持。

Sandbox(沙箱系统) 在原理上与乾坤的 Proxy 沙箱一脉相承,但做了重要的性能优化:

typescript
// Garfish 沙箱的副作用收集机制
class GarfishSandbox {
  private fakeWindow: Record<string, any>;
  private proxyWindow: WindowProxy;
  // 关键优化:副作用收集器
  private sideEffects: Array<() => void> = [];

  constructor() {
    this.fakeWindow = Object.create(null);
    this.proxyWindow = new Proxy(this.fakeWindow, {
      get: (target, key: string) => {
        // 优先从沙箱自身读取
        if (key in target) return target[key];
        // 回退到真实 window
        const value = (window as any)[key];
        // 如果是函数,需要绑定到真实 window(避免 this 指向问题)
        if (typeof value === 'function' && !this.isConstructor(value)) {
          return value.bind(window);
        }
        return value;
      },
      set: (target, key: string, value) => {
        target[key] = value;
        // 记录副作用,以便沙箱销毁时回收
        this.sideEffects.push(() => {
          delete target[key];
        });
        return true;
      },
    });
  }

  /**
   * 关键优化:拦截 addEventListener,
   * 在沙箱销毁时自动移除所有事件监听
   */
  patchEventListener(): void {
    const rawAddEventListener = window.addEventListener;
    const rawRemoveEventListener = window.removeEventListener;
    const listenerMap = new Map<string, Set<EventListener>>();

    this.proxyWindow.addEventListener = (
      type: string,
      listener: EventListener,
      options?: boolean | AddEventListenerOptions
    ) => {
      if (!listenerMap.has(type)) {
        listenerMap.set(type, new Set());
      }
      listenerMap.get(type)!.add(listener);
      rawAddEventListener.call(window, type, listener, options);
    };

    // 沙箱销毁时,自动移除所有注册的事件监听
    this.sideEffects.push(() => {
      for (const [type, listeners] of listenerMap) {
        for (const listener of listeners) {
          rawRemoveEventListener.call(window, type, listener);
        }
      }
      listenerMap.clear();
    });
  }

  /**
   * 沙箱销毁:逆序执行所有副作用回收
   */
  destroy(): void {
    // 逆序执行,保证后添加的副作用先被清理
    for (let i = this.sideEffects.length - 1; i >= 0; i--) {
      this.sideEffects[i]();
    }
    this.sideEffects = [];
  }

  private isConstructor(fn: Function): boolean {
    try {
      new (fn as any)();
      return true;
    } catch {
      return false;
    }
  }
}

副作用收集的设计思路是:与其在卸载时尝试"猜测"子应用做了哪些全局修改,不如在运行时就记录下每一个副作用,卸载时精确回收。 这比乾坤的快照对比方案更高效——不需要遍历整个 window 对象来发现差异。

14.1.3 插件系统:Garfish 的扩展性设计

下图展示了 Garfish 插件系统的钩子执行时序,覆盖了子应用从加载到卸载的完整生命周期:

Garfish 的插件系统借鉴了 Webpack 的 Tapable 设计,提供了贯穿整个生命周期的钩子:

typescript
// Garfish 插件接口
interface GarfishPlugin {
  name: string;
  version?: string;

  // 应用加载阶段
  beforeLoad?(appInfo: AppInfo): void | false;
  afterLoad?(appInfo: AppInfo, appInstance: App): void;

  // 应用挂载阶段
  beforeMount?(appInfo: AppInfo, appInstance: App): void;
  afterMount?(appInfo: AppInfo, appInstance: App): void;

  // 应用卸载阶段
  beforeUnmount?(appInfo: AppInfo, appInstance: App): void;
  afterUnmount?(appInfo: AppInfo, appInstance: App): void;

  // 资源处理钩子(这是乾坤没有的)
  beforeEval?(appInfo: AppInfo, code: string): string;
  afterEval?(appInfo: AppInfo): void;

  // 沙箱扩展钩子
  sandboxConfig?(appInfo: AppInfo): Partial<SandboxConfig>;
}

// 使用示例:自定义资源预处理插件
const cssModulesPlugin: GarfishPlugin = {
  name: 'garfish-plugin-css-modules',
  // 在执行 JS 之前,处理 CSS Modules 的命名空间
  beforeEval(appInfo, code) {
    // 为所有 CSS 类名添加子应用前缀
    return code.replace(
      /\bclassName\s*:\s*["']([^"']+)["']/g,
      (match, className) => {
        return match.replace(className, `${appInfo.name}__${className}`);
      }
    );
  },
};

// 注册插件
Garfish.use(cssModulesPlugin);

基于 VitePress 构建