全部文章

Writing

微前端与 SSR 共存:Layout Engine 的架构取舍

当一个跨境电商前台由多个团队并行维护,问题就不再是「要不要微前端」,而是「边界画在哪里」。

从组织问题开始

跨境电商前台有十几类页面:首页、类目、商品详情、营销落地页、购物车、结算。它们由不同团队维护,但共享商品、价格、币种、多语言这些基础能力。

单体架构下的典型症状是:一个团队要发版,所有人都得等;一个公共组件改动,要跑全量回归。于是平台决定引入 Layout Engine——按页面区域拆分模块联邦子应用,各自独立构建与发布。

边界:不是按技术分层,而是按发布单元

微前端最常见的错误是按「Header / Footer / 商品列表」切分,结果每个子应用都要依赖同一份全局状态,耦合反而更深。

这次切分的依据是发布节奏:

子应用 变更频率 归属
营销页 每周多次 增长团队
商品详情 每周 商品团队
结算流程 每月(谨慎) 交易团队
壳层(路由、布局、通信) 很少 平台团队

变更频率接近的模块放在一起,发布才不会互相牵制。壳层只做三件事:路由分发、事件转发、公共依赖注入。

通信:事件契约优先

子应用之间绝不直接 import,也不共享可变 store。所有跨模块交互通过结构化事件:

type ShellEvent =
  | { type: 'cart:add'; skuId: string; quantity: number }
  | { type: 'currency:change'; currency: string }
  | { type: 'locale:change'; locale: string };

export function emit(event: ShellEvent): void {
  window.dispatchEvent(new CustomEvent('shell:event', { detail: event }));
}

export function on(handler: (event: ShellEvent) => void): () => void {
  const listener = (e: Event) => handler((e as CustomEvent<ShellEvent>).detail);
  window.addEventListener('shell:event', listener);
  return () => window.removeEventListener('shell:event', listener);
}

约定两条规则:

  1. 只增不改:事件字段只能追加,不能重命名或删除;破坏性变更走新事件名。
  2. 壳层兜底:币种、语言这类全局状态由壳层统一维护,子应用只发意图,不自己请求。

联调成本因此从「拉齐所有团队」降到「对齐一个 TypeScript 类型」。

SSR:按路由声明,而不是全局二选一

营销页需要 SEO 与首屏速度,必须 SSR;后台配置页需要交互与状态,CSR 更合适。Layout Engine 允许按路由声明渲染模式:

export const routeConfig = {
  '/campaign/:slug': { render: 'ssr', template: 'campaign.hbs' },
  '/account/orders': { render: 'csr' },
} as const;

SSR 页面由 Node 渲染引擎输出 HTML,模板使用 Handlebars 保证渲染性能与可维护性;客户端 hydration 后接管交互。CSR 页面直接走客户端路由。

难点在于数据获取要统一:如果 SSR 与 CSR 各自请求接口,就会出现首次渲染与客户端状态不一致(hydration mismatch)。我们的做法是把数据请求抽成同构层,SSR 时在服务端预取并注入初始状态,客户端复用同一份数据。

性能:SSR 不是银弹

实测发现,SSR 页面首屏变快了,但 TTFB 变差了。原因有两个:

  • 渲染引擎串行请求多个接口,服务端等待时间叠加。
  • 模板中包含了大量非关键模块。

改造措施:

  1. 接口并行化,非关键数据延迟到客户端加载。
  2. 首屏只渲染视口内模块,其余用占位 + 懒加载。
  3. 模板缓存与接口缓存分层,静态部分不进渲染流程。

最终首屏时间下降约 40%,TTFB 回到可接受范围。

多语言:编译期与运行时分工

  • 静态文案在构建时按语言拆包,首屏只加载当前语言。
  • 动态文案(商品标题、营销规则)走接口,运行时插值。
  • 语言与币种切换通过事件广播,避免每个子应用重复请求。

结果与反思

  • 子应用可独立发布,回归范围收敛到单个模块。
  • SSR 营销页的 SEO 与首屏指标明显改善,CSR 页面交互不受影响。
  • 事件契约让跨团队协作有了明确接口,而不是靠文档与口头约定。

微前端真正的收益是组织的解耦,代价是架构复杂度上升:多一份壳层、多一套通信约定、多一层构建配置。如果团队只有两三个人、发布节奏一致,这套架构只会增加负担。

判断标准很朴素:当「等别人发版」成为常态时,微前端才值得。