URL 属于谁?
一条 Route,两种运行方式
Section titled “一条 Route,两种运行方式”在一个 Architecture Lab 中,一个 Angular Remote 有一条技术上很简单的 Route:
/details独立启动时,它通过同样简单的 URL 访问:
https://remote.example/details但在 Product Shell 中,同一个 Remote 被挂在额外的产品区域下面:
https://product.example/tasks/detailsRemote 应该继续自己定义 details 的含义以及对应 View;同时 Product Shell 又必须生成一致的 Product URL。问题就出现在这条看起来很不起眼的边界上。
Remote 中最初的 Navigation 是:
router.navigate(['details']);Standalone 模式下,它如预期产生:
/create → /details但嵌入 Product Shell 后,同样命令从 Router Root 解析:
/tasks/create → /details技术上可以解释,但业务上错了。真正需要的是:
/tasks/create → /tasks/details最直接的补丁是把 Host Prefix 硬编码到 Remote:
router.navigate(['/tasks', 'details']);这样 Product Shell 内的问题解决了,但 Standalone 模式会生成一条根本不存在的 Route:
/tasks/details真正可持续的方案不是宣布其中某一个完整路径才是“正确路径”,而是让 Navigation 相对于 Remote 的 Mount Context 解析:
router.navigate(['details'], { relativeTo: remoteShellRoute,});Navigation Intent 在两个运行模式里完全相同,改变的只是解析它的上下文:
Standalone: /create → /details
Host 中: /tasks/create → /tasks/details表面上这是 Angular Router 的一个细节,实际上它指向更普遍的架构问题:
当 Host 与 Remote 运行在不同外层 URL 下时,如何让它们仍然支持同一套业务 Navigation?
答案不是从 navigate、basename 或 pushState 开始,而是从 URL 的责任归属开始。
URL 是一个产品 Contract
Section titled “URL 是一个产品 Contract”Browser URL 在组合式应用中同时承担多个职责:
Browser URL├── 定位产品├── 决定入口├── 选择产品区域├── 可能定位业务状态├── 产生 Browser History└── Reload 后应可重建它既是 External Link、Bookmark、Authentication Target,也是进入产品区域的入口、History Entry 和产品状态的可观察切片。在微前端架构中,它还额外成为 Product Shell 与 Remote 之间的 Contract。
因此,“到底哪个 Router 或 Component 技术上修改 URL”这个问题太浅。真正重要的是:系统中的哪一部分负责解释某一个 URL Section 的意义。
Route Ownership 不是“哪个 Component 调用了 Router API”。它问的是:哪一部分系统负责 canonical Product URL 的哪一段。
技术 Router 可以写 URL,却不必拥有它全部的业务含义。反过来,Remote 可以拥有自己的 Route Space 意义,即使最终 canonical URL 是 Product Shell 的 Router 写出来的。
Navigation 是 Intent。URL 是可观察状态。
Navigation 首先表达一个目标。这个目标一旦被写入 Browser URL 和 History,就成为之后还要再次解释的 State,因此它必须支持 Direct Entry、Reload、Login Redirect、Remount 和 Browser Back。
Host 拥有外层 Product Path
Section titled “Host 拥有外层 Product Path”一个清晰的 Ownership Model 通常把外层 Product Area 与 Remote 内部 Relative Route Space 分开。
例如 Product Shell 把 Remote Mount 到 /tasks,而 Remote 自己定义内部 Route Tree:
Host└── /tasks
Remote└── projects ├── list ├── details └── completed于是不同运行上下文会产生不同完整 URL。
Standalone:
/projects/projects/details/projects/completedProduct Shell 中:
/tasks/projects/tasks/projects/details/tasks/projects/completed在这个模型中,Host 拥有 Product Origin、Outer Mount Prefix、Remote Selection,以及 canonical Product URL 的 Composition。它必须知道 Remote 通过哪个 Product Path 可访问。
但这不意味着 Host 必须理解 Remote 内部每条 Route 的业务语义。对 Mounting 来说,可能只需要知道 /tasks 属于哪个 Remote。至于 projects/details 内部意味着什么,仍然属于 Remote。
Host 拥有外层 Product Path。Remote 拥有它的 Relative Route Space。
共享同一个 Router 或兼容 Mount Mechanism 时,这个模型尤其自然。但它并不是唯一正确方案。Delegated Router Area 或 Navigation Port 同样可以建立这条分界线。
Remote 拥有自己的 Relative Route Space
Section titled “Remote 拥有自己的 Relative Route Space”Remote 定义它的 Relative Subpath 以及业务含义。例如它决定项目列表是 projects,详情页是 projects/details,归档是 projects/completed。
因此 Remote 拥有:
- View 到 Relative Route 的映射;
- Subpath 的业务含义;
- Product Area 内部 Navigation;
- 对被定位 View 的业务重建。
Remote 不需要拥有的是外层 Mount Prefix。
Host 必须知道 Remote 被挂在哪个 Product Path 下。Remote 不需要知道自己当前外层 Prefix 是什么。
这种分离同时保护双方。Product Shell 可以重命名 Product Area 或把 Remote Mount 到别处,而不需要重写 Remote 的内部 Navigation。Remote 也可以独立运行,而无需模拟一个假的 Host 结构。
这里还要区分 Technical Path Ownership 与 Business State Ownership。Host 可以拥有 /tasks,却完全不知道 projects/details 需要哪些数据;Remote 可以拥有 details 的业务含义,却不负责定义完整 Product URL。
为什么硬编码 Host Prefix 会破坏独立性
Section titled “为什么硬编码 Host Prefix 会破坏独立性”Architecture Lab 后来在另一个 Remote 中也出现了同样模式。最初使用 Relative Router Command:
['projects', 'detail'];Host Integration 时被改成绝对 Product Path:
['/tasks', 'projects', 'detail'];在 Product Shell 里看起来一切恢复正常。真正的问题直到 Standalone 运行时才暴露出来。
Remote 此时也会生成:
/tasks/projects/detail而 Standalone 根本没有这条 Route。Remote 不再只知道自己的业务 Subpath,也开始知道 Host 的名称与 URL Structure。
于是变化远不只是一个 Router Command:
- Host Prefix 成为了 Remote Implementation 的一部分;
- Mount Path 改动就要求修改 Remote;
- Standalone 与 Host 模式不再使用同一套 Internal Navigation;
- Remote Test 不再验证和 Host Integration 相同的 Routing Contract;
- 外部 Product Composition 开始泄漏进本地业务实现。
一个 Absolute Path 可以解决当前 Integration Bug,同时破坏架构独立性。
问题不在于 Absolute URL 本身。产品当然需要 canonical absolute URL。真正危险的是:一个业务独立的 Product Area 把当前 Host 的外部 Composition Path 当成自己的常量。
Host Path 是 Composition Configuration,不是 Remote 的业务知识。
硬编码 Host Prefix 不会让 Remote “集成得更好”,只会让它依赖 Host。
Relative Navigation 也需要 Mount Context
Section titled “Relative Navigation 也需要 Mount Context”绝对 Host Path 的替代方案并不是简单地“永远使用相对 Navigation”。
像下面这样的命令:
router.navigate(['details']);只有在明确相对于哪个 Activated Route 解析时才是完整定义。
没有上下文时,Router 可能把它挂到 Root:
Router-Root└── details但真正需要的可能是:
Remote-Mount└── tasks └── detailsAngular 场景下,因此把 Remote 的 Active Mount Route 作为技术上下文:
router.navigate(['details'], { relativeTo: remoteShellRoute,});底层 Contract 可以简化为:
interface RemoteMountContext { baseRoute: ActivatedRoute;}这个 Interface 不是通用 Microfrontend Standard。ActivatedRoute 是 Angular-specific Dependency,意味着双方共享 Angular Runtime。真正 framework-neutral 的原则是:
Remote 不需要硬编码 Host Path;它需要一个可靠 Context,告诉它 Relative Route 在哪里生效。
React Router 可以使用 basename 或 Delegated Router Area。其他 Integration 可以提供 Base Path、Mount Props 或 Navigation Abstraction。API 名字不重要,重要的是 Relative Route Space 的归属被明确表达。
没有明确 Mount Context 的 Relative Navigation 并不自动独立,它只是定义不完整。

Navigation Intent 不是 Router Synchronization
Section titled “Navigation Intent 不是 Router Synchronization”在 Architecture Lab 中,Component 并不是到处直接调用 Router,而是发送很小的语义 Navigation Intent:
openListopenDetailsopenEditopenProjectDetailIntent 描述应用想显示什么。它不包含 Host Prefix,也不包含 Angular Router Command,不需要知道 ActivatedRoute,更不描述 pushState、React Router 或 Browser History API。
技术 Adapter 再把 Intent 翻译成 Router Command:
{ commands: ['details'], relativeTo: remoteShellRoute, replaceUrl: false}Navigation Intent 属于业务或 Presentation-near Meaning;Router Command 属于 Technical Adapter。
在这个具体例子里,Intent 通过同一 Angular Application 内的 Angular/NgRx Signal Event Channel 传递。没有使用 window Event、CustomEvent 或 postMessage。这个 Channel 也不是独立 Runtime 之间的 framework-neutral Contract。
它最终只是把一个 Command 送到同一个 Angular Router。
这个 Event 没有同步多个 Router。它只是把 Navigation Intent 翻译为 Relative Router Command。
这个区别很重要。一个 Event 不会因为名字全局可见,就自动成为稳定 Microfrontend Contract。关键在于它承载什么责任。
一个小型 Product-wide Intent 可以是:
interface ProductNavigationIntent { target: ProductLocation; history?: 'push' | 'replace';}它只描述 Target,具体 URL 留给 Owner Adapter。它不分发 Router State,不传播其他 Domain State,也不要求多个 Store/Router 互相对齐。
真正危险的是:
Router A 修改 URL→ global event→ Router B 同步 State→ Router B 修改 URL→ another event这不是跨 Boundary Navigation,而是 Router Synchronization。
Navigation Intent└── Component 请求 URL Owner 导航。
Router Synchronization└── 多个 Router 尝试相互同步同一个 State。Navigation 可以作为 Intent 穿过 Boundary,但 canonical Browser History 仍然需要一个明确的 mutating Owner。
一个 Router 是最简单的情况
Section titled “一个 Router 是最简单的情况”在 Architecture Lab 的同构 Angular Integration 中,Host 使用一个 Angular Router,Angular Remote 导出 Relative Route Array,由 Host 在 Product Prefix 下 Mount。
简化后:
Browser-URL │ ▼Angular Router │ ├── /todos │ └── relative Todos routes │ ├── /tasks │ └── relative Tasks routes │ └── /members-directory └── relative Members routes所有部分共享同一个 Browser History。Remote 请求 Navigation,共同 Router 写 canonical URL,之后 Browser Back 仍由同一个 Router 解释。
Mutating Ownership 很明确:
一个 Router└── 一个 Browser History这不意味着 Remote 没有 Route Ownership。Remote 仍然拥有自己的 Relative Route Tree 和业务意义。共同 Router 只是把这些树组合到一个 canonical URL Space 中的技术机制。
正是这种区分避免了不必要的 Shell Monolith。Host 无需业务建模 Remote 内部所有 Route,只提供 Mount Context 并组合 Relative Route Tree。
两个 Router 共享同一个 Browser History
Section titled “两个 Router 共享同一个 Browser History”当 Product Area 内部再启动第二个 Router 时,问题更复杂。
Architecture Lab 里有一个 React Activity Stream。Angular Host Router 观察:
/activity-stream/...而 React Remote 内部使用自己的 BrowserRouter:
<BrowserRouter basename="/activity-stream">{/* Remote routes */}</BrowserRouter>于是两个 Router 都在观察同一个 URL Space:
Browser-History├── Angular Host Router└── React BrowserRouterbasename 可以让 React Router 正确解释 Product Prefix 下的 Relative Path,但并没有回答完整 Ownership 问题:
谁可以修改 canonical Browser URL,谁只能解释它?
Nested Router 并不天然错误。至少有几种可行模型。
Delegated URL Area
Section titled “Delegated URL Area”Host 只识别外层 Prefix:
Host└── recognizes /activity-streamReact Remote 拥有其下全部 URL:
React Remote└── owns relative URL area这种模型里,Remote 可以在 Delegated Area 内修改 History,但 Contract 必须清晰覆盖 Mount、Unmount、Direct Entry 与 popstate。
Host 是唯一 URL Owner
Section titled “Host 是唯一 URL Owner”React Remote 只发送 Semantic Navigation Target,由 Product Shell Adapter 写 Browser URL:
React Remote└── emits Navigation Intent
Host└── changes Browser URL这样减少了竞争 History Access,但 Remote 更依赖 Navigation Port。
没有明确 Contract 的 Shared History
Section titled “没有明确 Contract 的 Shared History”两个 Router 都修改并观察同一个 History:
Angular Router+React Router└── mutate and observe the same History一段时间内可能正常,但当 pushState、replaceState、popstate、Mount 与 Unmount 对 Active URL Area 的理解不一致时,风险迅速上升。
多个 Router 可以管理;同一个 History 有多个不清晰 Owner 就困难得多。

谁可以修改 canonical URL?
Section titled “谁可以修改 canonical URL?”没有一条规则适合所有架构。
共同 Router 场景里,技术责任通常自然属于它。Delegated URL Area 场景中,Remote 可以在边界内自己修改 Browser History。跨 Framework Boundary 时,Navigation Port 可能更清晰。
关键是明确选一个模型。
Remote 在自己的 Delegated Route Space 内当然可以技术导航,但跳到另一个 Product Area 时,不应该必然硬编码对方 URL Structure。
例如:
navigate('/members-directory/details/42');调用方现在知道了另一个 Product Area 的 Name、Mount Path 和 Internal Structure。
更稳定的 Product-wide Intent 可以是:
navigation.openProductArea({ area: 'members', target: 'detail', id: '42',});由 Platform Adapter 生成 canonical Product URL。
但这也不能演化成一个中央业务 Mega Router,把所有 Remote 内部 Route 全部登记进去。Contract 只应覆盖真正跨 Ownership Boundary 的 Navigation。
Remote 可以在自己的 Delegated Route Space 内技术导航;跨 Product Boundary 时,它应该描述目标,而不是拥有对方的 URL Structure。
Route Ownership 也不仅是 Path Segment。URL 与 History 的其他部分同样需要责任:
- Path;
- Query Parameter;
- Fragment;
- History Mode:
push或replace; - optional Navigation State。
Filter Parameter 属于谁?内部导航是否保留?Fragment 属于 Remote 还是 Shell?Technical Redirect 应该替换当前 History Entry 吗?Standalone 与 Host 模式 Parameter 是否正确映射?
Route Ownership 不止到 Path Segment。Query、Fragment 与 History Semantics 也必须有明确责任。
Browser Back 重建的是 Route
Section titled “Browser Back 重建的是 Route”主动 Navigation 与 Browser Back 是两个不同操作。
主动 Navigation 创建新状态:
用户点击“Details”→ 应用发送 Navigation Intent→ URL Owner 创建 History EntryBrowser Back 则激活已经存在的 Entry:
用户点击 Back→ Browser 激活已有 History Entry→ Router 解释新的 URLNavigation 创建 History;Browser Back 解释已有 History。
共同 Angular Router 场景中,它接收 popstate,识别 URL 并重新激活 Route Tree。不应该为了这样一个已有 History Entry 再重建一次业务 Navigation Intent。
如果每次观察到 URL 变化都重新发布 Navigation Event,就容易产生双重 History Entry 或 Feedback Loop:Router 解释一个旧 Entry,然后应用又把同一状态写成新的 Navigation。
因此 Browser Back 不是 Navigation Intent。它是外部 State Change,需要 URL Owner 与 Route Tree 解释。
Route 不会自动重建 View State
Section titled “Route 不会自动重建 View State”Architecture Lab 中有一个典型流程:
- Host 打开
/todos/board。 - User 选中一个 Todo。
- Local Store 保存 Selection。
- 应用导航到
/todos/details。 - User 切到另一个 Remote。
- Todos Remote 被卸载。
- User 点击 Browser Back。
/todos/details再次被激活。- 新 Store 已经不知道之前选中了哪个 Todo。
Router 完成了自己的职责:
URL restoredRoute recognizedRemote mounted但业务详情页仍无法重建。URL 只定位“Details”,没有定位具体 Todo;真正 Selection 只存在于已经销毁的 Store 中。
因此存在几个不同层次:
Document 可加载≠Route 可解析≠View 可重建≠业务 State 已恢复Route 被恢复,不等于 View State 被恢复。
Browser History 只能恢复 URL 或可由 URL Contract 推导出的状态。
这不自动是 Host 的责任。Remote 拥有 Route 的业务意义,也必须决定重建需要哪些 Reference。Host 可以重新激活 Product Area,但不应该猜 Remote 本地曾经选中了哪个 Entity。
SPA Fallback 还不是 Deep Link
Section titled “SPA Fallback 还不是 Deep Link”直接访问:
https://product.example/todos/details需要多个层次都工作:
- DNS / Reverse Proxy 选择正确 Service。
- Web Server 返回 Entry Document。
- Shell 识别 Remote Prefix。
- Remote 识别 Relative Subpath。
- 必要业务数据被重建。
- Authentication Redirect 保留原始 Target。
典型 Server Fallback:
try_files $uri $uri/ /index.html;只证明 Browser 能拿到 SPA Entry Document。
它不证明 Remote Mount 成功,也不证明 Relative Path 能解析,更不证明 Permission、Business Data 或 Entity Selection 能重建。Login Redirect 也可能丢失 Target,即使 Server 已正确返回 index.html。
Server Fallback 让 URL 可以加载,但不让业务状态自动可重建。
Resolvable Route 仍然不等于可靠 Deep Link。只有 URL 真正定位了 Reload、Remount 或 Browser Back 后所需状态,Deep Link 才可重建。

每个详情页都必须定位 Entity 吗?
Section titled “每个详情页都必须定位 Entity 吗?”像:
/todos/details这样的 Route 可以工作,前提是具体 Selection 能从其他稳定 State 重建。在连续工作流中,Local Store 可能足够。
但 Reload、Remount 或 External Link 后,Selection 可能不存在。因此更可重建的设计可以是:
/todos/42或者:
/todos/details/42这不代表每个 UI State 都应该写进 URL。
可靠 Deep Link 一般意味着可以 Share / Bookmark,并且 Reload、Login Redirect 或 Remount 后仍然工作。因此需要稳定 Entity Reference 或其他可复现 Contract。
Transient View State 则可能只在当前 Workflow 中存在,不需要 Bookmark,Reload 后可以丢失,也不必被强行提升成 Public Product State。
不是每个 View 都需要自己的可重建 URL,但每个 URL 都应该诚实表达它到底能重建什么。
因此没有 Entity ID 的 Route 并不自动错误。只有当它被当作稳定 Deep Link,而业务前提却只存在于 Volatile Memory 时,问题才真正出现。
Standalone 与 Host URL 可以不同
Section titled “Standalone 与 Host URL 可以不同”Remote 的两种运行方式拥有不同 Composition Context。
Standalone 可能是:
https://tasks.example/projects/details产品内同一业务 Route 可能是:
https://product.example/tasks/projects/detailsOrigin 与 Outer Prefix 不同,但业务 Route projects/details 完全相同。
这不是错误。Remote 不需要在每种运行模式下拥有同一完整 Browser URL。真正重要的是 Mapping 显式、可验证。
至少应该满足:
- Relative Route Space 稳定;
- Mount Context 显式;
- Internal Navigation 不硬编码 Host Prefix;
- Authentication Redirect 知道当前 canonical URL;
- 各 Origin 的 Document Fallback 正确;
- Test 同时覆盖 Standalone 和 Host Mode。
Independent Executability 不代表 External URL 完全相同,而是业务 Route 在每个 Context 中都能明确解析。
Standalone 可以提供自己的 Mapping。Mini Host 或 Local Shell 也可以把同一 Relative Route Space Mount 到不同 Outer Prefix。关键是这个 Context 不泄漏进 Remote Domain Logic。
Route Ownership Matrix
Section titled “Route Ownership Matrix”可以用一个简化矩阵看责任:
| 责任 | Host | Remote |
|---|---|---|
| Host 模式 Product Origin | 拥有 | 使用 |
| Outer Mount Prefix | 拥有 | 不得硬编码 |
| Relative Subpath | Mount | 定义 |
| Route 的业务意义 | 只知道必要部分 | 拥有 |
| Product-wide Navigation | 协调 | 发送 Intent |
| Remote Internal Navigation | 提供 Context | 拥有 |
| Browser History | 确保明确 Owner 或 Delegated Area | 请求修改或拥有 Delegated Area |
| Deep-Link Data | 激活 Product Area | 重建业务状态 |
| Server Fallback | 负责 Host Origin | 负责 Standalone Origin |
这不是不可变法律,而是用来暴露常被混淆的责任。
Host 可以拥有 Outer Path,却不控制每个 Subroute 的业务含义。Remote 可以拥有 Route Space,却不知道完整 Product Path。共同 Router 可以修改 History,却不自动拥有 URL 中业务 State 的含义。
Technical Path Ownership 与 Business State Ownership 不是一回事。
没有通用 Routing 规则
Section titled “没有通用 Routing 规则”不同 Integration 可以有不同有效模型:
- 共同 Router: Host Mount Relative Route Array,一个 Router 拥有 Browser History。
- Delegated Router Area: Host 识别 Outer Prefix,Remote 拥有其下 URL Space。
- Navigation Port: Remote 描述 Semantic Target,Platform Adapter 生成 canonical URL。
- Standalone / Host 分离 Mapping: Business Target Space 保持稳定,Composition Context 生成不同 Outer URL。
没有一个模型永远正确。真正判断标准是 Ownership 是否清楚、Direct Entry 与 Browser Back 是否可靠、Standalone 是否可以真实测试,以及业务 State 是否能被重建。
模型本身没有 Contract 清晰重要。
URL 是稳定的产品 Contract
Section titled “URL 是稳定的产品 Contract”Architecture Lab 中的具体 Routing Bug 最终可以用 relativeTo 技术解决。但真正架构意义不是这个 Router Option,而是明确分开两种责任。
Host 可以拥有 /tasks 作为 Outer Product Area。Remote 可以定义 details 作为自己的 Relative Route。Mount Context 把两者组合起来,而无需把 Host Prefix 写进 Remote Implementation。
硬编码 Host Prefix 可以修复局部 Routing Bug,却会制造结构依赖。
跨 Ownership Boundary 的 Navigation 可以表达为 Intent,再由负责的 Adapter、共同 Router 或明确 Delegated Router Area 生成 URL。Multiple Router 仍然可行,但必须有唯一 Mutating History Owner 或明确 Delegated URL Area。
责任也不会在 Route 解析完成时结束。Route 可以合法,但业务 State 仍然缺失;Server Fallback 可以返回 SPA Document,但需要的 Entity 仍然无法重建。
恢复 Route,不等于恢复业务 State。
Standalone 与 Host Mode 可以拥有不同 Outer URL。关键是 Composition Context 与 Relative Route Space 之间存在显式 Mapping。
因此稳定 Routing Contract 远不止回答“Component 如何从 create 到 details”。它必须回答:谁拥有 Outer Product Path、谁解释 Relative Subpath、谁创建 History Entry,以及 Reload、Remount 或 Browser Back 后哪些业务 State 能真正重建。
URL 不属于此刻正在修改它的 Router。它属于那个能够让它以后被正确重建的 Product Contract。