路由库设计思想
# 15.路由库设计思想
本篇定位:路由是分布式系统和端侧应用的"导航系统"——决定一个请求 / 跳转到底去哪里。本文从一个跨端不一致的故事讲起,回答三个核心问题——路由解决什么本质问题?端侧与服务端路由有什么共性?怎么设计一个跨端统一的路由库?
# 目录介绍
# 01.一次跳转的灾难
# 1.1 三端不一致
某电商在搞"商品详情页改版"——把商品 ID 从路径参数改成了查询参数:
旧:/product/123
新:/product?id=123&from=search
2
改了一周后,发现用户反馈:
- Android:从首页点商品能进,从消息推送进就崩溃
- iOS:从分享链接进的人看到的是空白页
- Web:搜索结果跳转还是用旧 URL,新 URL 报 404
flowchart TD
A[商品 URL 改版] --> B{各端实现?}
B -->|Android| And[在 47 处文件改硬编码 URL]
B -->|iOS| iOS[在 38 处文件改硬编码 URL]
B -->|Web| Web[在 23 处组件改硬编码 URL]
And --> Miss1[漏改了 5 处]
iOS --> Miss2[漏改了 3 处]
Web --> Miss3[漏改了 4 处]
Miss1 & Miss2 & Miss3 --> Crash[各种崩溃和 404]
style Miss1 fill:#ffebee
style Miss2 fill:#ffebee
style Miss3 fill:#ffebee
style Crash fill:#ffebee
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1.2 重构的代价
事后清点:
- 3 个端工程师各花 2 周排查漏改
- 回滚了 2 次发版
- 客服收到 4000+ 条相关投诉
- GMV 当周下跌 8%
更恶劣的是,这种"商品 URL 改版"在 3 年里发生过 5 次,每次都要走一遍这个流程。
# 1.3 反思路由设计
事后这个团队意识到了几个最深刻的问题:
- URL 不该硬编码在业务代码里——应该有统一的路由表
- 三端协议必须一致——别一个用 path 一个用 query
- 路由必须可灰度可降级——出问题能秒级回滚
- 跳转必须有兜底——目标页不存在时不能崩溃
路由是看似简单实则关键的基础设施——做对了项目稳定 5 年,做错了每次改版都是地狱。
# 02.要解决的核心矛盾
# 2.1 硬编码的痛
反例(业界普遍存在):
// 业务代码各处直接写 URL / 类名
fun gotoProduct(id: Long) {
val intent = Intent(this, ProductActivity::class.java)
intent.putExtra("id", id)
startActivity(intent)
}
// 上百个 Activity,几百个跳转点,全都这么写
2
3
4
5
6
7
8
问题:
- ProductActivity 改名 → 几百处都要改
- 加个公共拦截器(如登录校验) → 改不动
- 通过推送 / 链接进入 → 没法跳
# 2.2 灵活与性能
graph LR
A[功能强大<br/>正则 / 模糊匹配] --> B[每次匹配遍历所有路由]
B --> C[路由表大时性能差]
A2[结构精简<br/>HashMap 直接命中] --> B2[O 1 性能极佳]
B2 --> C2[但只支持精确匹配]
A3[树形匹配<br/>Trie 树 / Radix 树] --> B3[平衡灵活与性能]
style C fill:#fff3e0
style C2 fill:#fff3e0
style B3 fill:#e8f5e8
2
3
4
5
6
7
8
9
10
11
12
# 2.3 跨端一致性
移动端 + Web + 服务端,路由概念不同:
| 端 | 路由对象 | 协议 |
|---|---|---|
| Android | Activity / Fragment | Intent + Scheme |
| iOS | ViewController | URLScheme |
| Web | 组件 / 页面 | URL Hash / History |
| 服务端 | API 处理函数 | HTTP Path |
统一的关键:用 URL 作为通用契约——所有端都用 URL 表示路由。
# 2.4 路由的本质
路由 = "符号"到"实体"的映射 + 跳转控制
它的核心是 解耦"调用方"和"被调用方"——调用方只需要知道一个字符串(URL / 路径),不需要知道具体类、模块、地址。
# 03.业界主流方案
# 03.1 端侧路由库
| 平台 | 主流路由库 | 特点 |
|---|---|---|
| Android | ARouter(阿里)/ TheRouter(货拉拉) | 注解 + APT 编译期生成路由表 |
| iOS | JLRoutes / MGJRouter(蘑菇街) | 运行时注册 + Block 处理 |
| Web | React Router / Vue Router | Hash / History 路由 |
| Flutter | Fluro / GoRouter | 声明式路由 |
# 03.2 服务端路由
| 框架 | 路由风格 |
|---|---|
| Spring MVC | 注解 @RequestMapping |
| Express.js | 链式 API app.get('/path') |
| Gin (Go) | Radix Tree + 链式 |
| FastAPI (Python) | 装饰器 + 类型注解 |
# 03.3 横向对比矩阵
以 Android 端为例对比主流路由库:
| 维度 | ARouter | TheRouter | DeepLinkDispatch | 自研 |
|---|---|---|---|---|
| 作者 | 阿里 | 货拉拉 | Airbnb | - |
| 匹配方式 | URL Path + APT | URL Path + APT | URL Pattern + APT | 多样 |
| 支持模块化 | ✅ | ✅ | ✅ | 取决于实现 |
| 拦截器 | ✅ | ✅ | 弱 | 取决于实现 |
| 降级处理 | ✅ | ✅ | 弱 | - |
| 跨端 | ❌ | 部分 | ❌ | 自定义 |
| 性能 | 高(编译期) | 高 | 中 | - |
| 维护状态 | 较少更新 | ✅ 活跃 | 维护 | - |
实战推荐:
- 大型 App:TheRouter / ARouter
- 中型 App:DeepLinkDispatch
- 小型 App:自己写一个简单的路由表 Map 也行
# 04.设计核心原则
# 04.1 注解驱动原则
让"声明"和"实现"绑在一起:
// 直接在目标类上声明路由
@Route(path = "/product/detail")
class ProductDetailActivity : Activity() {
@Autowired var productId: Long = 0
}
// 跳转时只需要 URL
ARouter.getInstance()
.build("/product/detail")
.withLong("productId", 123)
.navigation()
2
3
4
5
6
7
8
9
10
11
好处:
- 路由信息一目了然
- 删除目标类时编译报错
- 不需要维护额外路由表
# 04.2 协议统一原则
铁律:所有路由跳转都走 URL——内部跳转、推送跳转、分享链接、Deeplink 都一致。
graph TB
subgraph "统一入口"
URL["xx://route/product/123?from=push"]
end
URL --> Parse[解析路由]
Parse --> Match[匹配路由表]
Match --> Inter[拦截器链]
Inter --> Target[目标页面]
Source[来源] --> S1[内部跳转]
Source --> S2[推送]
Source --> S3[Deeplink]
Source --> S4[分享链接]
Source --> S5[Web 跳原生]
S1 & S2 & S3 & S4 & S5 -->|都生成统一 URL| URL
style URL fill:#fff3e0
style Target fill:#e8f5e8
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 04.3 拦截器原则
横切关注点必须能集中处理:
flowchart LR
Req[路由请求] --> I1[登录拦截器]
I1 --> I2[实名认证拦截器]
I2 --> I3[网络检查拦截器]
I3 --> I4[白名单拦截器]
I4 --> I5[降级拦截器]
I5 --> Target[目标页面]
style I1 fill:#fff3e0
style I3 fill:#e8f5e8
style I5 fill:#ffebee
2
3
4
5
6
7
8
9
10
11
典型拦截器:
| 拦截器 | 作用 |
|---|---|
| 登录拦截 | 未登录跳转到登录页,登录后回到原目标 |
| 实名拦截 | 强制实名才能进入特定页面 |
| 白名单 | 灰度页面仅特定用户可进 |
| 降级 | 页面下线时跳备用页或提示 |
| 埋点 | 自动记录页面跳转 |
# 04.4 性能优先原则
路由匹配是高频操作——每次跳转都要做。性能要求:
- 匹配 O(1) 或 O(log N):用 HashMap / Trie / Radix Tree
- 避免反射:用 APT 编译期生成代码
- 路由表可分组:按业务模块分组,减少冲突
# 05.路由架构落地
# 05.1 整体架构
graph TB
subgraph "编译期"
Annotate["@Route 注解"]
APT[APT 注解处理器]
RouteTable[路由表代码]
Annotate --> APT --> RouteTable
end
subgraph "运行时"
App[App 启动]
Init[路由初始化<br/>加载路由表]
Map[路由表 HashMap]
App --> Init --> Map
end
subgraph "跳转流程"
Call[业务调用 navigate URL]
Parse[URL 解析]
Match[路由匹配]
Chain[拦截器链]
Target[目标实体]
Call --> Parse --> Match --> Chain --> Target
Match -.查询.-> Map
end
style APT fill:#fff3e0
style Map fill:#e8f5e8
style Chain fill:#f3e5f5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 05.2 路由表生成
编译期路由表生成的核心:
// 1. 业务代码声明
@Route(path = "/product/detail", desc = "商品详情页")
class ProductDetailActivity : Activity()
// 2. APT 在编译期生成对应的注册类
class Route$$Group$$product : IRouteGroup {
override fun loadInto(atlas: Map<String, RouteMeta>) {
atlas["/product/detail"] = RouteMeta(
type = ACTIVITY,
destination = ProductDetailActivity::class.java,
path = "/product/detail",
group = "product"
)
}
}
// 3. 启动时按需加载
ARouter.init { /* 自动加载所有 Route$$Group$$xxx */ }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
为什么编译期生成而不是运行时反射:
- 性能:反射开销巨大(每次都要扫包)
- 可控:编译期错误能立即发现(如重复路径)
- 支持模块化:每个模块生成自己的路由表
# 05.3 路由匹配算法
简单 HashMap 匹配:
val routeMap = HashMap<String, RouteMeta>()
val target = routeMap["/product/detail"] // O(1)
2
支持参数的 Trie 树匹配:
graph TD
Root["/"] --> Product[product]
Root --> User[user]
Product --> Detail[detail]
Product --> List[list]
Product --> ID[":id" 参数节点]
User --> Profile[profile]
User --> UID[":uid" 参数节点]
style ID fill:#fff3e0
style UID fill:#fff3e0
2
3
4
5
6
7
8
9
10
11
12
13
路径匹配示例:
/product/detail→ 直接命中 detail 节点/product/123→ 命中参数节点:id,提取 id = 123/user/456/profile→ user → :uid (uid=456) → profile
Radix Tree(基数树)优化:把"只有一个子节点的链"压缩成一个节点,Gin 框架就用这种。
# 05.4 拦截器链
经典责任链模式:
interface Interceptor {
fun intercept(chain: Chain)
}
class LoginInterceptor : Interceptor {
override fun intercept(chain: Chain) {
if (!isLoggedIn()) {
// 跳到登录页,登录后回到原目标
saveOriginalRoute(chain.request)
chain.redirect("/login")
} else {
chain.proceed() // 放行
}
}
}
class InterceptorChain {
fun run(request: Request) {
val interceptors = listOf(
LoginInterceptor(),
AuthInterceptor(),
DegradationInterceptor()
)
// 依次执行
executeNext(0, request, interceptors)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 05.5 跨端协议设计
统一的 URL 协议:
xx://route/{path}?{params}
例:
xx://route/product/detail?id=123&from=push
xx://route/h5?url=https://example.com
xx://route/native/login?redirect=/product/detail
2
3
4
5
6
协议字段约定:
| 字段 | 含义 | 示例 |
|---|---|---|
| scheme | App 内统一 scheme | xx |
| host | 路由标识 | route |
| path | 路由路径 | /product/detail |
| query | 路由参数 | id=123 |
| fragment | 锚点(Web) | #section1 |
三端实现接同一协议:
graph TB
URL["xx://route/product/detail?id=123"] --> A[Android Router]
URL --> I[iOS Router]
URL --> W[Web Router]
URL --> S[Server Path]
A --> ProductAct[ProductActivity]
I --> ProductVC[ProductViewController]
W --> ProductPage[ProductPage 组件]
S --> ProductHandler[/api/v1/product/detail Handler]
style URL fill:#fff3e0
2
3
4
5
6
7
8
9
10
11
12
# 06.关键问题解决
# 06.1 鉴权与降级
鉴权:通过拦截器实现:
@Route(path = "/order/list", needLogin = true)
class OrderListActivity : Activity()
// 拦截器自动处理
class LoginInterceptor : Interceptor {
override fun intercept(chain: Chain) {
val meta = chain.request.meta
if (meta.needLogin && !User.isLoggedIn()) {
chain.request.saveAsRedirect()
chain.replaceRoute("/login")
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
降级:远程配置降级路由表:
// 远程下发降级配置
{
"/product/detail": {
"enable": false,
"fallback": "/h5?url=https://example.com/maintenance"
}
}
// 拦截器优先检查降级配置
class DegradationInterceptor : Interceptor {
override fun intercept(chain: Chain) {
val config = remoteConfig.getRouteConfig(chain.request.path)
if (config?.enable == false) {
chain.replaceRoute(config.fallback)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 06.2 动态路由更新
热更新场景:上架新页面、临时下线某页面、跳转策略变更。
flowchart TD
Server[配置中心] -->|下发配置| App[App]
App --> Local[本地路由表]
Local --> Q1{有动态覆盖?}
Q1 -->|是| RemoteRoute[使用动态路由]
Q1 -->|否| StaticRoute[使用编译期路由]
Remote[动态配置项] --> R1[禁用某路由]
Remote --> R2[替换某目标]
Remote --> R3[新增临时路由]
style RemoteRoute fill:#fff3e0
2
3
4
5
6
7
8
9
10
11
12
13
# 06.3 跨端跳转一致
典型场景:Web 页面里点链接打开原生页。
sequenceDiagram
participant Web as Web 页面
participant Bridge as JSBridge
participant Router as 路由库
participant Native as 原生页面
Web->>Bridge: jumpRoute('xx://route/product/detail?id=123')
Bridge->>Router: 解析 URL
Router->>Router: 匹配路由表
alt 匹配成功
Router->>Native: 跳转
else 匹配失败
Router->>Web: 返回兜底 URL
Web->>Web: 用 H5 显示
end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 07.常见陷阱与反例
# 07.1 硬编码反例
反例:开篇那个改 URL 漏改了 12 处的故事。
教训:任何路由相关的字符串都不能散落在业务代码。集中到常量类或路由库。
# 07.2 路由地狱反例
反例:某团队为了"灵活",路由库支持:动态注册、运行时反射、模糊匹配、回调嵌套……结果每次跳转的代码超过 50 行。
// ❌ 过度设计
Router.with(this).context(this).withParams(params)
.interceptors(listOf(...))
.fallback(...)
.timeout(5000)
.onSuccess { ... }
.onFail { ... }
.onIntercept { ... }
.navigate()
2
3
4
5
6
7
8
9
教训:路由库要"简单到 1 行调用"。
// ✅ 简洁
Router.go("/product/detail", "id" to 123)
2
# 07.3 协议碎片反例
反例:Android 用 app://product/123,iOS 用 myapp://product?id=123,Web 用 /product?productId=123——三端 URL 不一致,分享链接、推送、Deeplink 全都要做适配。
教训:协议必须三端一致。在最早期就要约定。
mindmap
root((三大反例))
硬编码
URL 散落各处
改一处漏几处
重构成本高
路由地狱
过度设计
调用啰嗦
学习成本高
协议碎片
三端不一致
Deeplink 混乱
增加适配成本
2
3
4
5
6
7
8
9
10
11
12
13
14
# 08.演进路线
# 08.1 V1 字符串硬编码
特征:项目起步、跳转不多。
做法:直接 startActivity(Intent(this, XxxActivity::class.java))。
痛点:随项目变大不可维护。
# 08.2 V2 集中路由库
特征:项目复杂、模块化、需要拦截器。
做法:
- 引入 ARouter / TheRouter
- 注解 + APT
- 拦截器体系
- URL 协议统一
适用阶段:中大型 App
# 08.3 V3 跨端统一路由
特征:多端共存、动态运营。
做法:
- 三端协议统一
- 远程降级配置
- 动态路由表
- 数据驱动埋点
适用阶段:大型多端应用
flowchart LR
V1[V1 硬编码<br/>简单] --> V2[V2 路由库<br/>中大型 App]
V2 --> V3[V3 跨端统一<br/>多端协同]
style V1 fill:#e3f2fd
style V2 fill:#e8f5e8
style V3 fill:#fff3e0
2
3
4
5
6
7
# 09.总结与决策
# 09.1 上线检查表
新引入路由库或新增路由前对照:
- [ ] 路由协议(scheme/host/path)跨端一致
- [ ] 路由路径有命名规范(
/业务/页面) - [ ] 注解驱动而非手动注册
- [ ] 编译期校验(重复路径报错)
- [ ] 拦截器机制完备(登录 / 实名 / 降级 / 埋点)
- [ ] 远程降级配置就位
- [ ] 跳转失败有兜底(404 页 / 默认页)
- [ ] 参数传递有类型校验
- [ ] 跳转性能 < 50ms
- [ ] 路由 Map 监控(数量 / 命中率)
- [ ] Deeplink / Push 统一入口
- [ ] 文档化路由列表(自动生成)
# 09.2 选型决策树
flowchart TD
Start([我要做路由]) --> Q1{什么端?}
Q1 -->|Android| Q2{规模?}
Q1 -->|iOS| iOS{选 JLRoutes 或自研}
Q1 -->|Web| Web{用框架自带<br/>React Router/Vue Router}
Q1 -->|后端| Server{用框架自带<br/>Spring/Express/Gin}
Q2 -->|小型 App < 20 页面| Simple[自己写一个 Map 即可]
Q2 -->|中型 App| ARouter[ARouter / TheRouter]
Q2 -->|大型 App + 模块化| TheRouter[TheRouter 推荐]
Q3([跨端要求?]) --> Cross[三端协议先统一<br/>各端再选库]
style Simple fill:#e3f2fd
style ARouter fill:#e8f5e8
style TheRouter fill:#fff3e0
style Cross fill:#ffebee
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
最后一句话:路由库是代码量小但作用大的基础设施——一个商品详情 URL 改版能拖垮三个端的发布节奏,根因就在路由没设计好。
好的路由设计 = 简单到一行调用、灵活到任意拦截、统一到跨端一致、可控到秒级降级。