路由库设计思想
# 12.路由库设计思想
本篇定位:路由是端侧应用(Android/iOS/Web/Flutter)与前后端一体化系统的"导航神经"——决定"点这里去哪里"。做好了三端一致、灰度可控、跨端跳转丝滑;做差了每次改版都是全员加班漏改地狱。本文从一次 "商品 URL 改版翻车、三端漏改 12 处、GMV 掉 8%" 的事故讲起,回答七个刺入骨髓的问题——为什么 URL 不能硬编码?路由表怎么维护跨端一致?匹配算法怎么选?拦截器链怎么设计不打架?动态路由怎么下发不出事?
# 一、案例引入:一次 URL 改版,三端漏改 12 处
# 1.1 事故现场:一周改版,两周排查,GMV 掉 8%
某电商 App,"商品详情页"由商品 ID 从路径参数改成查询参数(老 URL /product/123,新 URL /product?id=123&from=search)。产品经理拍板"三端同步改",工期一周。改版上线第一天,客服接到 4000+ 投诉:
- Android 用户:从首页点商品能打开,从 推送通知 打开却直接崩溃(
NullPointerException: id==null)。 - iOS 用户:从 微信分享链接 进来的看到白屏(老 URL 打开找不到页面)。
- Web 用户:从 搜索结果页 点商品跳到 404(搜索结果模块没同步改)。
为什么翻车?各端排查发现,商品 URL 是"硬编码"分散在几百个文件里:
- Android:47 处
Intent/startActivity/deeplink。 - iOS:38 处
UIViewControllerpush /openURL:/Universal Link。 - Web:23 处
<a href>/router.push()/window.location。
三端各自改,Android 漏 5 处、iOS 漏 3 处、Web 漏 4 处——总计 12 处漏改。而且每个端的 URL 格式还不完全一致:Android 用 arca://product/123(自定义 scheme),iOS 用 https://your.com/product/123(Universal Link),Web 用 /#/product/123(hash 路由)。
代价:
- 三端工程师各花 2 周逐文件排查漏改。
- 回滚了 2 次发版。
- 客服4000+ 投诉。
- 事故当周 GMV 下跌 8%。
- 更恶劣的是:同类"URL 改版"3 年里发生过 5 次,每次都是这个流程。
# 1.2 顺藤摸根因:不是"改漏了",是"没路由表"
复盘会锁定 7 处设计缺陷(每一处都是端侧路由的经典雷区):
| # | 缺陷 | 直接后果 |
|---|---|---|
| ① | URL 硬编码分散在业务代码里 | 改一次要动几十上百处 |
| ② | 三端 URL 协议不统一(scheme/host/path 各不相同) | 分享链接跨端跳转必翻车 |
| ③ | 没有集中的路由表 | 全靠"grep 搜索"找散落的跳转 |
| ④ | 路由 → 目标页面运行时才知道,没有编译期检查 | 目标页删了或改了,链接照发 |
| ⑤ | 未知路由没有兜底页 | 老 URL 到新版直接崩 |
| ⑥ | 参数校验在业务里做,缺失就崩溃 | 参数不符预期直接 NPE |
| ⑦ | 没有路由拦截器(鉴权/白屏检测/降级) | 未登录进付费页也不拦 |
这 7 处联合起来构成了"每次改版必翻车"的根因。修好这些,5 年不用再改一处 URL。
# 1.3 七个"为什么":疑惑清单
复盘会上灵魂拷问:
- 硬编码 URL 有什么问题?统一放一个常量类不就好了吗?
- 端侧路由和后端路由(Spring Router、Express Router)本质上是一回事吗?
- App 里用
ARouter/iOS Router、Web 用React Router——它们内核是不是同一套? - 参数怎么传?path/query/fragment/JSON body,各有什么坑?
- 拦截器/中间件那么好用,多个拦截器同时存在时执行顺序怎么定?
- 动态下发路由能不能做?做了之后 App 是不是可以随时"改主意"?
- 深链接(Deep Link)、通用链接(Universal Link)、App Link 都是啥?
这 7 问就是本文的骨架。走完第 10 章会全部串起来。
# 二、架构决策三角:一致 × 灵活 × 性能
端侧路由本质上是在三个方向做联合最优化:
一致(三端相同、离线在线相同)
▲
/│\
/ │ \
/ │ \
/ 路 │ 由\
/ 库 │ 系 \
/ 决 │ 统 \
────────┼──────
灵活(可动态、可灰度、可 A/B) 性能(匹配 <1ms、启动无延迟)
- 只求一致 → 三端一致但都很笨(写死路由),不能动态调整。
- 只求灵活 → 云端可下发但三端可能实现不齐。
- 只求性能 → 编译期确定一切,但改一次 URL 要发新版本。
架构选择的本质:选择"接受哪个代价"。电商 / 内容 App 倾向"灵活>一致"(想改就改),金融 App 倾向"一致>灵活"(编译期确认),游戏倾向"性能>一切"。
# 三、端侧路由的"存在本质":把"跳转"从"UI 代码"里剥离出来
问:"我 Android 直接 startActivity(new Intent(this, ProductActivity.class)) 不就行?为什么还要引路由库?"这是新人最常问的问题,答案要拆三层。
# 3.1 硬编码跳转的 3 种致命病
病一:类型耦合,无法跨模块跳转。
// 商品模块要调订单模块的下单页
startActivity(new Intent(this, com.orders.OrderActivity.class))
这行代码强制商品模块必须 import 订单模块——两个模块耦合成一坨。真正做模块化 / 组件化(第 2 篇讲过)的项目里,模块之间不能 import——那这行代码就写不出来。
病二:编译期无法检查目标。
Intent 用类名跳转还好,用字符串 URL 就是灾难:
// 类似这种,编译时啥都检查不出来
Uri.parse("app://product/123").let(::startActivity)
product 拼错成 porduct——编译不报错、单元测试测不到、上线跑到运行时才崩。
病三:三端不一致,跨端跳转翻车。
Web 用户分享商品到微信,Android 用户点开——URL 格式对不上,App 打不开落到 H5 → H5 又跳不回 App 深链 → 用户流失。
# 3.2 路由库要解决的 3 件事
事一:解耦模块。
模块 A 不需要 import 模块 B,只需要知道"目标 URL"。业务模块间只通过"路由协议"通信,天生解耦。
事二:统一表达。
三端用同一套 URL 语义(比如 arca://product?id=123),业务代码里跳转都写这一个字符串。至于内部怎么解析、跳到哪个 Activity/ViewController/Component,路由库负责。
事三:可拦截、可扩展。
跳转前后可以插入 Hook——鉴权、埋点、灰度分流、降级替换、参数校验……业务代码零改动。
# 3.3 什么时候不需要路由库
- 单 Activity 应用 + 无跨端场景:直接
startActivity够了。 - 纯静态站点(无路由参数):直接
<a href>就行。 - 只有 3~5 个页面的小 Demo:引入路由库是过度设计。
结论:页面数 > 20 或 模块数 > 3 或 有跨端跳转需求,就必须引路由库。
# 四、端侧路由 vs 后端路由:底层其实是一回事
新人经常问:"App 里的路由和 Spring/Express 的路由是同一个东西吗?" 是——底层原理完全一致。
# 4.1 共同的内核
任何路由系统(前后端都一样)都由 4 个组件构成:
① 路由表(Route Table)
URL 模式 → 目标处理器 的映射关系
② 匹配器(Matcher)
拿到一个具体 URL,从路由表里找出唯一匹配
③ 参数解析器(Param Extractor)
从 URL 里提取 path/query/body 参数
④ 拦截器链(Interceptor Chain)
在匹配到目标之前/之后,插入横切逻辑
Spring MVC 的 @RequestMapping、Express 的 app.get()、ARouter 的 @Route、React Router 的 <Route path=""> —— 内核都是这四件套。
# 4.2 不同之处:处理器类型
| 系统 | 处理器 | 触发方式 |
|---|---|---|
| Spring MVC | Controller 方法 | HTTP 请求 |
| Express | 中间件函数 | HTTP 请求 |
| ARouter (Android) | Activity / Fragment 类 | Intent 跳转 |
| iOS Router | UIViewController 类 | present / push |
| React Router | React 组件 | history.push |
| Vue Router | Vue 组件 | router.push |
处理器不同,但分发机制一模一样:拿 URL → 匹配路由 → 抽参数 → 走拦截器 → 到处理器。
# 4.3 端侧路由的独特之处
端侧路由比后端多两个复杂度:
独特一:跨进程边界。 从推送/分享/浏览器/其他 App 唤起本 App 时,URL 是从进程外传进来的——需要处理 scheme 唤起、Universal Link、App Links、Intent Filter 等平台机制。
独特二:跳转有 UI 转场。 后端路由是"函数调用+返回";端侧路由是"页面 A 消失 → 页面 B 出现"——动画、返回栈、页面生命周期都是路由要管的。
# 五、路由表的三种维护形态:注解 / 配置 / 动态下发
# 5.1 形态一:注解 + 编译期扫描(推荐)
Android 的 ARouter、Kotlin 的 Compose Destinations、iOS 的 Swift Macro 都走这条路:
@Route(path = "/product/detail", name = "商品详情")
class ProductDetailActivity : AppCompatActivity() {
@Autowired var productId: Long = 0
@Autowired var from: String? = null
// ...
}
编译期通过注解处理器(APT) 扫描所有 @Route,生成一张静态路由表:
// 由 APT 生成,代码里根本看不到
public class RouterMap$$Product {
static {
register("/product/detail", ProductDetailActivity.class,
new ParamMeta("productId", Long.class),
new ParamMeta("from", String.class));
}
}
优点:
- 编译期就知道所有路由,写错路径立刻报错。
- 参数类型强校验(自动
Intent.getLongExtra())。 - 零反射,启动性能好。
缺点:
- 依赖 APT / KSP / Swift Macro,构建时间变长。
- 上线后不能改路由表——想调整只能发版。
# 5.2 形态二:中心化配置文件
Web 端 React Router / Vue Router 的经典做法:
// routes.js
export const routes = [
{ path: '/product/:id', component: ProductDetail, meta: { auth: true }},
{ path: '/order/list', component: OrderList, meta: { auth: true }},
{ path: '/login', component: Login },
{ path: '*', component: NotFound } // 404 兜底
];
优点:
- 一目了然,全部路由在一个文件里。
- 修改灵活。
缺点:
- 手动维护,容易漏。
- 无编译期检查。
# 5.3 形态三:云端动态下发
真正解决"每次改 URL 都要发版"的方案——路由表放服务端:
// 客户端启动时拉取
{
"version": 42,
"routes": [
{
"path": "/product/:id",
"target": "com.example.ProductActivity",
"params": ["id:long", "from:string?"],
"auth": false,
"abTest": { "abKey": "prod_v2", "onHit": "com.example.ProductActivityV2" }
},
{
"path": "/legacy/product/:id", // 老 URL 兼容
"target": "REDIRECT:/product/:id",
"auth": false
}
]
}
优点:
- URL 改版无需发版——服务端改一条配置,全端生效。
- 可 A/B:命中实验组跳新页面、对照组跳老页面。
- 老 URL 兼容:
REDIRECT语义直接改写目标。
缺点:
- 首次启动需要拉取路由表(可 fallback 到本地默认)。
- 云端配错影响全端——需要严格灰度。
# 5.4 三形态混合:生产最佳实践
- 核心路由(登录、首页、支付):注解静态编译(任何情况下必须能跳)。
- 业务路由(商品、订单、活动):注解 + 动态覆盖(可选择性云端改写)。
- 临时页面(活动落地页、H5 承接页):纯动态下发。
# 六、URL 匹配算法:字符串匹配到 Radix Tree
# 6.1 路由匹配是什么问题
给定一个具体 URL /product/detail?id=123,从路由表里找到唯一匹配的模式(/product/detail)。看起来简单,但——
- 有 参数化路径:
/user/:id/orders - 有 通配符:
/static/* - 有 正则:
/api/v(\\d+)/user - 有 优先级:
/product/hot应该优先匹配/product/hot而不是/product/:id
朴素的做法是顺序遍历——每条规则都试一下。路由 100 条以内没问题,1000 条就慢。
# 6.2 三种主流匹配算法
算法一:正则数组顺序匹配。
// React Router 早期做法
for (const route of routes) {
const match = route.pattern.exec(url);
if (match) return route;
}
复杂度:O(N)。写 1000 条路由每请求 1000 次正则匹配,慢。
算法二:Trie 树(前缀树)。
把路由按 / 切段,建成树:
/
├── product
│ ├── detail → ProductDetailActivity
│ └── :id
│ └── related → RelatedProductActivity
├── order
│ ├── list → OrderListActivity
│ └── :orderId → OrderDetailActivity
复杂度:O(URL 段数)——与路由总数无关。
算法三:Radix Tree(Trie 压缩版)。
Trie 里如果一条路径只有一条分支,就把连续几段压成一个节点。Envoy、Gin、Kong 的路由匹配都用这个。复杂度和 Trie 一致,但内存开销小 10 倍。
# 6.3 优先级规则:谁比谁"更精确"
同一个 URL 可能匹配多条路由,要有唯一优先级规则。业界共识:
优先级从高到低:
1. 完全静态路径 /product/hot
2. 混合路径 /product/hot/:id
3. 参数化路径 /product/:id
4. 通配符/正则 /product/*
5. 兜底路径 /*, /404
React Router、Vue Router、ARouter 都遵循这套。用户写路由时不用管顺序——框架自动排序。
# 6.4 参数解析的三种传递方式
| 方式 | URL | 优缺点 |
|---|---|---|
| Path 参数 | /product/123 | 语义清晰,SEO 友好,但只能传 ID 类简单值 |
| Query 参数 | /product?id=123&from=search | 灵活多值,但 URL 冗长 |
| Fragment | /product#123 | 客户端专用,不上服务器(老 SPA hash 路由用) |
| JSON Body | POST body | 适合复杂对象,但只能同一 App 内跳转 |
规则:
- 主键必须放 Path(
/product/123)——SEO 和分享 URL 语义化。 - 筛选、来源、扩展信息放 Query(
?from=search&sort=price)。 - 复杂对象(
{a:{b:[1,2]}})不要塞 URL——用内存共享 + traceId(详见 §7.4)。
# 七、拦截器链:路由的"AOP 时刻"
拦截器是路由库最有价值的能力。第 1 章事故里 12 处漏改背后暴露的问题——没有拦截器兜底。
# 7.1 拦截器能做的六件事
用户请求 route.push("/product/123?from=search")
↓
┌────── 拦截器链(Chain)──────┐
│ ① 参数校验(缺 id 就拦掉) │
│ ② 鉴权检查(付费页要登录) │
│ ③ 埋点上报(点击了哪个入口) │
│ ④ 灰度分流(10% 用户走新页) │
│ ⑤ 降级重写(新页崩了走老页) │
│ ⑥ 白屏兜底(页不存在跳 404) │
└────────────────────────────────┘
↓
目标页面
# 7.2 责任链的实现模式
同步版(简单场景):
interface Interceptor {
fun intercept(request: RouteRequest, chain: Chain): RouteResponse
}
class Chain(private val interceptors: List<Interceptor>, private val index: Int) {
fun proceed(request: RouteRequest): RouteResponse {
if (index >= interceptors.size) {
return doNavigate(request) // 真正跳转
}
val next = Chain(interceptors, index + 1)
return interceptors[index].intercept(request, next)
}
}
异步版(需要等待网络的鉴权/灰度决策):
interface AsyncInterceptor {
suspend fun intercept(request: RouteRequest, chain: Chain): RouteResponse
}
为什么要有异步版本:鉴权可能要调远程 SSO、灰度决策可能要拉云端配置——同步阻塞主线程 = ANR。
# 7.3 拦截器执行顺序:谁在前谁在后
三条规则:
规则一:优先级由框架管,不能反过来。
ARouter 用 priority 数字排序,React Router 用中间件注册顺序——业务层不应该依赖具体顺序。
规则二:全局拦截器在前,业务拦截器在后。
全局 ─→ 参数校验 ─→ 鉴权 ─→ 埋点 ─→ 灰度 ─→ 页面自己的 beforeEnter ─→ 跳转
规则三:Fail-Fast,能拦早拦。 参数校验放在最前面——缺 id 就没必要跑鉴权和埋点了。
# 7.4 跳转参数太大怎么办?——ID 化传递
拦截器很好,但有个陷阱——参数塞太多:
// ❌ 反例:把整个订单对象塞进 URL
router.push("/order/detail?order=${gson.toJson(order)}")
// URL 超过 4KB,安卓部分设备直接 crash
正确做法:内存栈存原对象,URL 只传 ID:
// 页面 A:把对象放到路由参数仓库
val txId = RouteBridge.put(order) // 返回一个 UUID
router.push("/order/detail?txId=$txId")
// 页面 B:从仓库取
class OrderDetailActivity {
override fun onCreate() {
val txId = intent.getStringExtra("txId")
val order = RouteBridge.take(txId) // 取出并从仓库删除
}
}
为什么好:
- URL 短,任何跨进程边界都能扛。
- 复杂对象不需序列化 / 反序列化。
- 上线一次配置,全端受益。
# 八、跨端一致:Deep Link / Universal Link / App Link
事故里 iOS 分享链接进不了 App 就是这一节的问题。三种跨端跳转方式,只有一种真正解决问题。
# 8.1 三种跨端跳转的对比
| 方式 | 触发 | Android | iOS | 缺点 |
|---|---|---|---|---|
| 自定义 Scheme | myapp://product/123 | Intent Filter | URL Scheme | 未安装 App 时白屏 |
| App Links (Android) | https://your.com/product/123 | 需要 .well-known/assetlinks.json | / | 只 Android |
| Universal Links (iOS) | https://your.com/product/123 | / | 需要 apple-app-site-association | 只 iOS |
| HTTP + 智能识别 | https://your.com/product/123 | 两端各自处理 | / | 需要 Web 兜底 |
# 8.2 生产级方案:一 URL 三端通用
用户点击 https://your.com/product/123
↓
浏览器 / IM / 邮件 / 短信
↓
├── iOS 装了 App,命中 UL → App 打开 → 路由到商品详情
├── Android 装了 App,命中 App Link → App 打开
└── 未装 App 或 UL 失败 → 打开网页版 → 提示"用 App 打开"
↓
页面中有 <meta property="al:ios:app_store_id" ...> 智能跳应用商店
关键点:
- URL 由 Web 定义(后端能生成,SEO 友好,任意分享)。
- App 侧配置 UL / App Link 白名单 —— 服务端提供
.well-known/apple-app-site-association和assetlinks.json。 - 未安装兜底:网页版 + 引导下载。
# 8.3 分享跳转的完整链路
微信 App 浏览器 your.com/product/123 App
│ │ │ │
① 用户在微信点分享链接
② 微信内置浏览器打开 URL
③ 触发 UL/App Link → 尝试唤起 App
④
├── App 已装且注册了 UL → 直接进 App → 路由库解析 URL 跳详情页
└── 未装 App → 显示网页 → 顶部横幅 "打开 App 体验"
# 九、动态路由与灰度:云端下发的正确姿势
回到事故——为什么改 URL 会翻车? 因为路由是"发版才能改"的。真正的解法是云端下发 + 灰度可控。
# 9.1 云端路由表的三大能力
能力一:REDIRECT——老 URL 兼容。
{ "path": "/legacy/product/:id", "target": "REDIRECT:/product/:id?from=legacy" }
改版后老 URL 依然能工作——不需要客户端改代码。
能力二:A/B 分流。
{
"path": "/product/:id",
"target": "com.example.ProductActivity",
"abTest": {
"abKey": "product_v2_experiment",
"control": "com.example.ProductActivity",
"treatment": "com.example.ProductActivityV2"
}
}
命中 treatment 组的用户走 V2,其他人走 V1。服务端配一次,全端一致。
能力三:紧急降级。
新版页面上线后 crash 率突然升高,5 分钟内改配置——所有用户跳老版本,无需发版。
# 9.2 云端路由的三重防护
云端配置错一次影响全端——必须有防护:
防护一:客户端本地兜底。 客户端总是内置一份最新版路由表,云端拉不到就用本地——不能因为网络挂了整个 App 不能跳转。
防护二:灰度发布。 新路由表先给 1% 用户 → 观察 30 分钟 → 5% → 30 分钟 → 逐步 100%。任何时刻都能一键停止。
防护三:客户端二次校验。 云端下发的 target 类必须存在,参数类型必须匹配——校验失败自动 fallback 到本地版。
# 9.3 版本兼容问题
问题:老版本 App(v1.0)拉到了新版路由表(含 v1.5 才有的页面),跳转会崩。
解法:每条路由标记 minAppVersion:
{
"path": "/product/detail",
"target": "com.example.ProductActivityV2",
"minAppVersion": "1.5.0",
"fallback": "com.example.ProductActivityV1"
}
老版本 App 自动降级 到 fallback。
# 十、综合案例串讲:把 7 问全部回扣
# 10.1 逐条回答一开始的 7 问
| # | 疑问 | 答案(章节) |
|---|---|---|
| ① | 硬编码 URL 有什么问题? | §3:类型耦合、无编译检查、三端不一致 |
| ② | 端侧 vs 后端路由本质? | §4:一样的 4 件套(路由表/匹配/参数/拦截) |
| ③ | ARouter/React Router 内核? | §4 + §5:注解或配置文件构建路由表 |
| ④ | 参数怎么传? | §6.4:主键 Path、筛选 Query、复杂对象 ID 化 |
| ⑤ | 拦截器执行顺序? | §7.3:全局在前 + 优先级数字 + Fail-Fast |
| ⑥ | 动态下发怎么做? | §9:REDIRECT + A/B + minAppVersion + 灰度 |
| ⑦ | Deep Link / UL / App Link? | §8:HTTP URL + UL + App Link 三端通用 |
# 10.2 事故重演:如果当时用了本文方案
回到开头 "12 处漏改、GMV 掉 8%" 的事故:
若当时用了完整方案:
- 注解 + APT 生成路由表 → 所有
/product/*跳转集中在一处,改一次配置全端生效。 - 云端动态下发 + REDIRECT → 老 URL
/product/123自动改写为新 URL/product?id=123,客户端不需要改一行代码。 - 灰度发布 → 1% → 5% → 30% 各观察一天,异常立刻回滚。
- UL/App Link 统一 → iOS 微信分享链接直接进 App,不再白屏。
- 404 兜底页 + 参数校验拦截器 → 缺 id 直接进搜索页而不是崩溃。
结果:改版工期从 1 周降到 1 天,漏改从 12 处降到 0 处,GMV 无损。
# 10.3 一次跳转的"路由一生"完整时序
── T0: 用户点击首页"iPhone 商品卡片" ─────────────
UI 层触发: router.push("/product?id=123&from=home&sort=price")
── T0+1ms: 路由库解析 ─────────────────
Step 1: URL 解析
scheme: arca
path: /product
query: {id=123, from=home, sort=price}
Step 2: 路由匹配(Radix Tree)
path=/product → 命中路由 { target: "ProductDetailActivity" }
Step 3: 云端 override 检查
拉取最新路由表 → /product 未被覆盖 → 用本地路由
Step 4: minAppVersion 检查
当前 App v2.3 ≥ minVersion 1.5.0 ✓
── T0+3ms: 拦截器链执行 ─────────────
① 参数校验
必要参数 id 存在 ✓
id 是 Long 类型 ✓
② 鉴权拦截器
路由无 auth 要求 → 跳过
③ 埋点拦截器
上报点击事件: {from:home, sort:price, targetSku:123}
④ 灰度拦截器
A/B key product_v2_experiment → hash(userId) % 100 = 15
未命中 5% 实验组 → 走 V1
⑤ 白屏检测拦截器
ProductDetailActivity 类存在 ✓
── T0+8ms: 跳转 ────────────────────
构造 Intent + 塞参数
intent.putExtra("id", 123L)
intent.putExtra("from", "home")
intent.putExtra("sort", "price")
startActivity(intent)
── T0+15ms: ProductDetailActivity onCreate ─────
@Autowired 注解自动注入:
productId = 123L
from = "home"
sort = "price"
业务代码直接用,无需 intent.getXXExtra()
── 后续: 如果发现新版 V2 有问题 ─────
运营配置中心 → 关掉 product_v2_experiment
5 分钟内全端拉到新版路由表 → 所有用户跳 V1
无需发版
# 10.4 四条路由设计哲学
哲学一:URL 是"合同",不是"字符串"。 一旦对外发布,就是永久兼容的承诺。老 URL 永远要能跳(通过 REDIRECT)——用户 3 年前收藏的链接,今天还得能用。
哲学二:三端一致比"技术选型"更重要。 ARouter / React Router / SwiftRouter 底层不重要,重要的是它们对外表达的 URL 协议是一样的。分享链接是产品的血脉,跨端不一致就是"血栓"。
哲学三:拦截器是路由的灵魂,不是可选装饰。 参数校验、鉴权、埋点、灰度、降级——每一个都是业务实实在在的需求。没有拦截器的路由库就是加了糖的 startActivity,没意义。
哲学四:静态编译解决"对不对",动态下发解决"能不能改"。 二者不是二选一——核心路由静态化保证一定跳得到,业务路由动态化保证想改就改。这是路由系统的真正艺术。
# 10.5 路由设计速查表
| 决策点 | 首选 | 备选 | 禁忌 |
|---|---|---|---|
| 维护形态 | 注解 + APT + 部分动态 | 中心化配置文件 | 硬编码分散 |
| 匹配算法 | Radix Tree | 前缀 Trie | O(N) 顺序遍历(>100 路由) |
| 参数传递 | Path 主键 + Query 辅助 + ID 化复杂对象 | 全 Query | JSON 整对象塞 URL |
| 跨端跳转 | HTTP URL + UL + App Link | 自定义 Scheme(未装 fallback) | 只支持 Scheme |
| 拦截器 | 责任链 + 优先级 + Fail-Fast | 事件订阅 | 硬编码在业务里 |
| 动态下发 | 云端覆盖 + 本地兜底 + minAppVersion + 灰度 | 只覆盖临时页面 | 无本地兜底 |
| URL 改版 | REDIRECT 兼容老 URL | 只发版 | 直接改破坏 |
# 10.6 上线检查清单(20 项)
路由表
- [ ] 所有页面走路由库跳转,
grep -r startActivity结果 ≤ 5 处 - [ ] 三端 URL 协议一致(scheme/host/path 相同)
- [ ] 编译期能检查路径拼写错误
- [ ] 每个路由标注 minAppVersion
匹配与参数
- [ ] 参数化路径(
/product/:id)覆盖 SEO 场景 - [ ] Query 参数覆盖筛选/来源
- [ ] 复杂对象 ID 化传递
- [ ] 参数类型自动校验
拦截器
- [ ] 参数校验拦截器
- [ ] 鉴权拦截器(付费/敏感页)
- [ ] 埋点拦截器
- [ ] 灰度拦截器(关键页可 A/B)
- [ ] 404 兜底拦截器(未知 URL 不崩)
- [ ] 白屏检测(目标类不存在自动 fallback)
跨端
- [ ] Universal Links / App Links 配置齐全
- [ ] 未安装 App 引导下载
- [ ] 分享链接实测通过(微信 / QQ / 短信)
动态下发
- [ ] 云端路由表 + 本地兜底
- [ ] 灰度机制(1%→5%→100%)
- [ ] 一键回滚(<5 分钟)
- [ ] 老 URL REDIRECT 兼容
# 十一、写在最后
路由库是"看不见的功课"——做好了 5 年不用碰,做差了每次改版都是灾难。12 处漏改、GMV 掉 8% 的教训不是"程序员不细心",而是**"没有集中的路由表"**。
路由设计的三条底线:
- URL 是合同——发出去了,你就永远要能兑现。
- 三端一致比"技术选型"重要——路由协议规范化,用什么库都能一致。
- 静态保底 + 动态改写——核心路径不变,业务路径想改就改。
下次面对"我们要接入路由库"或"URL 改版"的需求时,希望你脑子里冒出的不是"改一遍 grep 搜到的所有地方",而是——"REDIRECT 一条云端配置搞定,明天灰度上线"。这,就是路由库设计的真正功力。