知识库 App 有 6 个页面、4 个底部导航 tab、十几条路由跳转路径。页面之间的跳转不是随意的——每条路由都有明确的参数设计,参数的类型和内容决定了目标页面的行为。搞清楚"从哪来、传什么、怎么用",是理解整个 App 数据流的关键。

完整效果
在这里插入图片描述

一、路由参数的完整清单

源页面目标页面参数参数类型
Index(笔记本卡片)NoteListnotebookId, notebookNamenumber, string
Index(底部导航"搜索")NoteListnotebookId=-1, notebookName=‘全部笔记’number, string
Index("+"按钮)NoteEditor
Index(笔记卡片)NoteEditornoteIdnumber
Index(标签点击)TagFilterPagetagstring
Index(底部导航"收藏")FavoritesPage
Index(底部导航"我的")ProfilePage
NoteList(笔记卡片)NoteEditornoteIdnumber
FavoritesPage(笔记卡片)NoteEditornoteIdnumber
ProfilePage(标签云点击)TagFilterPagetagstring
TagFilterPage(标签点击)TagFilterPagetagstring(同页面切换)

11 条路由路径,4 种参数类型:

参数类型出现次数用途
notebookIdnumber2 次按笔记本筛选笔记
notebookNamestring2 次NoteList 标题显示
noteIdnumber3 次编辑指定笔记
tagstring3 次按标签筛选笔记

二、三种参数传递模式

模式一:双参数关联传递

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 值truefalse
导航栏标题“新建笔记”“编辑笔记”
收藏/删除按钮不显示显示
保存操作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行为参数原因
首页不跳转已经在首页
搜索跳转 NoteListnotebookId=-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、测试、元服务和应用上架分发等。

更多推荐