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);
  }
}

基于 VitePress 构建