Appearance
第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 个、页面级别的动态组合需求出现时,乾坤的一些设计假设开始被挑战。
最典型的三个痛点:
- 预加载策略过于粗放——乾坤的
prefetch要么全量预加载,要么不加载,缺乏基于路由优先级的细粒度控制 - 路由与应用的绑定过于刚性——一个路由对应一个子应用的模型,在"同一个页面需要组合多个子应用的不同区域"时力不从心
- 沙箱性能在高频切换场景下不够理想——飞书等 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 有两个关键改进:
- 去重加载——通过
loadingMap避免对同一个子应用的并发重复请求。在预加载和用户导航同时触发时,这个细节可以避免双倍的网络开销。 - 细粒度缓存控制——支持按应用粒度开关缓存,而不是全局的开或关。对于频繁变更的子应用可以关闭缓存,对于稳定的公共模块则开启缓存。
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);
}
}