Skip to content

路由分发与容器模式

基于微前端 2026 生态 · 核于 2026-07

速查

  • 容器(container / shell / root config / 主应用·基座)是整个系统里唯一常驻的一方,管四件事:路由分发、公共 chrome、鉴权注入、生命周期编排
  • 路由分发两种心智:声明式活性映射——URL 决定谁活着(single-spa activity function / qiankun activeRule);手动挂载——挂载点出现即加载(<micro-app> 标签、wujie 组件、single-spa parcel)
  • activity function = 纯函数 (location) => boolean:导航事件发生时被统一调用,框架据真值变化驱动各应用 mount/unmount
  • single-spa registerApplication 四要素:name / app 加载函数 / activeWhen / customProps
  • URL 是第一通信总线:Fowler 把路由跳转列为跨应用通信的推荐手段之一——地址栏就是全局状态里最稳、耦合最低的一份
  • deep link 铁律:任何页面直达 URL 刷新必须可渲染;「只能从首页点进来」的微前端等于路由没做完
  • 跨应用跳转手段梯度:容器导航 API > 共享 history > 原生 <a> 整页刷新(Vercel 多项目 zone 间导航即整页跳转,需专门优化)
  • 鉴权归容器(Fowler):「用户只应登录一次」——认证是坚定归容器所有的横切关注点;容器取 token、初始化时注入,微应用带 token 调自家 API
  • 最薄哲学(single-spa 官方):「root config 的存在只为启动各应用」——容器是所有团队的公共依赖与唯一耦合点,每加一行业务逻辑都在重建巨石
  • 容器变胖症状:子应用上线要等容器发版 / 容器里长出业务组件 / 「公共 utils」逐月膨胀——命中任何一条即需瘦身

一、容器是什么:称谓与职责

各家生态对同一个角色叫法不同,先对齐词汇:

称谓出处侧重
container applicationFowler渲染公共骨架、处理横切关注点、组合各微前端
app shellPWA/通用语境常驻的页面骨架
root configsingle-spa根 HTML + 注册各应用的那段 JS
主应用 / 基座qiankun 及国内生态注册与编排微应用的宿主
hostModule Federation消费 remote 模块的一方

职责清单与「为什么归它」:

职责内容归容器的理由
路由分发维护 URL ↔ 微前端活性的映射一个地址栏只能有一个裁判
公共 chromeheader/nav/footer、主题、Layout用户感知的「整体性」由常驻方保证
鉴权与会话登录流程、token 获取与注入用户只该登录一次(Fowler)
生命周期编排加载/挂载/卸载、加载失败的降级 UI韧性:一个子应用挂掉不能拖垮整站
依赖底座import maps / 共享库版本裁决全局只能有一个版本仲裁点(治理细节见核心机制叶

注意这份清单只有平台性职责、没有业务——这不是巧合,是第五节「最薄哲学」的伏笔。

二、路由分发的两种心智

2.1 声明式活性映射:URL 决定谁活着

中心化注册表:容器启动时把所有微应用注册进去,每个应用声明「URL 长什么样时我该活着」。single-spa 的原语最典型:

js
// root config:注册表即路由表
registerApplication({
  name: 'orders',
  app: () => import('https://cdn.example.com/orders/entry.js'), // 加载函数
  activeWhen: (location) => location.pathname.startsWith('/orders'), // activity function
  customProps: { authToken: '…' },
});

activity function 是纯函数:以 window.location 为入参、返回真值表示应用应当激活;框架在每次导航事件(popstate、路由库跳转、triggerAppChange)时统一调用全部活性函数,对比前后真值差异,自动驱动该挂载的挂载(mount)、该卸载的卸载(unmount)。qiankun 的 activeRule 是同一心智的字符串/函数形态。

这套心智的价值在于URL 成为唯一事实源:任何时刻「页面上活着哪些应用」可以由地址栏推导出来——可测试(给定 location 断言活性集合)、可深链(URL 即状态快照)、可并存(多个活性函数同时为真即多应用共存)。

2.2 手动挂载:挂载点决定谁加载

另一派把微前端当组件用:宿主页面(可能本身就是某个业务应用)在自己的组件树里放一个挂载点,挂载点渲染出来才加载对应微应用——micro-app 的 <micro-app name="…" url="…"> 标签、wujie 的 Vue/React 组件、single-spa 的 parcel(「渲染但不管路由」的形态)都属此类。此时路由裁判是宿主自己的 router:微前端出不出现,取决于宿主路由渲不渲染那个组件。

对比维度声明式活性映射手动挂载(组件式)
事实源中心注册表 + URL宿主组件树
适合粒度页面级/区块级应用切分页内嵌一块、弹窗、跨框架复用组件
嵌套场景主应用统一编排子应用里再嵌孙应用更自然
典型single-spa application、qiankunmicro-app、wujie、single-spa parcel

两种可以混用,但同一块业务只能有一个路由裁判。双裁判(容器注册了 activeRule,宿主组件又按自己的路由条件渲染挂载点)是挂载竞态、循环跳转与「卸载时机诡异」类 bug 的高发根源——排这类问题先画「谁在决定这块 DOM 的生死」。

三、应用间跳转与深链

Fowler 示例应用的做法值得记住:所有微应用共享同一个 history 实例——任何一个应用 history.push(),容器与其他应用都会响应。跨微前端跳转就是一次普通的路由跳转,「URL 变化」本身充当了应用间事件。这也是 Fowler 通信建议的一部分:能用路由表达的状态迁移,就不要再发明自定义消息。

跳转手段按耦合与代价排一个梯度:

手段体验代价
容器暴露统一导航 API / 共享 historySPA 级无刷新各应用对容器产生 API 依赖,需版本纪律
直接改 URL(pushState 约定)SPA 级无刷新约定散落,须文档化 base 前缀分配
原生 <a href> 整页跳转有白屏,但最解耦状态全丢,靠 URL 与存储重建;Vercel 多项目组合里跨 zone 导航即此形态,平台层再做预取优化

deep link 铁律:每一个业务页面的 URL 都必须支持直接刷新进入——冷启动时容器完成注册、活性判定、加载、挂载的整条链路要能凭 URL 独立走通。验收方式简单粗暴:把全站每个路由贴进新开的无痕窗口。同时给每个子应用分配互不重叠的 base 前缀/orders/*/profile/*),子应用内部路由必须挂在自己的前缀下——越界即抢别人的路由。

四、容器的横切职责:鉴权与公共 chrome

鉴权是 Fowler 写得最斩钉截铁的一段:「用户只应认证一次,所以鉴权坚定地属于容器应用拥有的横切关注点。」流程固定:容器完成登录、持有 token → 初始化/挂载时把 token 注入各微应用(如 customProps)→ 微应用带 token 请求自家后端。至于「自家后端」为什么最好是每个团队自己的 BFF,见与相邻方案的关系

公共 chrome(导航、页头页脚、主题切换)默认归容器;组织再大一点的变体是把导航本身拆成一个独立发布的「导航微前端」,容器只留槽位——代价是多一个部署单元,收益是改导航不用发容器。判断标准依旧是发布频率:谁改得勤,谁就该独立部署

错误兜底是容易漏掉的第四件事:子应用加载失败(CDN 抖动、发布窗口)时容器必须给降级 UI——这正是 Geers「韧性站点」理念在容器层的落点。加载函数返回的 Promise 被 reject 时渲染什么,应当是容器的显式设计而不是空白。

五、最薄哲学:容器越薄,自治越真

single-spa 官方文档对 root config 的定性只有一句话,但分量很重:

Your root config exists only to start up the single-spa applications.(root config 的存在只为启动各应用。)

为什么要执拗地薄?因为容器是全体团队的公共依赖,是整个架构里唯一无法独立于他人发布的东西

  • 容器发版,所有微应用被动回归——容器里的代码越多,全员陪跑越频繁;
  • 业务逻辑进了容器,业务变更就要动容器——「改我的功能却要等平台排期」,团队自治名存实亡;
  • 容器里的共享工具函数会自然膨胀成事实上的框架——新巨石的胚胎。

变胖症状自查:子应用上线要等容器发版窗口;容器仓库里出现业务组件;common/utils 目录逐月增长;容器发版频率追平业务应用。纪律对应也简单:业务逻辑一律下放子应用;公共 UI 抽成带版本的共享库或独立的导航微前端,而不是长在容器里;容器只留上面第一节那五行职责表。

工程后果:容器厚度是微前端架构最诚实的健康指标——root config 常年几十行,说明边界立住了;容器逼近万行,说明你在用微前端的成本运营一个分布式巨石。

小结

容器(shell/root config/主应用)是微前端里唯一常驻的一方,职责恰好五件:路由分发、公共 chrome、鉴权注入、生命周期编排、依赖底座——全是平台性职责,没有业务。路由分发两种心智:声明式活性映射让 URL 成为唯一事实源(activity function / activeRule),手动挂载让宿主组件树做裁判(标签/组件/parcel),可混用但一块业务只能有一个裁判。跨应用跳转优先走 URL 这条最低耦合的总线,deep link 直达是验收底线;鉴权 token 由容器统一获取注入。最后是纪律性最强的一条:root config 只为启动应用而存在——容器每厚一分,团队自治就假一分。容器之外,微前端还要跟一圈近亲划清边界——与相邻方案的关系