HarmonyOS知识库——导航架构与路由传参的设计模式
知识库 App 有 6 个页面、4 个底部导航 tab、十几条路由跳转路径。页面之间的跳转不是随意的——每条路由都有明确的参数设计,参数的类型和内容决定了目标页面的行为。搞清楚"从哪来、传什么、怎么用",是理解整个 App 数据流的关键。
完整效果
一、路由参数的完整清单
| 源页面 | 目标页面 | 参数 | 参数类型 |
|---|---|---|---|
| Index(笔记本卡片) | NoteList | notebookId, notebookName | number, string |
| Index(底部导航"搜索") | NoteList | notebookId=-1, notebookName=‘全部笔记’ | number, string |
| Index("+"按钮) | NoteEditor | 无 | — |
| Index(笔记卡片) | NoteEditor | noteId | number |
| Index(标签点击) | TagFilterPage | tag | string |
| Index(底部导航"收藏") | FavoritesPage | 无 | — |
| Index(底部导航"我的") | ProfilePage | 无 | — |
| NoteList(笔记卡片) | NoteEditor | noteId | number |
| FavoritesPage(笔记卡片) | NoteEditor | noteId | number |
| ProfilePage(标签云点击) | TagFilterPage | tag | string |
| TagFilterPage(标签点击) | TagFilterPage | tag | string(同页面切换) |
11 条路由路径,4 种参数类型:
| 参数 | 类型 | 出现次数 | 用途 |
|---|---|---|---|
| notebookId | number | 2 次 | 按笔记本筛选笔记 |
| notebookName | string | 2 次 | NoteList 标题显示 |
| noteId | number | 3 次 | 编辑指定笔记 |
| tag | string | 3 次 | 按标签筛选笔记 |
二、三种参数传递模式
模式一:双参数关联传递
params: { 'notebookId': nb.id, 'notebookName': nb.name } as Record<string,Object>

同时传 id 和名称——id 用于查询,名称用于显示。 NoteList 收到后用 notebookId 查询笔记列表,用 notebookName 设置导航栏标题。
为什么不只传 id? NoteList 需要显示笔记本名称作为标题。如果只传 id,NoteList 需要自己查 NOTEBOOKS 获取名称——多一次查询。双参数传递让目标页面直接拿到需要的数据。
踩坑记录: 最初 Index 跳转 NoteList 只传了 notebookId,NoteList 标题显示为空。查了原因发现没传 notebookName。加了双参数后解决。
模式二:单参数定向传递
// 编辑笔记
params: { 'noteId': n.id } as Record<string,Object>
// 标签筛选
params: { 'tag': t.name } as Record<string,Object>

只传一个参数——目标页面用这个参数做查询或设置状态。 NoteEditor 用 noteId 查询笔记详情,TagFilterPage 用 tag 筛选笔记。
为什么不需要传标题或名称? 因为目标页面可以从参数查询到完整数据——NoteEditor 通过 noteId 查到笔记的所有字段,TagFilterPage 通过 tag 查到所有匹配笔记。
模式三:特殊值约定
// "搜索" tab 跳转 NoteList
params: { 'notebookId': -1, 'notebookName': '全部笔记' } as Record<string,Object>
notebookId=-1 是约定值——表示"不按笔记本筛选,显示全部笔记"。 NoteList 收到 -1 后调用 getNotes() 而不是 getNotesByNotebook(-1)。
特殊值约定的风险: 如果未来 NOTEBOOKS 增加了 id=-1 的笔记本,逻辑就会出错。更安全的做法是不传 notebookId 参数,NoteList 判断参数不存在时显示全部笔记。但在原型阶段,约定值更简单。
三、NoteEditor 的双模式路由
// 新建模式
params: {} as Record<string,Object>
// 编辑模式
params: { 'noteId': n.id } as Record<string,Object>
有没有 noteId 参数决定了新建还是编辑。 NoteEditor 在 aboutToAppear 里判断:
if (p && p['noteId']) {
this.note = getNoteById(p['noteId'] as number)
if (this.note) { this.isNew = false; /* 预填数据 */ }
}
有 noteId → 编辑模式 → 预填标题/内容/笔记本/标签
无 noteId → 新建模式 → 所有字段为空
两种模式共享同一个页面——减少代码重复。 新建和编辑的 UI 结构一样(标题输入+内容输入+笔记本选择+标签管理),只是数据来源不同。用 @State isNew 控制差异部分。
新建和编辑的路由参数对比
| 维度 | 新建 | 编辑 |
|---|---|---|
| 参数 | 空对象 {} |
{ noteId: number } |
| NoteEditor 判断 | if (p && p['noteId']) 为 false |
为 true |
| isNew 值 | true | false |
| 导航栏标题 | “新建笔记” | “编辑笔记” |
| 收藏/删除按钮 | 不显示 | 显示 |
| 保存操作 | addNote() 新增 | 修改 note 属性 |
四、TagFilterPage 的入口差异
// 从首页标签点击
params: { 'tag': t.name } as Record<string,Object>
// 从 ProfilePage 标签云点击
params: { 'tag': t.name } as Record<string,Object>
// 直接打开(无参数)
无参数
三种入口传相同的参数格式——但初始状态不同。 有 tag 参数 → 直接显示筛选结果;无参数 → 显示标签网格供选择。
TagFilterPage 的状态由 selectedTag 控制:
if (p && p['tag']) {
this.selectedTag = p['tag'] as string
this.filterNotes()
}
// selectedTag 有值 → 显示笔记列表
// selectedTag 为空 → 显示标签网格
同一个页面两种视图——靠参数决定初始状态。 这是"条件渲染"在路由层面的应用。
五、底部导航栏的路由设计
@Builder Nav(label: string, active: boolean) {
Column({ space: 2 }) {
Text(label === '首页' ? '🏠' : (label === '搜索' ? '🔍' :
(label === '收藏' ? '❤️' : '👤'))).fontSize(18)
Text(label).fontSize(9).fontColor(active ? A : '#C0BFC6')
}.onClick(() => {
if (label === '搜索') router.pushUrl({ url: 'pages/NoteList',
params: { 'notebookId': -1, 'notebookName': '全部笔记' } as Record<string,Object> })
else if (label === '收藏') router.pushUrl({ url: 'pages/FavoritesPage' })
else if (label === '我的') router.pushUrl({ url: 'pages/ProfilePage' })
})
}
四个 tab 的路由策略
| Tab | 行为 | 参数 | 原因 |
|---|---|---|---|
| 首页 | 不跳转 | — | 已经在首页 |
| 搜索 | 跳转 NoteList | notebookId=-1 | 搜索 = 查看全部笔记 |
| 收藏 | 跳转 FavoritesPage | 无 | 收藏页独立数据源 |
| 我的 | 跳转 ProfilePage | 无 | 个人中心独立页面 |
“搜索” tab 不是真正的搜索页——它跳转到 NoteList 并传入 notebookId=-1。 这是一个设计选择:把"查看全部笔记"和"搜索"合并在同一个页面。NoteList 既有全部笔记列表,又有搜索框,一个页面承担两种功能。
“首页” tab 不跳转的处理
“首页” tab 没有 onClick 跳转逻辑——因为 Index 本身就是首页。 如果写了 router.pushUrl({ url: 'pages/Index' }),会导致页面重建,搜索框内容丢失。
踩坑记录: 最初"首页" tab 也写了跳转,点击后页面闪烁,首页状态丢失。去掉跳转后解决。这是 @Entry 页面的特性——重复 push 同一个 @Entry 会重建实例。
六、返回导航的统一模式
所有非首页页面的返回按钮都是同一段代码:
Row() { SymbolGlyph($r('sys.symbol.chevron_left')).fontSize(20).fontColor([T1]) }
.width(34).height(34).borderRadius(17).backgroundColor('rgba(0,0,0,0.03)')
.justifyContent(FlexAlign.Center).onClick(() => { router.back() })

5 个非首页页面都用了这段代码。 结构完全一致——34×34 点击区域、chevron_left 图标、3% 透明度背景、router.back()。
为什么不抽取 Builder? 因为这段代码只有 3 行有效逻辑(SymbolGlyph + 样式 + onClick),抽取 Builder 的收益不大。在 5 个页面的规模下,重复 3 行代码比引入 Builder 更简洁。
router.back() 的行为
router.back() 返回上一个页面——不需要指定目标。 系统自动恢复上一个页面的状态(如果是 @State 页面,状态保留)。
和 router.pushUrl 的区别:
| 方法 | 行为 | 页面栈 |
|---|---|---|
| router.pushUrl | 新页面入栈 | 栈深度 +1 |
| router.back() | 当前页出栈 | 栈深度 -1 |
用户操作路径示例:
首页 → 点击笔记 → NoteEditor(栈:首页, NoteEditor)
→ 点击返回 → 首页(栈:首页)
七、路由参数的类型安全
所有路由参数都用了 as Record<string,Object> 类型断言:
params: { 'noteId': n.id } as Record<string,Object>
为什么需要类型断言? router.pushUrl 的 params 类型要求 Record<string, Object>。直接传 { 'noteId': n.id } 会报类型不匹配——因为 TypeScript 推断 { noteId: number } 不完全等于 Record<string, Object>。
接收参数时也需要断言:
const p = router.getParams() as Record<string, Object>
const id: number = p['noteId'] as number
类型断言是 ArkTS 路由传参的标准做法——虽然繁琐,但保证了类型安全。
八、路由架构的整体模式
页面的角色分类
| 角色 | 页面 | 特点 |
|---|---|---|
| 入口页 | Index | 底部导航栏,不接收路由参数 |
| 列表页 | NoteList, FavoritesPage, TagFilterPage | 接收参数,渲染列表 |
| 详情/编辑页 | NoteEditor | 接收 noteId,双模式 |
| 展示页 | ProfilePage | 不接收参数,纯展示 |
数据流向
Index(入口)
├→ NoteList(笔记本筛选/搜索)
│ └→ NoteEditor(编辑)
├→ NoteEditor(新建)
├→ TagFilterPage(标签筛选)
│ └→ TagFilterPage(同页面切换)
├→ FavoritesPage(收藏列表)
│ └→ NoteEditor(编辑)
└→ ProfilePage(个人中心)
└→ TagFilterPage(标签筛选)
所有页面都汇聚到 NoteEditor——它是唯一的"写入"页面。 其他页面都是"读取"或"筛选",只有 NoteEditor 能新建和编辑笔记。这种"多入口单出口"的设计让数据写入集中管理,降低了数据不一致的风险。
更多推荐



所有评论(0)