编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 序卷方法论

  • 数据的本质

  • 运行时模型

  • 并发的设计

  • 内存的真相

  • 交互和系统

    • README
    • 1.窗口核心设计思想
    • 2.视图加载渲染设计
    • 3.图形渲染管线原理
    • 4.手势事件设计灵魂
    • 5.消息机制设计思想
    • 6.跨进程的通信设计
    • 7.组件生命周期管理
    • 8.页面导航与路由设计
      • 目录
      • 0.路由事故
        • 0.1 案例代码
        • 0.2 五个埋雷点
        • 0.3 路由化修复
        • 0.4 本节启示
      • 1.为何需要路由
        • 1.1 手动跳转混乱
        • 1.2 三大根因
        • 1.3 路由设计目标
      • 2.路由五件套
        • 2.1 URI 标识
        • 2.2 路由栈
        • 2.3 参数传递
        • 2.4 拦截器守卫
        • 2.5 转场动画
        • 2.6 跨端名词矩阵
      • 3.Web 路由
        • 3.1 Hash vs History
        • 3.2 React Router
        • 3.3 Vue Router 守卫
        • 3.4 文件系统路由
      • 4.Android 导航
        • 4.1 Intent 跳转
        • 4.2 TaskAffinity launchMode
        • 4.3 Jetpack Navigation
        • 4.4 ARouter 组件化
      • 5.iOS 导航
        • 5.1 UINavigation 栈
        • 5.2 Modal/Push/Present
        • 5.3 URLRouter 协调器
        • 5.4 SwiftUI NavStack
      • 6.跨端路由
        • 6.1 Flutter Navigator
        • 6.2 go_router 声明式
        • 6.3 RN/Compose 导航
        • 6.4 小程序导航 API
      • 7.横向对比矩阵
        • 7.1 能力对齐表
        • 7.2 设计哲学站位
      • 8.反模式与陷阱
        • 8.1 硬编码跳转
        • 8.2 大对象传参
        • 8.3 栈污染
        • 8.4 深链未鉴权
        • 8.5 模块编译墙
      • 9.现代演进
        • 9.1 类型安全
        • 9.2 声明式路由
        • 9.3 深链三件套
        • 9.4 路由状态恢复
      • 10.心智与哲学
        • 10.1 跨端术语对照
        • 10.2 本卷章节呼应
        • 10.3 通用心智口诀
        • 最终升华
    • 9.响应式数据绑定设计
    • 10.国际化适配的设计
  • 内功
  • 交互和系统
杨充
2026-06-28
目录

8.页面导航与路由设计

# 8.页面导航与路由设计

📍 本篇位置:第 5 卷 · 交互与系统 · 第 8 篇(承接 5.1「窗口」、5.2「视图加载」、5.7「生命周期」——5.1 解决「容器从哪里来」,5.2 解决「视图怎么挂上去」,5.7 解决「容器怎么活怎么死」,5.8 解决「容器之间怎么跳、怎么回、怎么传参、怎么栈管理」) 🎯 核心矛盾:「业务想随心所欲地跳页面」 vs 「系统必须维护一个有限可回退的栈」——产品想要"从首页 → 详情 → 评论 → 个人主页 → 详情(同一个)→ 回退到首页"这种自由跳转;但系统受限于内存、可回退栈深度、URL 可分享性、深链可达性等约束。解法只有一个:把"页面跳转"抽象为「路由表 + 栈管理 + 参数传递 + 生命周期联动」的状态机——这就是路由(Router) 🧭 设计灵魂:路由 = 「URI + 栈 + 参数 + 拦截器 + 转场」五件套。任何导航框架(Web History API/React Router/Vue Router/Android Navigation/iOS UINavigationController/Flutter Navigator 2.0/小程序 wx.navigateTo)都在这五件套上做选择——理解了通用骨架,所有平台的路由都是同一棵树的不同枝叶 🌐 跨平台覆盖:Web (History/Hash/React Router/Vue Router/Next.js App Router) · Android (Intent/startActivity/Jetpack Navigation/ARouter/DeepLink) · iOS (UINavigationController/Push-Modal/URLRouter/SwiftUI NavigationStack) · 跨端 (Flutter Navigator 1.0/2.0/go_router · RN Stack Navigator · Compose Navigation · Taro) · 小程序 (wx.navigateTo / redirectTo / switchTab / reLaunch) · 鸿蒙 (Router/Navigation) 🔗 延伸阅读:← 5.1 窗口核心设计思想 (opens new window) · ← 5.2 视图加载渲染设计 (opens new window) · ← 5.7 组件生命周期管理 (opens new window) · → 5.9 响应式数据绑定设计 (opens new window) · ↔ 6.x 架构思想 · ↔ 4.x 内存的真相 💡 通用心智:忘掉 startActivity / pushViewController / history.push / Navigator.push / wx.navigateTo 这些具体 API,记住一句话——路由 = 「把一个『字符串 URI』映射成『一个页面 + 一组参数 + 一种转场动画 + 一个回退栈位置』的状态机」。Web 的 URL、Android 的 Intent、iOS 的 Push、Flutter 的 Route、小程序的 wx.navigate——本质都是把"我要去哪里"这件事标准化、可序列化、可分享、可回退、可拦截。核心理解:① 路由的本质是"栈"——不是树、不是图,是 LIFO 后进先出栈;② URI 是路由的"主键"——能 URI 化的页面才能深链、分享、恢复;③ 拦截器是路由的"权限网关"——登录、埋点、AB 测试全靠它——把这三点想明白,所有导航问题就有答案了。


# 目录

  • 0.路由事故
  • 1.为何需要路由
    • 1.1 手动跳转混乱
    • 1.2 三大根因
    • 1.3 路由设计目标
  • 2.路由五件套
    • 2.1 URI 标识
    • 2.2 路由栈
    • 2.3 参数传递
    • 2.4 拦截器守卫
    • 2.5 转场动画
    • 2.6 跨端名词矩阵
  • 3.Web 路由
    • 3.1 Hash vs History
    • 3.2 React Router
    • 3.3 Vue Router 守卫
    • 3.4 文件系统路由
  • 4.Android 导航
    • 4.1 Intent 跳转
    • 4.2 TaskAffinity launchMode
    • 4.3 Jetpack Navigation
    • 4.4 ARouter 组件化
  • 5.iOS 导航
    • 5.1 UINavigation 栈
    • 5.2 Modal/Push/Present
    • 5.3 URLRouter 协调器
    • 5.4 SwiftUI NavStack
  • 6.跨端路由
    • 6.1 Flutter Navigator
    • 6.2 go_router 声明式
    • 6.3 RN/Compose 导航
    • 6.4 小程序导航 API
  • 7.横向对比矩阵
    • 7.1 能力对齐表
    • 7.2 设计哲学站位
  • 8.反模式与陷阱
    • 8.1 硬编码跳转
    • 8.2 大对象传参
    • 8.3 栈污染
    • 8.4 深链未鉴权
    • 8.5 模块编译墙
  • 9.现代演进
    • 9.1 类型安全
    • 9.2 声明式路由
    • 9.3 深链三件套
    • 9.4 路由状态恢复
  • 10.心智与哲学
    • 10.1 跨端术语对照
    • 10.2 本卷章节呼应
    • 10.3 通用心智口诀

# 0.路由事故

场景:某 O2O App 上线分享功能后,运营反馈:「用户从微信点开分享链接 → App 启动 → 偶发性出现『进入了别人的订单详情页』、『未登录用户直接看到付款页』、『回退键直接退出 App』三连问题」。崩溃率没涨,但客诉单堆成山。

# 0.1 案例代码

// MainActivity 处理外部深链
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val uri = intent.data  // 假设是 myapp://order/detail?id=12345
    
    when (uri?.host) {
        "order" -> {
            val orderId = uri.getQueryParameter("id")
            // ❌ 直接跳转,未检查登录状态
            startActivity(Intent(this, OrderDetailActivity::class.java).apply {
                putExtra("id", orderId)
            })
        }
        "pay" -> {
            val amount = uri.getQueryParameter("amount")
            // ❌ 付款页直达,未做权限/订单归属校验
            startActivity(Intent(this, PayActivity::class.java).apply {
                putExtra("amount", amount)
            })
        }
    }
    finish()  // ❌ MainActivity 直接 finish,回退栈只剩目标页,按返回直接退出
}

// OrderDetailActivity 内部跳转
fun gotoComment(orderId: String) {
    // ❌ 字符串硬编码,没有统一的路由表
    startActivity(Intent(this, CommentActivity::class.java).apply {
        putExtra("orderId", orderId)
        // ❌ 通过 Intent 传了一个 1MB 的商品图列表
        putParcelableArrayListExtra("images", largeImageList)
    })
}

# 0.2 五个埋雷点

# 埋雷点 触发条件 后果
1 深链直达未做权限校验 攻击者构造 myapp://order/detail?id=他人订单ID 越权查看他人订单(横向越权 IDOR)
2 付款页未做归属校验 构造 myapp://pay?amount=0.01&orderId=他人订单 金额篡改 / 替他人付款
3 MainActivity 过早 finish 用户从外链进入 → 目标页 → 按返回键 直接退出 App(栈深度只剩 1,体验崩溃)
4 Intent 传大对象 Bundle 超过 1MB(TransactionTooLargeException) 跨进程 Binder 崩溃(5.6 跨进程通信痛点)
5 跳转字符串硬编码 改名 / 删除目标 Activity 编译期不报错,运行时 ClassNotFoundException

# 0.3 路由化修复

// 1. 定义路由表(单一事实源)
object Routes {
    const val ORDER_DETAIL = "/order/detail"
    const val PAY = "/pay"
    const val LOGIN = "/login"
}

// 2. 注册路由 + 拦截器
Router.register(Routes.ORDER_DETAIL, OrderDetailActivity::class.java)
    .interceptor(LoginInterceptor())        // 必须登录
    .interceptor(OwnershipInterceptor())    // 必须是订单归属人

Router.register(Routes.PAY, PayActivity::class.java)
    .interceptor(LoginInterceptor())
    .interceptor(OrderOwnershipInterceptor())  // 必须是订单归属人
    .interceptor(AmountValidInterceptor())     // 金额必须等于订单金额

// 3. 深链入口统一交给路由
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val uri = intent.data
    if (uri != null) {
        // 不 finish MainActivity,让路由把"首页"放到栈底
        Router.with(this)
            .uri(uri)
            .ensureHomeInStack()  // ★ 保证首页在栈底,回退不会退出 App
            .navigate()
    }
}

// 4. 内部跳转用路由 + 参数 Builder
Router.with(this)
    .path(Routes.COMMENT)
    .withString("orderId", orderId)
    .withImages(largeImageList, transferMode = MMAP)  // ★ 大对象走 MMAP 不走 Bundle
    .navigate()

# 0.4 本节启示

  • 路由不是"跳页面",是"系统级状态机"——它要同时管理 URI 解析、栈深度、参数传递、权限拦截、转场动画 5 件事
  • 5 个埋雷点对应 5 个设计契约:权限网关 = 拦截器、栈深度 = ensureHomeInStack、大参数 = 不走 Bundle、硬编码 = 路由表、归属校验 = 业务拦截器
  • 本章要回答:① 为什么需要路由(§1)② 路由的通用契约是什么(§2)③ 各端怎么实现的(§3-§6)④ 反模式与现代演进(§8-§9)

# 1.为何需要路由

# 1.1 手动跳转混乱

想象一个没有路由的 App,所有页面跳转都靠"直接 new + 入栈":

// 直接 new 一个页面对象
val detail = OrderDetailActivity(orderId)
windowManager.push(detail)

听起来很简洁,但马上会面对四个无解的问题:

问题 具体表现
解耦失败 模块 A 想跳模块 B,必须 import B——模块化彻底失败,编译时间爆炸
深链无门 微信分享一条 myapp://order/detail?id=123,App 无法把字符串 URI 映射回页面对象
回退栈混乱 用户从首页 → A → B → A(同一个 A)→ 回退几次能到首页?没人说得清
跨进程不可达 从浏览器拉起 App、从 Push 通知拉起 App、从 Widget 拉起 App——没有 URI 就无法描述目标

结论:只要 App 不是只有一个页面,就必须有路由。路由 = "把『去哪里』这件事从『代码调用』升级为『字符串协议』"。

# 1.2 三大根因

为什么所有平台、所有框架最终都演化出了"路由系统"?归根到底是三个根本需求:

# 根因 1:模块解耦(Decoupling)

  • 现象:组件化 / 微前端 / 插件化时代,模块之间不能互相 import
  • 解法:模块只发布 URI(/order/detail),不发布类名(OrderDetailActivity),通过路由中心做映射
  • 典型代表:ARouter(Android)、Module Federation(Web)、Coordinator(iOS)

# 根因 2:栈管理(Stack Management)

  • 现象:用户的"返回"操作必须有可预期的语义,不能让 App 莫名其妙退出
  • 解法:路由系统维护一棵或一棵以上的"页面栈",规定入栈、出栈、清栈、替换的语义
  • 典型代表:Android Task / Back Stack、iOS UINavigationController、小程序页面栈(最多 10 层)

# 根因 3:深链可达(Deep Link)

  • 现象:从 App 外(微信、浏览器、Push、Widget、二维码)必须能直达 App 内任意页面
  • 解法:每个页面都有一个全局唯一的 URI,外部链接 → URI 解析 → 路由匹配 → 页面打开
  • 典型代表:Web URL、Android App Links、iOS Universal Links、Flutter 的 Navigator 2.0

# 1.3 路由设计目标

任何合格的路由系统都必须同时实现这 7 个目标——少一个都不够格:

# 目标 含义 反例
1 URI 化 任何页面都有唯一字符串标识 字符串硬编码 OrderDetailActivity 跳转
2 可序列化 路由状态可保存到 SP/disk/进程间 页面对象本身不可序列化
3 可拦截 跳转前可注入登录/埋点/AB 测试 在每个 Activity 的 onCreate 手写 if (!login) finish
4 可回退 系统返回键有明确语义 按返回直接退出 App
5 可传参 参数有类型约束(不只是 String) Bundle 传大对象导致 TransactionTooLarge
6 可降级 目标页面不存在时有 fallback 直接 ClassNotFoundException 崩溃
7 可定制转场 动画 / 共享元素 / 模态 所有跳转都是同一种系统动画

这 7 个目标,就是 §2 五件套契约的现实需求。


# 2.路由五件套

本节是全篇的"骨架"——抽象出所有端通用的五件套契约,后面 §3-§6 的所有平台实现,都是这五件套在不同端的具体投影。

# 2.1 URI 标识

URI = "页面的主键",必须做到:

scheme://host/path?query#fragment

myapp://order/detail?id=12345&from=push
https://yccoding.com/pages/f54c32/
/pages/order/detail?id=12345
维度 设计要点
scheme 区分应用边界:https(Web)、myapp://(私有协议)、内部路由用 /
host 模块名 / 子系统名(电商 / 社交 / 支付)
path 具体页面(/detail、/list、/edit)
query 参数(短小、可见、可分享)
fragment 锚点 / 页面内位置(Web 专属)

URI 的本质:把"跳哪里"从"运行时引用"提升为"编译时字符串契约"——只要双方约定好字符串协议,模块之间就完全解耦。

# 2.2 路由栈

栈是路由的核心数据结构。所有平台的"返回键"语义,本质都是 stack.pop():

栈顶 ─→  PayActivity        ← 当前可见
        OrderDetailActivity
        OrderListActivity
栈底 ─→  HomeActivity       ← 按返回最终回到这里

栈的五种操作语义(跨端通用术语):

操作 语义 Android iOS Web Flutter 小程序
push 入栈一个新页面 startActivity pushViewController history.pushState Navigator.push navigateTo
pop 弹出栈顶 finish/back popViewController history.back Navigator.pop navigateBack
replace 替换栈顶(不入栈、不可回退到原页面) finish+start setViewControllers history.replaceState pushReplacement redirectTo
clearTop 清除栈顶到目标页之间的所有页面 FLAG_CLEAR_TOP popTo —— popUntil ——
reLaunch 清空整个栈,重新作为唯一页面 NEW_TASK rootViewController history.replaceAll pushAndRemove reLaunch

栈管理的三个铁律:

  1. 栈深度有上限(小程序 10 层、iOS 实际可超但体验差、Android Back Stack 受任务亲和性影响)
  2. 同一页面可重复入栈——这是 90% 的"栈污染"根因(详见 §8.3)
  3. 栈状态必须可序列化——进程死亡后要能从 SP / SavedInstanceState 重建

# 2.3 参数传递

参数传递的三种范式:

范式 适用场景 局限
URI Query < 200 字符的基础类型 只能 String、容易暴露
Bundle/Object 中等结构体、可 Parcelable/Serializable Android 上限 1MB(Binder 限制,5.6 章详述)、iOS 无强限制但建议 < 4MB
外部存储 + Key 大对象(图片列表、文件、富文本) 跳转方 put、目标方 get、销毁方 delete——需要生命周期管理

参数设计的两个铁律:

  1. 能 URI 化的就别用 Bundle——URI 可分享、可恢复、可埋点、可降级
  2. 大对象(> 100KB)绝对不走 Bundle——用 MMAP / 临时文件 / EventBus 中转
// ❌ 错误:通过 Bundle 传 List<Bitmap>
intent.putParcelableArrayListExtra("bitmaps", bitmaps)  // TransactionTooLargeException

// ✅ 正确:先存到全局缓存,传 key
val key = ImageCache.put(bitmaps)
intent.putExtra("imagesKey", key)
// 目标页:
val bitmaps = ImageCache.get(intent.getStringExtra("imagesKey"))

# 2.4 拦截器守卫

拦截器是路由系统的"权限网关"——所有横切关注点(登录、埋点、AB 测试、降级)都在这里处理:

拦截器责任链:

外部 URI ─→ [全局拦截器] ─→ [模块拦截器] ─→ [页面拦截器] ─→ 目标页面
              ↓                  ↓                ↓
          埋点/AB           登录/会员           归属/金额

拦截器的四种典型职责:

职责 典型实现
登录态 LoginInterceptor:未登录 → 跳到登录页,登录后回到原路由
权限 PermissionInterceptor:检查角色/会员等级
埋点 TrackInterceptor:记录"从哪来、到哪去、什么参数"
降级 FallbackInterceptor:目标页不存在时跳兜底页

Web/Vue Router 的"路由守卫"是同一个东西:

router.beforeEach((to, from, next) => {
    if (to.meta.requireAuth && !isLoggedIn()) {
        next('/login?redirect=' + to.fullPath)
    } else {
        next()
    }
})

# 2.5 转场动画

转场是用户对"页面切换"的唯一感官输入,必须可定制:

转场类型 含义 场景
Push/Pop 横向推进/回退 同一层级的前进后退
Modal/Present 纵向弹出/收起 独立任务(编辑、设置)
Fade/Cross 淡入淡出 启动页 → 首页
Shared Element 共享元素过渡 列表 → 详情的图片放大
Custom 完全自定义(如 PIP、Hero、3D 翻转) 视频小窗、特殊动效

转场动画的设计哲学:动画不只是"好看",它是"信息层级"的可视化——横向推进表示"同一任务的下一步",纵向弹出表示"独立的子任务"。混淆它们会让用户失去方向感。

# 2.6 跨端名词矩阵

概念 Android (Activity) Android (Navigation) iOS (UIKit) iOS (SwiftUI) Web (React Router) Flutter 小程序
URI Intent + URI NavDeepLink URL Scheme URL path RouteSettings path
栈 Task + Back Stack NavController UINavigationController NavigationStack History Navigator 页面栈
push startActivity navigate pushVC navigationDestination Link / navigate Navigator.push navigateTo
pop finish popBackStack popVC dismiss navigate(-1) Navigator.pop navigateBack
replace finish + start popUpTo + navigate setViewControllers replace replace pushReplacement redirectTo
拦截器 (无原生) NavController.addListener 自定义中间件 自定义中间件 beforeEach (Vue) / loaders (RR v6) RouteGuard App.onLaunch
转场 overridePendingTransition NavOptions.anim UIViewControllerAnimatedTransitioning NavigationTransition CSS Transition PageRouteBuilder 系统默认

记住一个观察:所有端的路由系统都在"五件套契约"上做选择——URI、栈、参数、拦截器、转场——只是叫法不同。理解了这点,再学任何新框架(鸿蒙 Router、Compose Multiplatform、Server Components)都能 5 分钟上手。


# 3.Web 路由

Web 路由是所有平台路由的"祖师爷"——URL 天生就是 URI,浏览器天生就是状态机。后来 SPA 时代,前端把"路由"从浏览器手里抢了过来,自己做栈管理、参数解析、拦截守卫——本质是"在客户端重新发明了一遍服务端路由"。

# 3.1 Hash vs History

SPA 出现之前,"路由"是服务端的事——浏览器请求 /order/detail?id=123,服务端返回完整 HTML。SPA 时代,前端要在不刷新页面的前提下切换"视图"——这就有了两种方案:

# 方案 1:Hash 路由(# 后面的部分)

https://example.com/#/order/detail?id=123
  • 原理:URL 中 # 后面的部分(fragment)不会触发浏览器刷新,但变化时会触发 hashchange 事件
  • 优点:兼容性好(IE8+)、无需服务端配合
  • 缺点:URL 难看(多个 #)、SEO 不友好(搜索引擎默认忽略 hash)
// 监听 hash 变化
window.addEventListener('hashchange', () => {
    const path = location.hash.slice(1)  // 去掉 #
    render(path)
})
// 跳转
location.hash = '/order/detail?id=123'

# 方案 2:History 路由(HTML5 History API)

https://example.com/order/detail?id=123
  • 原理:用 history.pushState() / replaceState() 修改 URL 但不触发刷新
  • 优点:URL 干净、SEO 友好
  • 缺点:必须服务端配合——刷新 /order/detail 时服务端要 fallback 到 index.html,否则 404
// pushState 不触发刷新,但会在历史栈中加一条记录
history.pushState({ id: 123 }, '订单详情', '/order/detail?id=123')

// 监听浏览器前进后退
window.addEventListener('popstate', (event) => {
    const state = event.state  // 之前 pushState 时存的对象
    render(location.pathname, state)
})

对比矩阵:

维度 Hash 路由 History 路由
URL 形式 /#/order /order
服务端配合 不需要 需要(fallback to index)
SEO 不友好 友好
兼容性 IE8+ IE10+
状态携带 只能 URL URL + state 对象
现代推荐 内嵌 H5、不可控环境 独立 SPA、官网应用

# 3.2 React Router

React Router 是 SPA 路由的"事实标准",从 v4 开始全面拥抱声明式路由:

# 声明式:路由就是一棵组件树

// React Router v6
<BrowserRouter>
  <Routes>
    <Route path="/" element={<Layout />}>
      <Route index element={<Home />} />
      <Route path="order" element={<OrderLayout />}>
        <Route path="list" element={<OrderList />} />
        <Route path="detail/:id" element={<OrderDetail />} />
      </Route>
      <Route path="login" element={<Login />} />
      <Route path="*" element={<NotFound />} />  {/* 兜底 */}
    </Route>
  </Routes>
</BrowserRouter>

三个核心特性:

特性 体现
嵌套路由 <Outlet /> 占位符让父路由决定子路由渲染位置——天然实现"母版页 + 内容区"
路径参数 :id 自动解析为 useParams().id
动态 import lazy() + Suspense 实现路由级代码分割(首屏加速)

# 命令式:useNavigate

function ProductCard({ id }) {
    const navigate = useNavigate()
    return (
        <div onClick={() => navigate(`/order/detail/${id}`)}>
            ...
        </div>
    )
}

# v6.4+ 的 Loaders:数据获取与路由绑定

const router = createBrowserRouter([
    {
        path: "/order/detail/:id",
        element: <OrderDetail />,
        loader: async ({ params }) => {
            // ★ 路由匹配后、组件渲染前先取数据
            const res = await fetch(`/api/order/${params.id}`)
            if (!res.ok) throw new Response("Not Found", { status: 404 })
            return res.json()
        },
        errorElement: <ErrorPage />,
    }
])

function OrderDetail() {
    const order = useLoaderData()  // ★ loader 的返回值
    return <div>{order.title}</div>
}

Loader 模式的颠覆点:把"数据获取"从组件 useEffect 里搬到了"路由层"——这意味着:① 避免瀑布流请求 ② 天然支持 SSR ③ 路由切换前就能确定数据状态。

# 3.3 Vue Router 守卫

Vue Router 的设计哲学和 React Router 类似,但路由守卫(Guard)做得最完整——这是它最值得借鉴的部分:

# 三层守卫

const router = createRouter({
    history: createWebHistory(),
    routes: [
        {
            path: '/order/detail/:id',
            component: OrderDetail,
            meta: { requireAuth: true },
            // ★ 路由级守卫
            beforeEnter: (to, from) => {
                if (!checkOwnership(to.params.id)) return '/403'
            }
        }
    ]
})

// ★ 全局前置守卫(登录校验、埋点)
router.beforeEach((to, from) => {
    if (to.meta.requireAuth && !isLoggedIn()) {
        return { path: '/login', query: { redirect: to.fullPath } }
    }
})

// ★ 全局后置守卫(埋点、标题)
router.afterEach((to, from) => {
    document.title = to.meta.title
    track('page_view', { from: from.path, to: to.path })
})

// 组件内守卫
export default {
    beforeRouteEnter(to, from, next) { /* 进入前 */ },
    beforeRouteUpdate(to, from) { /* 同组件路径变化 */ },
    beforeRouteLeave(to, from) { /* 离开前——常用于"未保存提示" */ }
}

守卫的执行顺序(13 步):

导航触发 → 失活组件 beforeRouteLeave → 全局 beforeEach → 重用组件 beforeRouteUpdate
        → 路由配置 beforeEnter → 解析异步组件 → 激活组件 beforeRouteEnter
        → 全局 beforeResolve → 导航确认 → 全局 afterEach → DOM 更新
        → 调用 beforeRouteEnter 的 next 回调

记住一句话:守卫就是"路由的中间件"——所有横切关注点(登录、权限、埋点、未保存提示)都应该在守卫里处理,不要污染业务组件。

# 3.4 文件系统路由

2023 年后 Web 路由的最大演进:"文件系统即路由表"——文件路径自动映射为 URL:

app/
├── page.tsx                    →  /
├── login/page.tsx              →  /login
├── order/
│   ├── layout.tsx              →  (嵌套布局)
│   ├── list/page.tsx           →  /order/list
│   └── detail/[id]/page.tsx    →  /order/detail/:id
└── api/
    └── order/[id]/route.ts     →  GET /api/order/:id

四个革命性特性(Next.js App Router):

特性 含义
文件即路由 不再手写 routes 数组,文件路径就是 URL——天然"自解释"
layout.tsx 同目录自动共享布局,类似嵌套路由的 Outlet
loading.tsx 路由级 Loading UI(基于 Suspense)
Server Components 路由可以是 RSC——数据获取直接在路由级完成,零客户端 JS

这是路由演进的终局形态:URL = 文件系统路径 = 数据源 = 组件树——四者统一,开发者不需要再"维护路由表"了。


# 4.Android 导航

Android 的导航演进是一部"渐进式架构升级史"——从最早的 Activity + Intent,到 Fragment 时代的混乱,到 Jetpack Navigation 的官方收编,再到组件化时代的 ARouter/TheRouter。每一步都在解决前一阶段暴露的问题。

# 4.1 Intent 跳转

Intent 是 Android 的"通用通信总线"——它不只是跳页面,还能跳服务、广播、跨应用:

# 显式 Intent:类名直达

val intent = Intent(this, OrderDetailActivity::class.java).apply {
    putExtra("id", 12345)
}
startActivity(intent)

特点:编译期绑定、跨模块需要 compile project(":order")——这就是组件化的"编译墙"。

# 隐式 Intent:通过 Action / Data / Category 匹配

<!-- AndroidManifest.xml 注册过滤器 -->
<activity android:name=".OrderDetailActivity">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="myapp" android:host="order" android:pathPrefix="/detail" />
        <!-- App Links:用真实域名做深链 -->
        <data android:scheme="https" android:host="example.com" android:pathPrefix="/order/detail" />
    </intent-filter>
</activity>
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("myapp://order/detail?id=123"))
startActivity(intent)

隐式 Intent 的本质:Android 系统级的"URI → Activity"路由表——所有 App 共享这张表(PackageManagerService 维护)。这就是 Android 的"系统路由"。

# 4.2 TaskAffinity launchMode

Activity 不是简单进栈,而是按 Task 和 launchMode 组织:

Task A (taskAffinity = com.example.app)
├── MainActivity   (standard)
├── OrderList      (standard)
└── OrderDetail    (singleTop)

四种 launchMode:

launchMode 行为 典型场景
standard 每次都创建新实例 默认(详情页等)
singleTop 如果当前栈顶就是它,不创建新实例(onNewIntent) 搜索结果页、通知点击
singleTask 全局只有一个实例,已存在则把它上面的全部 pop 首页、登录页
singleInstance 独占一个 Task,整个系统只有一个实例 拨号器、闹钟

栈污染的根因:90% 的"按返回退不出去"都是因为 standard 模式 + 不正确的 Intent Flag 组合——例如把同一个详情页推 N 次。

配合 Intent Flags:

intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)    // 清除栈顶到目标页之间的所有
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)     // 新开 Task
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)   // 等同 singleTop
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)   // 清空目标 Task(配合 NEW_TASK 用)

# 4.3 Jetpack Navigation

Android 团队 2018 年推出 Jetpack Navigation,目的:消灭多 Activity 时代的栈混乱,回归单 Activity + 多 Fragment。

# 核心三件套

<!-- nav_graph.xml:路由图(声明式) -->
<navigation xmlns:app="..." app:startDestination="@id/home">
    <fragment android:id="@+id/home" android:name=".HomeFragment">
        <action android:id="@+id/to_order_list" 
                app:destination="@id/order_list" />
    </fragment>
    
    <fragment android:id="@+id/order_list" android:name=".OrderListFragment">
        <action android:id="@+id/to_order_detail" 
                app:destination="@id/order_detail" />
    </fragment>
    
    <fragment android:id="@+id/order_detail" android:name=".OrderDetailFragment">
        <!-- ★ 类型安全的参数 -->
        <argument android:name="orderId" app:argType="string" />
        <!-- ★ 深链 -->
        <deepLink app:uri="myapp://order/detail?id={orderId}" />
    </fragment>
</navigation>
// Activity 只放一个 NavHostFragment
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        setContentView(R.layout.activity_main)
        // 处理深链
        navController.handleDeepLink(intent)
    }
}

// Fragment 内跳转:类型安全
class OrderListFragment : Fragment() {
    fun gotoDetail(orderId: String) {
        val action = OrderListFragmentDirections.toOrderDetail(orderId)
        findNavController().navigate(action)
    }
}

四个革命性收益:

收益 解决了什么
可视化路由图 路由关系一眼可见——告别"全工程 grep startActivity"
Safe Args 编译期生成参数类——告别 getString("id")?: "" 这种字符串 key
统一深链 <deepLink> 标签 + handleDeepLink——告别 manifest 写一堆 filter
回退栈可控 popUpTo / popUpToInclusive 在 XML 里声明——告别 Flag 组合记忆

# 4.4 ARouter 组件化

Jetpack Navigation 解决了"单 Activity 内部"的导航,但组件化场景下"模块 A 跳模块 B"还是不能 import——这就是 ARouter(阿里)、TheRouter(字节)等第三方路由的舞台。

# 核心设计:注解 + APT 自动生成路由表

// 模块 B 声明
@Route(path = "/order/detail")
class OrderDetailActivity : AppCompatActivity() {
    @Autowired var orderId: String = ""  // 自动注入
}

// 模块 A 跳转(不需要 import 模块 B)
ARouter.getInstance()
    .build("/order/detail")
    .withString("orderId", "12345")
    .navigation()

# 完整的拦截器体系

@Interceptor(priority = 1)
class LoginInterceptor : IInterceptor {
    override fun process(postcard: Postcard, callback: InterceptorCallback) {
        if (postcard.path == "/login" || UserManager.isLoggedIn()) {
            callback.onContinue(postcard)
        } else {
            // 跳登录页,带 redirect
            ARouter.getInstance().build("/login")
                .withString("redirect", postcard.path)
                .navigation()
            callback.onInterrupt(null)  // 中断原路由
        }
    }
}

# 设计核心

  • APT 编译期生成路由表:每个模块独立编译为 Routes$$App_OrderModule.java,应用启动时合并——这是"组件化"的关键
  • 跨模块解耦:模块 A 只依赖 /order/detail 字符串,不依赖 OrderDetailActivity.class
  • 服务发现:除了页面路由,还能注册"服务接口"——这是模块化 IPC 的另一种形态(5.6 跨进程通信的"App 内 IPC 版")

对比矩阵:

维度 Intent Jetpack Navigation ARouter
路由表 manifest 注册 nav_graph.xml 注解 + APT 自动生成
类型安全 ❌ ✅ Safe Args ⚠️ @Autowired 半自动
跨模块跳转 需要 import 不支持(设计为单 Activity) ✅ 字符串解耦
拦截器 ❌ ⚠️ listener ✅ 完整责任链
深链 manifest filter <deepLink> @Route(path) 即 URI
转场动画 overridePending NavOptions withTransition
适用场景 跨 App / 系统级 单 App 单 Activity 大型组件化 App

# 5.iOS 导航

iOS 导航的精髓是"栈即视图"——UINavigationController 本身就是一个可视化的栈容器,pushVC 时屏幕从右边推入,popVC 时从左边滑出,栈的"数据结构"和"UI 视觉"是统一的。这种"所见即栈"的设计哲学,被后来的 SwiftUI NavigationStack 完整继承。

# 5.1 UINavigation 栈

let nav = UINavigationController(rootViewController: HomeVC())

// Push 入栈
nav.pushViewController(OrderListVC(), animated: true)
nav.pushViewController(OrderDetailVC(orderId: "123"), animated: true)

// 栈状态
print(nav.viewControllers)  // [HomeVC, OrderListVC, OrderDetailVC]

// Pop 出栈
nav.popViewController(animated: true)              // 弹出栈顶
nav.popToRootViewController(animated: true)        // 回到根
nav.popToViewController(orderListVC, animated: true)  // 回到指定 VC

// Replace:直接重置整个栈
nav.setViewControllers([HomeVC(), NewVC()], animated: true)

栈模型的三个特性:

  1. viewControllers 是开放的数组——可以读、可以改、可以重排(Android 的 BackStack 是封闭的)
  2. navigationItem 是 VC 自己的属性——每个 VC 自带导航栏配置,无需父级配合
  3. interactivePopGestureRecognizer——iOS 原生右滑返回手势(5.4 手势事件章详述)

# 5.2 Modal/Push/Present

iOS 的页面切换有三种层次完全不同的范式,混用是最常见的设计灾难:

范式 API 视觉 语义 栈关系
Push pushViewController 横向滑入 同一任务的"下一步" 加入当前导航栈
Present present(_:animated:) 从下往上弹出 独立的子任务(编辑、设置、登录) 开启新栈
Show show(_:sender:) 自适应(Push 或 Modal) Adaptive:在 iPad/Mac 自动调整 由 containerVC 决定

典型混用错误:

// ❌ 反模式:在 Modal 弹出的 VC 里做 push
let editVC = EditVC()
self.present(editVC, animated: true)  // editVC 没有 navigationController!
editVC.navigationController?.pushViewController(...)  // ⚠️ nil,跳转失效

// ✅ 正确:present 时包一层 NavigationController
let editVC = EditVC()
let nav = UINavigationController(rootViewController: editVC)
self.present(nav, animated: true)  // 现在 editVC.navigationController 不为 nil

记住一句话:Push 是"同一任务的延续",Present 是"独立任务的开始"——前者共享栈,后者开启新栈。

# 5.3 URLRouter 协调器

iOS 原生没有"路由表"的概念,所以社区演化出两套主流方案:

# 方案 1:URLRouter(类似 ARouter)

// 注册
Router.register("myapp://order/detail") { params in
    let vc = OrderDetailVC()
    vc.orderId = params["id"] as? String ?? ""
    return vc
}

// 调用
Router.open("myapp://order/detail?id=123")

代表库:JLRoutes、URLNavigator、SwiftRouter。

# 方案 2:Coordinator 模式(更"iOS 原生"的方案)

Coordinator 模式的核心:VC 不知道"自己之后跳哪",由 Coordinator(协调器)决定——这就把"导航逻辑"从 VC 里剥离出来:

protocol Coordinator {
    var navigationController: UINavigationController { get }
    func start()
}

class OrderCoordinator: Coordinator {
    let navigationController: UINavigationController
    
    init(nav: UINavigationController) {
        self.navigationController = nav
    }
    
    func start() {
        let listVC = OrderListVC()
        listVC.onSelectOrder = { [weak self] orderId in
            // ★ VC 只发"我选中了订单"事件,不知道之后去哪
            self?.showDetail(orderId: orderId)
        }
        navigationController.pushViewController(listVC, animated: true)
    }
    
    private func showDetail(orderId: String) {
        let detailVC = OrderDetailVC(orderId: orderId)
        detailVC.onCheckout = { [weak self] in
            self?.showPayment(orderId: orderId)  // ★ 决定下一步
        }
        navigationController.pushViewController(detailVC, animated: true)
    }
    
    private func showPayment(orderId: String) {
        let payVC = PayVC(orderId: orderId)
        // ★ 支付完成后的去向也由 Coordinator 决定
        navigationController.pushViewController(payVC, animated: true)
    }
}

Coordinator 的核心收益:

  • VC 不耦合"导航逻辑":可独立测试、可复用到不同流程
  • 流程可视化:每个业务流程(下单流、注册流)就是一个 Coordinator
  • 嵌套 Coordinator:父 Coordinator 启动子 Coordinator,形成树状导航图

# 5.4 SwiftUI NavStack

SwiftUI 在 iOS 16+ 引入 NavigationStack——这是 iOS 路由设计的"声明式革命":

struct ContentView: View {
    @State private var path = NavigationPath()  // ★ 路径就是数据
    
    var body: some View {
        NavigationStack(path: $path) {
            HomeView()
                .navigationDestination(for: Order.self) { order in
                    OrderDetailView(order: order)
                }
                .navigationDestination(for: User.self) { user in
                    UserProfileView(user: user)
                }
        }
    }
    
    // 跳转:直接操作数据
    func gotoOrder(_ order: Order) {
        path.append(order)  // 数据 push → UI push
    }
    
    func popToRoot() {
        path = NavigationPath()  // 清空数据 → 清空 UI 栈
    }
}

三个革命性特性:

特性 含义
数据驱动路由 path 是 @State,栈的状态就是普通数据——可保存、可恢复、可反序列化
类型安全分发 navigationDestination(for: Order.self)——按类型自动分发到目标视图
深链原生支持 NavigationPath 实现了 Codable——直接 JSON 序列化/反序列化

对比 UIKit 的 UINavigationController:

UIKit:           栈 = UIViewController 数组(对象)
SwiftUI:         栈 = NavigationPath(值类型 / Codable 数据)
                      ↑
                      这是质变——数据驱动 UI,而不是反过来

这就是路由设计的终极形态之一:"栈状态" = "可序列化数据"——天然支持深链、状态恢复、URL 同步。Flutter Navigator 2.0(§6.1)走的是同一条路。


# 6.跨端路由

跨端框架的路由设计有一个共同的演进方向:从"命令式 push/pop"走向"声明式 state-driven"——Flutter Navigator 1.0 → 2.0、React Native v4 → v5、小程序的页面栈管理——本质都是同一场革命。

# 6.1 Flutter Navigator

# Navigator 1.0:命令式

// 跳转
Navigator.push(context, MaterialPageRoute(
    builder: (_) => OrderDetailPage(orderId: '123')
));

// 回退
Navigator.pop(context);

// 命名路由
Navigator.pushNamed(context, '/order/detail', arguments: '123');

问题:

  • 栈状态由 Navigator 内部管理,外部读不到完整栈
  • 深链处理麻烦:需要在 onGenerateRoute 里手写解析
  • Web 端 URL 和 App 端栈无法同步

# Navigator 2.0:声明式(2020 年推出)

class AppRouterDelegate extends RouterDelegate<AppRoutePath> {
  // ★ 整个栈是一个"值"
  AppRoutePath _currentPath = AppRoutePath.home();
  
  @override
  Widget build(BuildContext context) {
    return Navigator(
      pages: [
        MaterialPage(child: HomePage()),
        if (_currentPath.isOrderList) MaterialPage(child: OrderListPage()),
        if (_currentPath.isOrderDetail) 
          MaterialPage(child: OrderDetailPage(id: _currentPath.orderId)),
      ],
      onPopPage: (route, result) {
        if (!route.didPop(result)) return false;
        // ★ pop 时更新数据
        _currentPath = AppRoutePath.home();
        notifyListeners();
        return true;
      },
    );
  }
  
  // ★ 从 URL 解析路径
  @override
  Future<void> setNewRoutePath(AppRoutePath path) async {
    _currentPath = path;
  }
}

Navigator 2.0 的三个革命:

革命 1.0 2.0
栈表达 不可见,内部数组 由 pages 列表完全描述(可见、可改)
URL 同步 手动 双向自动同步(RouteInformationParser)
状态可序列化 不行 路径对象本身就是数据,可保存/恢复

坦白说:Navigator 2.0 写起来非常啰嗦——所以社区出现了 go_router。

# 6.2 go_router 声明式

go_router 是 Flutter 团队推荐的"高级 API"——封装 Navigator 2.0 的复杂性,提供类似 React Router 的声明式 API:

final router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(path: '/', builder: (ctx, state) => HomePage()),
    GoRoute(
      path: '/order',
      builder: (ctx, state) => OrderListPage(),
      routes: [
        GoRoute(
          path: 'detail/:id',  // ★ 路径参数
          builder: (ctx, state) {
            final id = state.pathParameters['id']!;
            return OrderDetailPage(id: id);
          },
        ),
      ],
    ),
    GoRoute(path: '/login', builder: (ctx, state) => LoginPage()),
  ],
  // ★ 全局拦截器(路由守卫)
  redirect: (ctx, state) {
    final isLoggedIn = AuthService.isLoggedIn;
    final isLoggingIn = state.matchedLocation == '/login';
    if (!isLoggedIn && !isLoggingIn) {
      return '/login?redirect=${state.matchedLocation}';
    }
    return null;
  },
);

// 跳转
context.go('/order/detail/123');
context.push('/order/detail/123');  // push 不替换栈顶

go_router 把 Flutter 路由拉回了"易用区"——保留了 Navigator 2.0 的所有优势(声明式、URL 同步、可恢复),同时 API 简单得像 React Router。

# 6.3 RN/Compose 导航

# React Native:React Navigation v6

const Stack = createNativeStackNavigator()

function App() {
    return (
        <NavigationContainer linking={{
            prefixes: ['myapp://', 'https://example.com'],
            config: {
                screens: {
                    Home: '',
                    OrderDetail: 'order/detail/:id',
                }
            }
        }}>
            <Stack.Navigator>
                <Stack.Screen name="Home" component={HomeScreen} />
                <Stack.Screen 
                    name="OrderDetail" 
                    component={OrderDetailScreen}
                    options={{ animation: 'slide_from_right' }}
                />
            </Stack.Navigator>
        </NavigationContainer>
    )
}

// 跳转
navigation.navigate('OrderDetail', { id: '123' })
navigation.goBack()

特点:

  • 多种 Navigator 组合:Stack(栈式)、Tab(底部 Tab)、Drawer(抽屉)——可嵌套
  • 底层用各端原生导航:iOS 用 UINavigationController、Android 用 Fragment——所以转场和原生一致
  • linking 配置即深链:一处配置,App Links / Universal Links 同时启用

# Jetpack Compose Navigation

Android 的 Compose 版本与 Jetpack Navigation 一脉相承,但 API 更"现代":

val navController = rememberNavController()

NavHost(navController, startDestination = "home") {
    composable("home") { HomeScreen(navController) }
    composable(
        "order/detail/{id}",
        arguments = listOf(navArgument("id") { type = NavType.StringType }),
        deepLinks = listOf(navDeepLink { uriPattern = "myapp://order/detail/{id}" })
    ) { backStackEntry ->
        val id = backStackEntry.arguments?.getString("id")!!
        OrderDetailScreen(id)
    }
}

// 跳转
navController.navigate("order/detail/123")
navController.popBackStack()

Compose Navigation 把 XML(nav_graph)改成了 DSL——本质相同,写法更现代。

# 6.4 小程序导航 API

小程序的导航 API 看似简单,但 90% 的小程序 Bug 都跟这四个 API 混用有关:

API 行为 栈变化 典型场景
navigateTo 跳转到新页面 push(最多 10 层) 详情页、子页面
redirectTo 关闭当前页跳转 replace 登录成功 → 首页
switchTab 跳转到 Tab 页 清空非 Tab 栈 跳到首页 Tab
reLaunch 关闭所有页面,打开到新页 清空整个栈 退出登录、深链入口
navigateBack 返回 N 层 pop(N) 多层流程后跳回

四种 API 的"决策树":

我要去哪?
├── 一个新的"子页面",按返回能回到当前 → navigateTo
├── 替换当前页(流程页转换) → redirectTo
├── 一个 Tab 页 → switchTab
├── 清空所有,重新开始 → reLaunch
└── 回到 N 层之前 → navigateBack(delta)

核心约束:页面栈深度最多 10 层(微信小程序硬约束)——超过会"路由失败"。所以长流程必须用 redirectTo 替换,而不是层层 navigateTo。

// ❌ 错误:注册流走了 5 步 navigateTo,加上原本 5 层,第 6 步炸了
wx.navigateTo({ url: '/pages/register/step1' })
// ... 用户走到 step5,栈深 10,再 navigateTo 失败

// ✅ 正确:流程页之间用 redirectTo
wx.redirectTo({ url: '/pages/register/step2' })  // 替换 step1

鸿蒙 Router 的设计和小程序高度相似(pushUrl / replaceUrl / clear / back),可以认为是"小程序 + 类型安全"的进化版。


# 7.横向对比矩阵

# 7.1 能力对齐表

维度 Web (React Router) Android (Navigation) iOS (UIKit) iOS (SwiftUI) Flutter (go_router) RN (React Navigation) 小程序
URI 形式 /order/detail/:id order/detail/{id} URL Scheme NavigationPath /order/detail/:id screen + params pages/xx/xx
栈数据结构 History stack NavController viewControllers [] NavigationPath pages [] StackNavigator 页面栈(最多10层)
声明式 ✅ Routes 组件 ✅ nav_graph.xml ❌ 命令式 ✅ NavigationStack ✅ GoRoute 列表 ✅ Stack.Screen ⚠️ app.json 配置
类型安全 ⚠️ TS 加持 ✅ Safe Args ❌ ✅ navigationDestination ⚠️ 字符串路径 ⚠️ TS 类型 ❌
路由守卫 ✅ loaders + guards ⚠️ listener ❌(Coordinator 自实现) ❌(手动) ✅ redirect ⚠️ listener ⚠️ App 级钩子
深链 ✅ 天然 URL ✅ <deepLink> ✅ URL Scheme + Universal ✅ Codable Path ✅ uriPattern ✅ linking 配置 ✅ URL Scheme
转场动画 ✅ CSS 完全可控 ✅ NavOptions.anim ✅ 自定义 transition ⚠️ 系统默认+部分自定义 ✅ pageBuilder ✅ animation 配置 ⚠️ 系统默认
栈状态可恢复 ✅ 浏览器历史 ✅ SavedStateHandle ⚠️ 状态恢复 API ✅ Codable ✅ RouteInformation ✅ persistKey ⚠️ enterOptions

# 7.2 设计哲学站位

                  「命令式 push/pop」
                          ▲
              UIKit ●     │     ● Navigator 1.0
                          │
   小程序 ●              │              ● Intent
                          │
   ─────────────────────────────────────── 老派 vs 现代
                          │
              ARouter ●  │  ● React Router v5
                          │
            ● Jetpack Navigation
                          │
            ● Compose Navigation
                          │
                          │  ● go_router / React Router v6 loaders
                          │
         SwiftUI NavStack ●    ● Next.js App Router
                          │
                          ▼
                  「声明式 state-driven」

观察:

  • 底部:所有现代框架都在向"声明式 + 数据驱动"汇聚——栈是数据,UI 是渲染
  • 顶部:传统命令式 API 仍在大量存在(旧项目维护成本)——理解它们也是必修课
  • 左侧:Android 阵营走"声明式 XML/DSL"
  • 右侧:Web/SwiftUI 阵营走"声明式 + 类型安全"
  • 未来终点:URL = 文件系统 = 数据源 = 组件树(Next.js App Router)

# 8.反模式与陷阱

本节是"血泪汇总"——每一个反模式都对应一个真实事故。

# 8.1 硬编码跳转

症状:

// 工程里 200 处这种代码
startActivity(Intent(this, OrderDetailActivity::class.java).apply {
    putExtra("id", id)
    putExtra("from", "list")
})

问题:

  • 改一个类名要全工程修改——OrderDetailActivity 改名 OrderDetailActivityV2,编译期不报错(字符串 key),运行时全崩溃
  • 跨模块不能跳——模块 A 想跳模块 B,必须 import B
  • 无法做埋点 / AB 测试 / 拦截——分散在各处,无切面

修复:建立全局路由表(§4.4 ARouter / §3.2 React Router routes 数组)。

# 8.2 大对象传参

症状:

intent.putParcelableArrayListExtra("products", productList)  // 5000 个 Product 对象

触发条件:

  • Android Binder 单次事务上限 1MB(实际 < 800KB 才安全)
  • 超过抛 TransactionTooLargeException,Crashlytics 后台才看得到

修复:

数据大小 方案
< 200 字符 URI Query 参数
< 100KB Bundle / Intent Extras
100KB ~ 10MB 中转容器(单例缓存 + key)
> 10MB 临时文件 + 文件路径(参考 5.6 IPC)
// ✅ 中转容器方案
object DataBus {
    private val cache = mutableMapOf<String, Any>()
    fun put(value: Any): String {
        val key = UUID.randomUUID().toString()
        cache[key] = value
        return key
    }
    fun <T> take(key: String): T? = cache.remove(key) as? T  // ★ 一次性,自动释放
}

// 跳转方
val key = DataBus.put(productList)
intent.putExtra("dataKey", key)

// 目标方
val products: List<Product>? = DataBus.take(intent.getStringExtra("dataKey")!!)

# 8.3 栈污染

症状 1:同页面重复入栈

Home → Detail(id=1) → User → Detail(id=2) → User → Detail(id=3) → ...
栈深度爆炸,按返回十几次都退不出去

修复:根据业务语义选择 launchMode 或 popUpTo:

// Jetpack Navigation:跳转时清掉栈顶到指定页之间的所有
findNavController().navigate(
    R.id.detail,
    args,
    navOptions {
        popUpTo(R.id.home) { inclusive = false }  // 保留 home,清掉中间所有
    }
)

症状 2:死循环跳转

LoginGuard 检查:未登录 → 跳 Login 页
Login 页 onCreate 检查:已登录 → 跳 Home → LoginGuard → ... 死循环

修复:拦截器一定要有"豁免页面"列表:

class LoginInterceptor : IInterceptor {
    private val exemptPaths = setOf("/login", "/register", "/forgot")
    override fun process(postcard: Postcard, callback: InterceptorCallback) {
        if (postcard.path in exemptPaths || UserManager.isLoggedIn()) {
            callback.onContinue(postcard)
        } else {
            // ... 跳登录
        }
    }
}

# 8.4 深链未鉴权

症状(参考 §0 案例):

攻击者构造:myapp://order/detail?id=别人的订单ID
→ 直接进入详情页,看到他人订单

这是 IDOR(不安全的直接对象引用)漏洞——OWASP Top 10 的高危项。

修复:所有"敏感页面"必须在路由拦截器里做归属/权限校验:

@Interceptor(priority = 10)
class OwnershipInterceptor : IInterceptor {
    override fun process(postcard: Postcard, callback: InterceptorCallback) {
        when (postcard.path) {
            "/order/detail", "/pay" -> {
                val orderId = postcard.extras.getString("id")
                if (orderId != null && !OrderRepo.belongsToCurrentUser(orderId)) {
                    // ★ 拒绝,跳到 403 页
                    ARouter.getInstance().build("/403").navigation()
                    callback.onInterrupt(null)
                    return
                }
            }
        }
        callback.onContinue(postcard)
    }
}

铁律:任何 URI 参数都视为"用户可控输入"——必须服务端二次校验,绝不可信客户端的"已登录用户 = 已授权"。

# 8.5 模块编译墙

症状:

:app
├── :order (依赖 :pay, :user)
├── :pay (依赖 :order, :user)   ⚠️ 循环依赖
└── :user

为什么这是反模式:

  • 改 :user 的任何代码,:order 和 :pay 都要重新编译——单次构建 5 分钟
  • 单元测试要拉起整个依赖链
  • 无法做"独立模块发布"(业务台、SDK 化)

修复:模块化路由(§4.4)

:app
├── :router-api  ← 只有接口和路由协议
├── :order  → depends on :router-api
├── :pay    → depends on :router-api
└── :user   → depends on :router-api

模块之间不再互相 import,全部通过 :router-api 的字符串路由协议交互

这就是为什么"路由系统"是组件化的基石——没有路由,组件化就是伪命题。


# 9.现代演进

2020 年之后,路由设计有四个共同演进方向:类型安全、声明式、深链统一、状态可恢复。这四个方向不是孤立的——它们最终汇聚到同一个终极目标:让"路由"成为应用状态的一部分,可序列化、可推导、可测试。

# 9.1 类型安全

老派写法:

intent.putExtra("id", "12345")           // key 拼错了?运行时才知道
intent.putExtra("amount", 100)            // 类型不对?运行时才知道
intent.getStringExtra("ID")               // 大小写错了?返回 null,NPE

现代写法 1:Kotlin Sealed Class + Type-Safe Args

// Compose Navigation 2.8+ 的 Type-Safe Routes
@Serializable
sealed class Route {
    @Serializable data object Home : Route()
    @Serializable data class OrderDetail(val id: String, val from: String = "list") : Route()
    @Serializable data class Pay(val orderId: String, val amount: Long) : Route()
}

NavHost(navController, startDestination = Route.Home) {
    composable<Route.Home> { HomeScreen() }
    composable<Route.OrderDetail> { entry ->
        val route: Route.OrderDetail = entry.toRoute()
        OrderDetailScreen(route.id, route.from)
    }
}

// ★ 跳转——参数和类型完全编译期校验
navController.navigate(Route.OrderDetail(id = "12345"))

现代写法 2:TypeScript Discriminated Union

type Route = 
  | { kind: 'home' }
  | { kind: 'orderDetail'; id: string; from?: 'list' | 'push' }
  | { kind: 'pay'; orderId: string; amount: number }

function navigate(route: Route) { /* ... */ }

navigate({ kind: 'orderDetail', id: '123' })  // ★ TS 编译期校验
navigate({ kind: 'orderDetail' })  // ❌ 编译失败:缺少 id

类型安全的本质:把"路由错误"从"运行时崩溃"提前到"编译期报错"——这是工程化的核心收益。

# 9.2 声明式路由

老派的"命令式路由":

状态变化 ──→ 调用 push/pop ──→ 栈变化 ──→ UI 渲染
(开发者要保证 状态 和 栈 同步——经常忘记)

现代的"声明式路由":

状态变化 ──→ UI 自动渲染(栈是状态的一部分)
URL 变化 ──→ 状态变化 ──→ UI 自动渲染
(栈、UI、URL 三者由数据驱动,自动同步)

典型实现:

框架 声明式 API
React Router v6 <Routes><Route> JSX 树 + useNavigate
SwiftUI NavigationStack(path: $path) + navigationDestination
Flutter pages: [...] + Navigator 2.0 / go_router
Compose NavHost { composable() }
Next.js App Router 文件系统路径 = URL

记住一句话:声明式路由 = "栈是数据的投影,不是开发者手动操作的副产品"。这一步走通了,深链、状态恢复、URL 同步都是自动获得的副产品。

# 9.3 深链三件套

深链是"路由系统的对外接口"——让 App 内的页面可以被 App 外的链接直达:

类型 平台 触发方式 优劣
URL Scheme iOS/Android myapp://order/detail ⚠️ 可被其他 App 抢注、无 fallback、微信屏蔽
Universal Link iOS https://example.com/order ✅ 真实域名 + apple-app-site-association 文件,未装 App 走 H5
App Links Android https://example.com/order ✅ assetlinks.json 验证,autoVerify=true 可独占
Intent Scheme Android intent://...#Intent;...end 浏览器拉起 App,配合 fallback URL

最佳实践组合:

分享链接:https://example.com/order/detail?id=123
                            │
            ┌───────────────┴───────────────┐
        装了 App                       未装 App
            │                              │
    Universal Link / App Links         在浏览器打开 H5 页
            │                              │
    直接打开 OrderDetailVC          引导下载/扫码

深链的安全铁律(呼应 §8.4):

  1. 深链参数必须二次校验——别相信 URL 里的 userId、amount
  2. 敏感页面必须经过登录拦截器——直达付款页 = 灾难
  3. 保留"首页"在回退栈底——别让用户从深链进来后按返回直接退出 App

# 9.4 路由状态恢复

呼应 5.7 章「组件生命周期」的进程死亡——Android/iOS 在内存吃紧时会回收后台应用进程,恢复时要能"无感"重建用户当前所在页面。

Web 的天然优势:URL = 状态。用户从 /order/detail?id=123 离开,回来时 URL 还在,刷新就重建。

App 端需要主动设计:

// Android:SavedStateHandle + Compose Navigation
@Composable
fun OrderDetailScreen(
    viewModel: OrderDetailViewModel = hiltViewModel()
) {
    // ★ SavedStateHandle 自动保存 NavArgs,进程恢复自动重建
    val orderId = viewModel.orderId  // 从 SavedStateHandle 拿
    // ...
}
// iOS:SceneDelegate 持久化栈
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
        // ★ 把当前栈编码为 NSUserActivity
        let activity = NSUserActivity(activityType: "com.example.routePath")
        activity.userInfo = ["path": currentNavigationPath.encoded]
        return activity
    }
    
    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        // ★ 进程恢复时重建栈
        if let pathData = userActivity.userInfo?["path"] as? Data {
            navigationPath = NavigationPath.decode(pathData)
        }
    }
}

这就是"声明式路由"最大的现代收益——栈即数据,数据即可序列化,可序列化即可恢复。


# 10.心智与哲学

# 10.1 跨端术语对照

概念 命令式术语 声明式术语 本质
路由表 散落的 startActivity Routes / nav_graph / GoRouter 字符串 URI → 页面的映射
栈 viewControllers / Task path / pages / history LIFO 数据结构(可序列化)
跳转 push / startActivity navigate(to) / state update 修改"栈数据",UI 自动响应
回退 pop / finish / back navigate(-1) / pop state 弹出栈顶,触发回调
守卫 自定义检查代码 beforeEach / redirect / loader 拦截器责任链
深链 URL Scheme + manifest uriPattern / linking config 外部字符串 → 内部路由的映射
状态恢复 SavedInstanceState URL / Codable path 栈数据持久化 + 重建
转场 overridePendingTransition NavOptions.anim / Transition 栈状态变化的可视化呈现

# 10.2 本卷章节呼应

路由不是一个孤立的设计——它贯穿了整本第 5 卷:

章节 与路由的关系
5.1 窗口设计 路由的"载体"——每个 Activity / VC / Window 都是路由栈中的一个节点
5.2 视图加载 路由跳转的"内部动作"——push 即触发新 VC 的 viewDidLoad / onCreateView
5.3 图形渲染 转场动画的实现层——CALayer / Compositor 负责"两屏过渡"的合成
5.4 手势事件 路由的"输入入口"——返回手势、抽屉手势都是路由的触发器
5.5 消息机制 路由内部依赖——所有跳转都通过主线程消息队列调度
5.6 跨进程通信 跨 App 跳转的底层——Intent 走 Binder、URL Scheme 走 PackageManagerService
5.7 生命周期 路由的"时间维度"——push/pop 触发新页生命周期开始、旧页生命周期暂停/结束
5.9 响应式数据 路由参数 → ViewModel → UI 的数据流入口
5.10 数据加密 深链参数的安全防护——签名、加密、防篡改

核心观察:路由是第 5 卷所有篇章的"连接器"——它把窗口、视图、生命周期、手势、消息、IPC 全部串起来,让用户的"点一下"变成"换一个世界"。

# 10.3 通用心智口诀

记住这七句,所有平台的路由设计都能融会贯通:

  1. 路由 = 字符串 URI + 栈 + 参数 + 拦截器 + 转场——五件套缺一不可
  2. 栈是 LIFO 数据结构,不是树、不是图——所有"返回键"都是 pop
  3. 能 URI 化的就别用 Bundle——URI 可分享、可恢复、可拦截
  4. 大对象(>100KB)绝不走 Intent/Bundle——用 MMAP / 临时文件 / 中转容器
  5. 拦截器是权限网关——登录、归属、埋点、AB 全在这里
  6. 声明式路由是终局——栈即数据,数据驱动 UI,自动同步 URL
  7. 深链参数永远不可信——必须二次校验,IDOR 是高频漏洞

# 最终升华

路由的本质不是"跳页面",而是「把『用户当前所在何处』这件事,从『内存中的对象引用』提升为『可序列化的字符串协议』,让 App 的每一个角落都可被分享、被恢复、被拦截、被埋点」。

  • 2010 年代:Activity + Intent,开发者手写跳转代码
  • 2015 年代:ARouter / React Router,路由表登场,模块解耦
  • 2020 年代:Jetpack Navigation / SwiftUI NavigationStack / go_router,声明式革命,栈即数据
  • 2025 年代:Server Components + 文件系统路由,URL = 文件 = 数据 = 组件

走过这一段历史,回头看就懂了——所有路由框架都在做同一件事:让"位置"成为一等公民。位置是数据,位置可序列化,位置可拦截,位置可恢复——这就是路由的设计哲学,也是现代应用架构的灵魂。


本篇结——下一篇 5.9 响应式数据绑定设计 (opens new window) 我们将看到,当"位置"变成数据之后,"数据"本身又如何反过来驱动 UI——这是 SwiftUI/React/Vue/Compose 共同走向的"声明式终局"。

上次更新: 2026/07/15, 11:23:11
7.组件生命周期管理
9.响应式数据绑定设计

← 7.组件生命周期管理 9.响应式数据绑定设计→

最近更新
01
audit
07-27
02
C++入门教程全章思考题汇编
07-24
03
12.技术团队建设能力
07-21
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式