Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes are documented here. This project follows [Semantic Versioni

## [Unreleased]

## [2.4.7] - 2026-07-19

### Changed

- Reorganized the English and Chinese documentation into a concise README plus task-focused getting-started, architecture, and production-governance guides.
- Added a clear documentation map and aligned the beginner blog with the repository guides, so readers can move from the first route to production operations without duplicated or conflicting instructions.
- Refreshed repository metadata and documentation links for clearer discovery of the Swift Package, supported platforms, and practical integration path.

## [2.4.6] - 2026-07-18

### Added
Expand Down
474 changes: 108 additions & 366 deletions README.md

Large diffs are not rendered by default.

460 changes: 103 additions & 357 deletions README.zh-CN.md

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# URLRouter documentation

[中文](README.zh-CN.md) · [Repository README](../README.md) · [Beginner blog tutorial](https://zhangjipeng.com/post-urlrouter.html)

Use the guide that matches the job in front of you:

| Guide | Use it when you need to… |
| --- | --- |
| [Getting started](getting-started.md) | install the package, create a Feature route, host it in SwiftUI, and add Universal Links |
| [Architecture](architecture.md) | decide package ownership, URL shape, public contracts, and App-shell responsibilities |
| [Production governance](production-governance.md) | add remote restrictions, a circuit breaker, concurrent-route coordination, telemetry, and contract checks |
| [ADR 0001](adr/0001-public-compatibility-gates.md) | understand the API and route-contract compatibility gates in pull requests |

## Recommended reading order

1. Complete the five-minute example in the repository README.
2. Read **Getting started** while wiring the first real Feature.
3. Read **Architecture** before a second team or Feature publishes routes.
4. Adopt the relevant parts of **Production governance** only when the product needs them.

The English and Chinese guides describe the same public behavior. Code examples
use `example.com`; replace it with an HTTPS domain controlled by your team.
22 changes: 22 additions & 0 deletions docs/README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# URLRouter 文档

[English](README.md) · [仓库 README](../README.zh-CN.md) · [新手完整教程](https://zhangjipeng.com/post-urlrouter.html)

按你当前要完成的工作选择文档:

| 文档 | 适合什么场景 |
| --- | --- |
| [接入指南](getting-started.zh-CN.md) | 安装 Package、创建 Feature 路由、接入 SwiftUI 和 Universal Link |
| [架构说明](architecture.zh-CN.md) | 设计 Package 边界、URL 形状、公开契约和 App 壳层职责 |
| [生产治理](production-governance.zh-CN.md) | 接入远程限制、紧急熔断、并发协调、遥测和契约检查 |
| [ADR 0001](adr/0001-public-compatibility-gates.md) | 了解 PR 中的公开 API 与路由契约兼容性门禁 |

## 推荐阅读顺序

1. 先完成仓库 README 的 5 分钟示例。
2. 接第一个真实 Feature 时阅读**接入指南**。
3. 第二个团队或 Feature 要发布路由前阅读**架构说明**。
4. 只有产品确实需要时,再按需采用**生产治理**的能力。

中英文文档描述相同的公开行为。示例中的 `example.com` 只是占位符;请替换为
团队自己控制的 HTTPS 域名。
127 changes: 127 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# URLRouter architecture and route contracts

[中文](architecture.zh-CN.md) · [Documentation index](README.md)

This guide explains the boundaries that keep a routing setup understandable as
the number of screens and Feature packages grows.

## The four responsibilities

| Part | Owns |
| --- | --- |
| URL contract | A stable, public address for a destination and its presentation style |
| Feature Package | URL parsing plus creation of the destination view |
| App shell | Linked module registration, trusted hosts, policy, and scene navigation state |
| URLRouter | URL validation, route resolution, and SwiftUI navigation state changes |

An optional fifth piece, `URLRouterPolicyProvider`, refreshes a remote policy
into an in-memory policy store. It never performs navigation and the core
library does not depend on it.

## Recommended package direction

```text
App target
├── ArticleFeature package ──> URLRouter
├── SettingsFeature package ─> URLRouter
└── URLRouterPolicyProvider (optional, App target only)
```

Features do not import each other to present views. The App depends on Feature
packages and registers their public `RouteModule`s. A Feature may expose a
small URL-builder API, or a shared contracts package may expose builders when a
strict dependency graph requires it. Do not expose another Feature's view type
as a navigation API.

## Design URLs as public contracts

Use URLs that a web page, notification, and another Feature can all understand:

```text
https://example.com/articles/42?presentation=push&version=1
```

Keep these rules from the first release:

- HTTPS only, with hosts controlled by your team.
- Stable IDs only; never credentials, tokens, contact data, or full JSON.
- `presentation` is required: `push`, `tab`, `sheet`, or `fullScreenCover`.
- Public or long-lived links include a contract `version`.
- The owning Feature documents and constructs its own URLs with `URLComponents`.

A published URL has the same compatibility cost as a public Swift API. Prefer
adding a new path or version during a migration; do not quietly reinterpret or
delete an existing path.

## Keep parsing local to the Feature

One module can own several links. Put literal paths before parameterized paths:

```swift
switch link.pathComponents {
case ["articles"]:
return ModuleRoute(moduleID: "articles", routeID: "list")
case ["articles", "saved"]:
return ModuleRoute(moduleID: "articles", routeID: "saved")
case ["articles", let id] where !id.isEmpty:
return ModuleRoute(moduleID: "articles", routeID: "detail", parameters: ["id": id])
default:
return nil
}
```

Do not accept path suffixes that the contract does not define. A strict parser
makes a malformed external link fail safely instead of opening a surprising
screen.

`ModuleRoute` contains a module ID, route ID, and string parameters. Keep
business objects out of it; the destination can load fresh data through its
view model or use case. That makes restoration, testing, and cross-package
boundaries simpler.

## Keep the App shell small

`AppRoutes.swift` should only register modules already linked into the app:

```swift
enum AppRoutes {
static let registry = ModuleRouteRegistry(modules: [
ArticleFeature.module,
SettingsFeature.module
])
}
```

It should not become one giant switch over every path. The registry validates
duplicate module IDs, incorrect module ownership, and missing destinations for
presented routes.

The shell is the appropriate place for global rules: trusted hosts, supported
URL versions, feature flags, authorization decisions, and analytics adapters.
It is not the right place to hard-code an app's login implementation. Let the
app decide how to authenticate; let the routing policy say whether the current
route may open.

## Use tabs deliberately

`RouterHost` manages pushed and modal destinations. For tab routes it writes
`router.selectedTab`. Bind that state to the root `TabView` selection, and use
the same identifier for the URL route ID and the tab tag. This keeps a tab URL
from being “handled” without visibly switching tabs.

## Publishing or changing a route

For every public route change, update these items in one pull request:

1. The Feature's parser and URL builder.
2. `RouteContracts.json`.
3. Tests and caller documentation.
4. Migration notes if an old URL remains in emails, websites, notifications, or
released apps.

The repository CI catches structural and breaking catalog changes; it cannot
decide your product migration policy. Treat a removed or reinterpreted URL as a
breaking change and plan it accordingly.

For rollout, remote policy, concurrency, and observability guidance, continue
to [Production governance](production-governance.md).
115 changes: 115 additions & 0 deletions docs/architecture.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# URLRouter 架构与路由契约

[English](architecture.md) · [文档目录](README.zh-CN.md)

这篇说明解释项目变大后,如何仍让路由边界保持清楚、容易维护。

## 四个职责

| 部分 | 负责什么 |
| --- | --- |
| URL 契约 | 目标页面稳定、公开的地址,以及它的展示方式 |
| Feature Package | URL 解析和目标 View 的创建 |
| App 壳层 | 已链接模块的注册、可信 host、策略和场景导航状态 |
| URLRouter | URL 校验、路由解析和 SwiftUI 导航状态更新 |

还有一个可选的第五部分:`URLRouterPolicyProvider`。它把远程策略刷新到内存
Store 中,不负责任何页面跳转,核心库也不依赖它。

## 推荐的 Package 依赖方向

```text
App target
├── ArticleFeature package ──> URLRouter
├── SettingsFeature package ─> URLRouter
└── URLRouterPolicyProvider(可选,仅 App target)
```

Feature 不应为了展示对方页面而互相 import。App 依赖各 Feature Package,并
注册它们公开的 `RouteModule`。Feature 可以公开一个很小的 URL builder API;依赖
边界特别严格时,也可以由一个共享 contracts package 提供 builder。不要把另一个
Feature 的 View 类型当成导航 API 暴露出去。

## 把 URL 当作公开契约来设计

让网页、推送和另一个 Feature 都能理解同一条 URL:

```text
https://example.com/articles/42?presentation=push&version=1
```

从第一版开始坚持这些规则:

- 只使用 HTTPS 和团队控制的 host。
- 只放稳定 ID;不放凭据、token、联系方式或完整 JSON。
- `presentation` 必填:`push`、`tab`、`sheet` 或 `fullScreenCover`。
- 对外或长期存在的链接带契约 `version`。
- 由拥有它的 Feature 用 `URLComponents` 文档化和构造 URL。

一旦发布,URL 的兼容成本和公开 Swift API 一样高。迁移时优先新增路径或版本;
不要悄悄改变或删除一条已有路径的含义。

## 让 Feature 就地解析

一个 module 可以拥有多条链接。固定路径要写在带参数的路径前面:

```swift
switch link.pathComponents {
case ["articles"]:
return ModuleRoute(moduleID: "articles", routeID: "list")
case ["articles", "saved"]:
return ModuleRoute(moduleID: "articles", routeID: "saved")
case ["articles", let id] where !id.isEmpty:
return ModuleRoute(moduleID: "articles", routeID: "detail", parameters: ["id": id])
default:
return nil
}
```

契约没有定义的路径后缀不要接受。严格解析会让不合法的外部链接安全失败,而不是
打开意外页面。

`ModuleRoute` 只包含 module ID、route ID 和字符串参数。不要把业务对象塞进去;
目标页面应通过 ViewModel 或 Use Case 加载最新数据。这样恢复状态、测试和跨 Package
边界都会更简单。

## 让 App 壳层保持很小

`AppRoutes.swift` 只注册已链接进 App 的模块:

```swift
enum AppRoutes {
static let registry = ModuleRouteRegistry(modules: [
ArticleFeature.module,
SettingsFeature.module
])
}
```

它不应变成一个解析全 App 所有路径的大 `switch`。registry 会校验重复 module ID、
错误的模块归属,以及需要展示却缺少 destination 的路由。

壳层适合放全局规则:可信 host、支持的 URL 版本、Feature 开关、权限决定和埋点
适配器;不适合写死 App 的登录实现。登录怎么做由 App 决定,路由策略只回答“当前
这条路由是否允许进入”。

## 有意识地处理 Tab

`RouterHost` 管理 push 和模态目标。收到 tab 路由时,它会写入
`router.selectedTab`。将这个状态绑定到根部 `TabView` 的 selection,并让 URL 的
route ID 与 tab tag 完全一致。这样路由不会出现“已处理但界面没有切换 tab”的问题。

## 发布或修改一条路由

每次修改公开路由,都在同一个 PR 中更新:

1. Feature 的解析器和 URL builder。
2. `RouteContracts.json`。
3. 测试和调用方文档。
4. 旧 URL 已存在于邮件、网页、推送或已发布 App 时的迁移说明。

仓库 CI 会阻止目录结构错误和破坏性契约变化,但它不会替你决定产品迁移策略。
删除或改变已有 URL 的含义,应按破坏性变更处理并提前规划。

关于灰度、远程策略、并发与可观测性,请继续阅读
[生产治理](production-governance.zh-CN.md)。
Loading