
一、我们想解决什么问题
1.1 生活场景引入
先来打个比方,帮你建立一个对路由的直观印象。
你现在住在一个公寓楼里,有一天你想从自己家去楼下超市买点东西。你有两条路可以走:一条是坐电梯直接到一楼出门,另一条是爬楼梯绕一大圈再出去。当然,正常人都会选坐电梯------更快、更舒服、更符合直觉。
路由(Router)在 App 里扮演的角色,就相当于这个"电梯":你告诉它"我要去超市",它就自动帮你规划好路线,带你到达目的地。你不需要关心电梯是怎么升降的、楼层是怎么切换的,你只需要说"我要去哪"就够了。
1.2 开发中的具体痛点
现在把镜头拉到实际的 App 开发场景。你在做一个 GitHub 客户端 App,用户的使用路径大概是这样的:
- 打开 App,看到仓库列表(首页)
- 点击某个仓库名字,跳转到仓库详情页
- 在详情页里点"Issues"标签,看到这个仓库的 Issue 列表
- 点某一条 Issue,跳转到 Issue 详情页
- 在 Issue 详情页里点"评论",看到所有评论
这个过程中,发生了多少次页面跳转?粗略数一下,至少 4 次。而每一次跳转,都面临同样的两个核心问题:
问题一:怎么跳? 用 push 还是 replace?要不要带动画?用户按返回键应该退到哪里?
问题二:带什么? 从列表页到详情页,要带哪些数据?仓库名字当然要带,star 数要不要?作者头像要不要?这些数据怎么传、怎么接、传错了怎么办?
如果你没把路由设计好,上面这些问题就会像地雷一样遍布你的代码。今天发现传参丢了,明天发现页面栈乱套了,后天发现按返回键直接退出了 App------这些问题单个看起来都不大,但累积起来会让你的 App 体验变得非常糟糕。
1.3 常见的错误思路
我见过一些新手在处理页面跳转时的几种错误思路,在这里列出来帮你避坑:
第一种,什么数据都用 AppStorage 全局存着。比如把仓库信息存到 AppStorage 里,跳转页面时什么都不带,等到了详情页再从 AppStorage 里取。听起来也能工作,但问题在于:AppStorage 是全局的,你的 App 里可能有几十个页面都在往 AppStorage 里写数据,时间久了你就不知道哪个页面改了哪个数据,而且 AppStorage 里的数据不会随着页面销毁而清理,容易造成内存泄漏。
第二种,用 URL 参数拼接字符串 。比如把参数拼成 pages/Detail?id=123&name=test 这样的字符串传过去,然后在详情页再自己解析。这不是不行,但对于复杂对象(比如一个包含几十个字段的仓库对象),URL 参数根本兜不住,而且解析的时候还容易出错。
第三种,分不清 Page 和 UIAbility。明明只是同一个应用内两个页面之间的跳转,非要用 startAbility 去启动一个新的 Ability,结果发现数据传递特别麻烦,生命周期也乱了。这就是对 HarmonyOS 应用架构理解不到位导致的。
看完这三个错误思路,你应该对"什么不该做"有了一些感觉。那么,正确的做法是什么呢?让我们继续往下看。
二、数据模型设计
2.1 为什么先设计数据模型
做任何功能之前,我强烈建议你先把数据结构定义清楚。这就像盖房子之前要先画图纸------你得知道你要盖的是一栋什么样的楼,有几层,每层干什么用,房间里放什么家具。这些都定了,施工的时候才不容易返工。
写代码也是一样的道理。如果你不提前定义好数据模型,一边写一边临时决定"这个字段叫什么、那个字段什么类型",最后代码就会变得非常散乱,改一个地方要同时改好几个文件,还容易漏掉。
2.2 仓库详情页的数据结构
对于我们的 GitHub 客户端,仓库详情页需要展示的数据大概有这些:
typescript
// 传递给仓库详情页的数据结构
export interface RepositoryDetailParams {
// 仓库全名,带所有者前缀,例如 "openharmony/docs"
repoFullName: string;
// 仓库描述,一句话介绍这个仓库是干嘛的
description: string;
// star 数量,反映项目热度
stargazersCount: number;
// fork 数量,反映项目被二次开发的程度
forksCount: number;
// 主要编程语言
language: string;
// 作者头像 URL
ownerAvatarUrl: string;
// 最后更新时间
updatedAt: string;
// 仓库是否私有(可选)
isPrivate?: boolean;
// Issues 总数(可选)
issuesCount?: number;
}
这个 interface 设计出来之后,列表页和详情页就有了一个共同的"契约":列表页知道我要把哪些数据给出去,详情页知道我要从哪个通道把数据接进来。两边对上了,参数传递就不会出问题。
2.3 为什么要用 interface 而不是直接传对象
你可能会问:我直接在 pushUrl 的时候把对象塞进去不行吗?为什么要先定义 interface?
好问题。直接塞对象当然能跑,但有两个隐患:
第一个是隐式依赖。你在列表页写了一个对象,过了两周,你想在详情页访问这个对象的某个字段,结果发现列表页当初根本就没传这个字段,代码就崩了。如果你用了 interface,TypeScript 编译器在编译阶段就能告诉你"你定义了这个字段但没传"或者"你访问了这个字段但 interface 里没有"。
第二个是重构成本高。假设你要给所有页面传递的数据加一个新字段,没有 interface 的话,你得在整个项目里搜索所有相关的 pushUrl 调用,手动加字段。有了 interface,你只需要在一个地方改,然后让 TypeScript 告诉你哪些地方需要同步更新。
三、核心设计决策
3.1 HarmonyOS 路由方案全景图
HarmonyOS 提供了多种页面导航手段,但它们并不是同一个东西,理解它们的层次关系很重要。我来给你梳理一下:
第一层:Page 内导航------在一个页面内部切换视图,比如 Tab 之间切换、展开折叠面板等。这类需求一般用 ArkUI 内置的组件(如 Tabs、Navigation)就能搞定,不需要 router 介入。
第二层:同一 UIAbility 内的 Page 跳转 ------这是最常见的情况,比如从首页跳到详情页、从设置页跳到账号管理页。这类需求用 router.pushUrl 或 router.replaceUrl 来处理,是最轻量、最简单的方式。
第三层:跨 UIAbility 跳转 ------比如从你的 App 跳到另一个 App 的某个页面,或者在同一个 App 的不同模块(UIAbility)之间跳转。这类需求要用 startAbility,涉及 Want 对象,复杂度更高。
3.2 pushUrl vs replaceUrl 详细对比
这是最容易选错的点,我把它掰开了揉碎了讲。
想象一下你在看一本书,你在第 50 页停下来夹了书签,然后翻到了第 80 页。这个时候你面前的书是第 80 页,但第 50 页的内容并没有消失,它就压在下面,你随时可以翻回去。pushUrl 就相当于这个动作------新页面叠在旧页面的上面。
现在换个场景:你把第 50 页撕掉了,然后把第 80 页粘到原来的位置。你面前还是一本书,但这本书只有 80 页了,第 50 页彻底没了。replaceUrl 就相当于这个动作------新页面把旧页面"吃掉"了。
| 特性 | pushUrl | replaceUrl |
|---|---|---|
| 页面栈变化 | 压栈,栈深度 +1 | 替换,栈深度不变 |
| 返回行为 | 按返回键回到上一页 | 按返回键回到上上一页 |
| 适用场景 | 正常有来有回的导航 | 流程终结点,如登录→主页 |
| 用户体验 | 符合直觉,不会"迷路" | 需谨慎用,防止用户"回不去" |
| 性能 | 几乎无差别 | 几乎无差别 |
什么时候用 pushUrl? 几乎所有正常的页面导航都用它。用户点进去看详情,看完了按返回键回来------这是天经地义的用户行为。
什么时候用 replaceUrl? 场景很有限,但很关键。比如:用户注册完成后跳到主页,此时如果用户按返回键,不应该让他回到注册页(因为注册流程已经结束了);再比如从列表页点了某个操作后进入编辑页,编辑完成后跳回列表页并刷新------这种情况下用 replace 把编辑页替换掉列表页,可以防止用户按返回键又回到编辑页的尴尬。
3.3 为什么优先选 router 而不是 startAbility
我用一个生活场景来解释这个区别。
router 就像是你们公司内部各部门之间的电话转接------你从销售部打电话到财务部,只要知道对方的分机号就能接通,非常方便,而且通话记录都在公司内部系统里,出了问题好查。
startAbility 更像是你从自己公司打电话到另一家公司------你得知道对方的公司名称、对方公司的具体部门,而且两家公司之间的通话规则不一样,有时候还需要对方公司那边确认才能接通,复杂多了。
所以,在大多数情况下,只要是在同一个 App 内部跳转,优先用 router。只有在真正需要跨 App 或者需要某个独立能力模块的时候,才考虑 startAbility。
四、完整代码实现
4.1 列表页:发起跳转
typescript
// RepositoryList.ets - 仓库列表页
import router from '@ohos.router';
import type { RepositoryDetailParams } from '../model/RepositoryModel';
@Entry
@Component
struct RepositoryList {
@State repositoryList: RepositoryDetailParams[] = [];
aboutToAppear() {
// 模拟加载仓库数据
this.repositoryList = [
{
repoFullName: 'openharmony/docs',
description: 'OpenHarmony 开发者文档',
stargazersCount: 2356,
forksCount: 892,
language: 'TypeScript',
ownerAvatarUrl: 'https://avatars.githubusercontent.com/u/xxx1',
updatedAt: '2024-12-01'
},
{
repoFullName: 'openharmony/kernel_linux',
description: 'Linux 内核适配层',
stargazersCount: 3847,
forksCount: 1102,
language: 'C',
ownerAvatarUrl: 'https://avatars.githubusercontent.com/u/xxx2',
updatedAt: '2024-11-28'
}
];
}
build() {
Column() {
// 顶部标题栏
Row() {
Text('🔍 搜索仓库')
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.padding(16)
// 仓库列表
List() {
ForEach(this.repositoryList, (repo: RepositoryDetailParams) => {
ListItem() {
this.RepoItem(repo)
}
.onClick(() => {
// 关键:pushUrl 的 params 字段就是你要传递的数据
router.pushUrl({
url: 'pages/RepositoryDetail',
params: repo
});
})
}, (repo: RepositoryDetailParams) => repo.repoFullName)
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
@Builder
RepoItem(repo: RepositoryDetailParams) {
Column() {
Text(repo.repoFullName)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#333')
Text(repo.description)
.fontSize(14)
.fontColor('#666')
.margin({ top: 4 })
Row() {
Text(`⭐ ${repo.stargazersCount}`)
.fontSize(12)
.fontColor('#888')
Text(` 🍴 ${repo.forksCount}`)
.fontSize(12)
.fontColor('#888')
Text(` 📝 ${repo.language}`)
.fontSize(12)
.fontColor('#888')
}
.margin({ top: 8 })
}
.width('100%')
.padding(16)
.backgroundColor('#FFF')
.borderRadius(8)
}
}
4.2 详情页:接收参数
typescript
// RepositoryDetail.ets - 仓库详情页
import router from '@ohos.router';
import type { RepositoryDetailParams } from '../model/RepositoryModel';
@Entry
@Component
struct RepositoryDetail {
// 从 router 获取参数,推荐方式
@State params: RepositoryDetailParams =
router.getParams() as RepositoryDetailParams;
// 如果你更习惯在生命周期里初始化,也可以这样做
@State repoFullName: string = '';
@State description: string = '';
@State stargazersCount: number = 0;
@State forksCount: number = 0;
@State language: string = '';
@State ownerAvatarUrl: string = '';
@State updatedAt: string = '';
aboutToAppear() {
// 接收跳转时传来的参数
const navParams = router.getParams();
if (navParams) {
const p = navParams as RepositoryDetailParams;
this.repoFullName = p.repoFullName;
this.description = p.description;
this.stargazersCount = p.stargazersCount;
this.forksCount = p.forksCount;
this.language = p.language;
this.ownerAvatarUrl = p.ownerAvatarUrl;
this.updatedAt = p.updatedAt;
}
}
build() {
Column() {
// 返回导航栏
Row() {
Button('← 返回')
.onClick(() => router.back())
}
.width('100%')
.padding(12)
Divider()
// 详情内容区
Column() {
Text(this.repoFullName)
.fontSize(26)
.fontWeight(FontWeight.Bold)
.fontColor('#1A1A1A')
Text(this.description)
.fontSize(16)
.fontColor('#555')
.margin({ top: 12 })
Divider().margin({ top: 16, bottom: 16 })
Row() {
Column() {
Text(`${this.stargazersCount}`).fontSize(22).fontWeight(FontWeight.Bold)
Text('Stars').fontSize(12).fontColor('#888')
}
.layoutWeight(1)
Column() {
Text(`${this.forksCount}`).fontSize(22).fontWeight(FontWeight.Bold)
Text('Forks').fontSize(12).fontColor('#888')
}
.layoutWeight(1)
Column() {
Text(this.language).fontSize(16).fontWeight(FontWeight.Bold)
Text('Language').fontSize(12).fontColor('#888')
}
.layoutWeight(1)
}
.width('100%')
.padding({ vertical: 16 })
Text(`最后更新: ${this.updatedAt}`)
.fontSize(13)
.fontColor('#999')
Row() {
Button('Watch').backgroundColor('#E8E8E8').fontColor('#333')
Button('Star').backgroundColor('#F0A500').fontColor('#FFF')
}
.margin({ top: 20 })
.width('80%')
}
.width('100%')
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor('#FFF')
}
}
4.3 路由配置:module.json5 中声明页面
json5
{
"module": {
"pages": "$profile:main_pages", // 指向页面路由配置文件
}
}
对应的 main_pages.json:
json5
{
"src": [
"pages/RepositoryList",
"pages/RepositoryDetail"
]
}
五、深度技术原理
5.1 页面栈到底是个什么东西
前面我好几次提到了"页面栈"这个词,但可能你还没有一个很清晰的概念。让我来把这个概念好好解释一下。
栈(Stack)是一种数据结构,特点是"后进先出"(Last In First Out)。你可以把它理解为一个叠起来的盘子------最后放上去的盘子,最先被拿下来。
在 HarmonyOS 的 router 里,这个"盘子"就是页面。当用户打开 App 时,栈里只有一个页面(首页)。用户点了某个列表项,pushUrl 把详情页"放到栈顶"。用户再点进去看 Issue 列表,pushUrl 把 Issue 列表页放到栈顶。
此时栈的结构是(从栈底到栈顶):[首页] → [详情页] → [Issue列表页]
用户按返回键,router.back() 把栈顶的 Issue 列表页弹出去,露出下面的详情页。再按一次,返回到首页。
这就是页面栈的工作原理。理解了这一点,你就知道:
- pushUrl 会增加栈深度:栈越来越深,返回的时候要按很多次。
- replaceUrl 不增加栈深度:新页面直接覆盖旧页面。
- 栈是有容量限制的:如果无限 push,栈会满。所以设计 App 时要合理规划导航层级。
5.2 参数传递的底层机制
你可能会好奇:pushUrl 的时候把一个对象塞进 params 参数里,这个对象是怎么到达目标页面的?中间经历了什么?
实际上,HarmonyOS 框架在底层做了这些事情:
发送端 :当你调用 router.pushUrl({ url, params }) 时,框架会把 params 对象序列化成一个内部的数据结构,然后和目标页面的 url 一起存入路由管理器的队列里。序列化就是把你写的 JavaScript/TypeScript 对象转换成一种可以存储和传输的格式,这个过程对你是透明的。
传输过程:这个序列化的数据和路由指令一起,被发往 HarmonyOS 的路由服务(Router Service)。这个服务是系统级别的,它负责协调所有应用内的页面跳转。
接收端 :目标页面被加载后,框架会把序列化数据反序列化(重新变回 JavaScript 对象),然后注入到目标页面的上下文中。你调用 router.getParams() 时,框架就从这个上下文里把数据取出来还给你。
整个过程你完全不需要写任何序列化、反序列化的代码------框架全包了。这就像你寄快递,快递公司帮你打包、运输、派送,你只需要把东西交给快递员就行,不需要自己开车送。
5.3 router.back 的进阶用法
router.back() 不仅仅能把用户带回上一页,它还有两个非常实用的参数:
typescript
// 带参数返回上一页
router.back({
url: 'pages/RepositoryList', // 可选:指定返回到哪个页面
params: { refreshed: true } // 可选:带回一些数据给上一个页面
});
这个功能在什么场景下有用呢?比如你从列表页跳到编辑页,编辑完成后保存并返回到列表页,同时希望列表页自动刷新显示最新数据。这种时候,你就可以用 router.back() 把一个 refreshed: true 带回列表页,列表页在 onPageShow 生命周期里检查这个参数,发现是 true 就去刷新数据。
5.4 UIAbility 与 Page 的关系
这是 HarmonyOS 区别于普通前端开发的一个重要概念。在 Android 开发里有 Activity,在 iOS 开发里有 ViewController,UIAbility 就是 HarmonyOS 里的这个"能力单元"。
一个 UIAbility 就像一个"车间",里面可以有多台"机器"(Page)。不同车间之间是相互独立的,有各自的进程入口和生命周期;同一个车间里的机器之间用 router 跳转,就像在一个工厂内部走动一样方便。
为什么要区分这个?因为有些场景确实需要不同的 UIAbility。比如你的 App 有一个"分享"功能,你想让用户在任意页面都能触发分享------这时候分享功能最好做成一个独立的 UIAbility,因为它不属于任何一个具体的页面,而是一个横跨整个 App 的能力。
再比如,你做一个 App 有"主应用"和"便携卡片"两个入口,这两个入口的业务逻辑完全不同,就可以分别用两个 UIAbility 来承载。
六、常见问题解答
Q1:router.pushUrl 跳转后,目标页面收不到参数怎么办?
这是新人最容易遇到的问题之一。按顺序检查以下三点:
第一步,打开浏览器 DevTools 的日志面板(或者用 hilog 打印日志),确认 pushUrl 调用时 params 确实有值。如果 push 时 params 本身就是空的,那传不过去很正常。
第二步,确认目标页面的 url 和你在 push 时写的 url 完全一致,包括大小写。HarmonyOS 的路由是严格匹配的,pages/RepositoryDetail 和 pages/repositoryDetail 是两个不同的路由。
第三步,确认目标页面确实在 main_pages.json 里注册了。如果页面没有注册,HarmonyOS 会直接报路由未找到的错误。
第四步,如果以上都没问题,检查 params 里传的对象字段名和你定义的 interface 是否完全一致。TypeScript 在编译时可能不报错(如果你用了 as 断言),但运行时拿到的是 undefined。
Q2:连续多次 pushUrl 后,页面栈越来越深,返回时要按很多次,有没有优雅的解决方案?
有两个思路。一个是从产品设计上优化:不要设计太深的导航层级,一般推荐不超过 3 层。如果你的 App 有超过 3 层的页面,那就要考虑是不是某些页面可以改成 modal 弹窗或者底部抽屉,而不是单独开一个页面。
另一个是从技术手段上处理:如果确实需要深度导航,可以在某些节点用 replaceUrl 替换掉中间的一些页面。比如从 Issue 列表到 Issue 详情再到某条评论详情,如果用户从评论详情返回后不需要回到 Issue 详情(因为那个 Issue 已经被记住了),可以用 replace 把评论详情替换掉 Issue 详情。
Q3:router.back() 能不能带回数据给上一个页面?
可以,但有条件。router.back() 支持两个可选参数,url 和 params。你可以在返回时通过 params 把数据带回上一个页面。上一个页面需要在 onPageShow 生命周期里通过 router.getParams() 接收这些数据。
不过要注意,如果用户按的是系统返回键(不是你自己放的返回按钮),这种 back 是没有机会注入参数的。所以如果你需要带参数回来,建议用明确的按钮触发 back,而不是依赖系统返回键。
Q4:replaceUrl 和 pushUrl 在性能上有差别吗?
几乎没有。两者调用的都是 HarmonyOS 路由框架的底层能力,开销相当。真正影响 App 性能的是页面的渲染速度和接口请求的快慢。选 push 还是 replace,应该完全基于你的业务逻辑和用户交互体验来决定,而不是考虑性能。
Q5:跳转到一个没有在路由表里注册的页面会怎样?
HarmonyOS 会抛出异常,App 会崩溃。所以每次新增页面时,除了写代码,还要记得同步更新 main_pages.json 路由表。可以把这个作为一个规范写进你们的项目 README 里:新增页面 = 写代码 + 注册路由。
Q6:AppStorage 和 router params 传参怎么选?
核心区别在于生命周期和作用范围:
- router params:临时性、单次传递,只在这一次跳转中有效,目标页面关闭后数据就消失了。
- AppStorage:全局共享,整个 App 生命周期内都存在,任何页面随时可以访问。
我的建议是:跳转时需要用到的数据用 router params,这样职责清晰、生命周期明确;全局共享的数据(如用户登录状态、主题配置)用 AppStorage。两者各司其职,不要混用。
七、运行效果
下面是用 ASCII 字符画展示的 App 运行效果,展示从仓库列表到仓库详情的完整跳转流程:

↓
[ 点击 openharmony/docs ]
↓

八、扩展方向
学完了基础路由,你已经可以在项目里正常做页面跳转和参数传递了。但路由的世界远不止于此,以下几个方向值得你继续探索:
8.1 路由守卫与登录拦截
在企业级应用里,大多数页面都需要用户先登录才能访问。传统的做法是在每个页面的 aboutToAppear 里写一段"检查登录状态"的逻辑------如果没有登录就跳转到登录页。
这种做法的问题是:代码重复,而且容易遗漏。
更好的方案是封装一个路由守卫(Route Guard)。你在路由层面统一做拦截,每次跳转之前先检查用户登录状态,状态过期就自动重定向到登录页,登录成功后再跳回用户原本想去的页面。这就好比公司大门装了门禁,不管你进哪栋楼,先刷一次卡就行,不用每栋楼门口再刷一次。
8.2 路由参数验证
当前我们的实现里,列表页给详情页传什么,详情页就接收什么,没有任何校验。生产环境里,这是个隐患------万一哪天后端接口改了,返回的数据结构变了,你的 App 可能就会在某个页面崩溃。
建议在详情页的 aboutToAppear 里加一层参数校验:用 TypeScript 的类型守卫或者一个简单的 validator 函数,检查 params 里每个必填字段是否存在、类型是否正确。如果校验不通过,至少给用户一个友好的提示(比如"数据加载失败,请重试"),而不是让 App 直接白屏崩溃。
8.3 集中式路由配置
随着 App 规模扩大,你的 main_pages.json 里可能会注册几十个页面,每个页面的 url 字符串散落在代码的各个角落。有一天你想改一个页面的路径(比如把 pages/RepositoryDetail 改成 pages/Repo/Detail),你就要在整个项目里搜索所有用到这个路径的地方,手动改。
解决这个问题的思路很简单:把所有的页面路径定义到一个常量文件里,用命名常量代替字符串字面量。比如:
typescript
// routes/RouteNames.ts
export const RouteNames = {
REPOSITORY_LIST: 'pages/RepositoryList',
REPOSITORY_DETAIL: 'pages/RepositoryDetail',
ISSUE_LIST: 'pages/IssueList',
ISSUE_DETAIL: 'pages/IssueDetail',
} as const;
// 跳转时
router.pushUrl({ url: RouteNames.REPOSITORY_DETAIL, params: repo });
以后要改路径,只需要在 RouteNames.ts 里改一处就行了。
8.4 页面转场动画
HarmonyOS 的 router 支持自定义页面切换的动画效果。如果你觉得默认的滑动动画太朴素,可以自定义:滑入方向、速度曲线、持续时间、背景模糊等。
比如,你想做一个从底部弹出的分享面板,就可以用 modal 配合自定义动画来实现,而不是用 router 跳转------因为分享面板本质上是当前页面的一个叠加视图,不是独立的页面。
再比如,你想让从列表到详情的转场更有电影感,可以用 hero 动画(Hero Animation):列表里的那张缩略图在跳转过程中"飞"到详情页的大图位置上,让用户有一种视觉连续性,体验非常丝滑。
8.5 深度链接(Deep Link)
如果你的 App 支持被外部链接唤醒,比如用户在浏览器里点击一个 GitHub 仓库链接,你的 App 就能直接打开并跳转到对应的仓库详情页------这就是深度链接。
实现深度链接需要配置 App 的 module.json5 中的 skills 字段,声明你的 App 响应哪些 URL 协议。当 App 被外部链接拉起时,会触发 UIAbility 的 onNewWant 生命周期回调,你可以在那里解析链接参数,然后跳转到对应页面。
这个功能做起来比普通路由复杂一些,但用户一旦用上就会觉得非常爽------点击链接直接进 App,比先复制链接、再切 App、再粘贴搜索这个流程快了不止一点点。
8.6 路由与状态管理的协同
当 App 页面多了之后,状态管理会变成一个头疼的问题。比如你在详情页 star 了一个仓库,然后返回到列表页------列表页怎么知道 star 状态变了?需要重新拉接口刷新吗?
这里就涉及到路由和状态管理的协同设计了。你可以:
- 用 AppStorage 或 GlobalState 统一管理"用户是否已 star 这个仓库",详情页和列表页都读同一个数据源。
- 在
router.back()的时候带上一个标志位,告诉列表页"数据有变化,需要刷新"。 - 用
@Link或@Watch装饰器监听状态变化,自动触发 UI 更新。
路由只是导航工具,真正的体验取决于它和状态管理的配合程度。