知识库 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 能新建和编辑笔记。这种"多入口单出口"的设计让数据写入集中管理,降低了数据不一致的风险。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐