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);
}
}下图展示了 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);