Navigation 是鸿蒙推荐的路由方案,替代了早期的 Router API。健康菜谱助手从 Router 迁移到 Navigation 的过程经历了三个阶段:路由表声明式注册、NavDestination 页面栈管理、以及转场动画与生命周期配合。这篇完整记录迁移过程中的设计决策、踩过的坑、以及 Navigation 相比 Router 的真正优势。

一、为什么迁移:Router 的三个瓶颈
Router API 在简单场景够用,但复杂场景有三个瓶颈。第一,路由路径是字符串硬编码------没有编译期检查,路径写错只有运行时才发现。第二,转场动画定制困难------Router 的转场只有几种预设模式,要自定义需要用 animateTo 配合路由回调,代码侵入性强。第三,与 Navigation 容器的配合不佳------Router 跳转的页面无法作为 Navigation 的子页签存在。Navigation 用路由表(route map)解决第一个问题,用 NavDestination 的转场属性解决第二个,用组件化的架构天然解决第三个。
| 维度 | Router | Navigation |
|---|---|---|
| 路由注册 | 字符串路径 | 路由表声明式 |
| 编译检查 | 无 | 路由名常量 |
| 转场动画 | 预设模式 | NavDestination自定义 |
| 嵌套容器 | 不支持 | 天然支持 |
二、路由表的声明式注册
Navigation 的路由通过 route map JSON 文件声明------每个页面注册一个路由名和对应的组件文件。编译期会检查路由名与组件的映射关系,路径错误在编译时就能发现。菜谱应用注册了 14 个路由页面:首页、分类、搜索、详情、编辑、设置等。路由名的命名规范:模块名_页面名(recipe_detail、search_main),避免不同模块的路由名冲突。
ts
// route_map.json
{
"routerMap": [
{ "name": "recipe_detail", "pageSourceFile": "src/main/ets/pages/RecipeDetail.ets" },
{ "name": "search_main", "pageSourceFile": "src/main/ets/pages/SearchMain.ets" }
]
}
// 代码跳转:使用常量而非字符串
this.navPathStack.pushPathByName(RouteName.RECIPE_DETAIL, params)
三、NavDestination 的生命周期:与 @Component 的区别
NavDestination 是 Navigation 管理的每个页面的容器组件------它有自己独立的生命周期:onShown(页面显示)、onHidden(页面隐藏)、onBackPressed(返回拦截)。这些与 @Component 的 aboutToAppear/aboutToDisappear 不同:onShown/onHidden 在页面切换时触发(包括 Tab 切换),而 aboutToAppear 只在组件创建时触发一次。菜谱详情页依赖 onShown 来刷新收藏状态(用户从收藏页切换过来时收藏状态可能变了),依赖 onBackPressed 来拦截返回并弹出确认对话框(编辑中未保存时提示)。
ts
NavDestination() {
// 页面内容
}
.onShown(() => {
this.refreshFavStatus() // 每次显示时刷新收藏
})
.onBackPressed(() => {
if (this.hasUnsavedChanges) {
this.showConfirmDialog()
return true // 拦截返回
}
return false // 不拦截
})
四、参数传递:PushPathByName 的类型安全
Navigation 的 pushPathByName 支持传递任意类型的参数------但默认没有类型检查。菜谱助手通过定义路由参数接口+泛型包装实现了类型安全:每个路由定义一个 Params 接口,pushPathByName 传入的参数必须符合接口类型。这样在编译期就能发现参数字段名的错误。参数的接收在 NavDestination 的 onReady 回调中获取------onReady 在页面构建完成前触发,可以安全地把参数赋给组件状态。
ts
// 路由参数接口
interface RecipeDetailParams { recipeId: string, fromSource: string }
// 跳转
this.navPathStack.pushPathByName(RouteName.RECIPE_DETAIL, { recipeId: id, fromSource: 'list' } as RecipeDetailParams)
// 接收
NavDestination() { ... }
.onReady((ctx: NavDestinationContext) => {
const params = ctx.pathInfo.param as RecipeDetailParams
this.loadRecipe(params.recipeId)
})
五、转场动画:NavDestination 的 customNavContentTransition
Navigation 支持自定义转场动画------通过 customNavContentTransition 回调,可以完全控制页面进入和退出的动画。菜谱详情页的转场效果:从列表页点击卡片,卡片图片"放大"为详情页头图的共享元素转场。实现需要在列表页和详情页都标记 geometryTransition 的相同 ID。踩过的坑:共享元素转场在列表滚动后可能找不到源元素------需要确保源元素在转场触发时仍然在可见区域内。
六、真实坑:NavDestination 的嵌套 Scroll 手势冲突
NavDestination 内部包含 Scroll 时,返回手势(侧滑)与 Scroll 的滚动会冲突------用户想侧滑返回,但 Scroll 消费了水平手势。解法是在 NavDestination 上设置 enableModeChangeAnimation 和 nestedScroll 配置,让侧滑手势优先于 Scroll 的水平滚动。另一个方案是用 Navigation 自带的返回手势替代自定义侧滑------Navigation 默认支持边缘侧滑返回,只要不拦截 onBackPressed 的默认行为,侧滑返回是自动的。

七、路由栈管理:popTo 与 replacePath 的使用
Navigation 的路由栈操作不止 push 和 pop。popTo 可以回退到路由栈中的任意一个指定页面(跳过中间页面),replacePath 可以替换栈顶页面(不增加栈深度)。菜谱应用的真实使用:搜索流程"搜索页→结果页→详情页",从详情页返回时直接回搜索页------用 popTo('search_main') 而不是连续 pop 两次。编辑保存成功后用 replacePath 替换编辑页为详情页------用户按返回回到列表而不是回到已保存的编辑页。
ts
// 回到搜索页(跳过结果页)
this.navPathStack.popTo('search_main')
// 编辑保存后:替换编辑页为详情页
this.navPathStack.replacePath(RouteName.RECIPE_DETAIL, { recipeId: savedId })
八、真实坑:Navigation 嵌套 Tabs 的路由管理
首页是 Navigation 嵌套 Tabs 的结构------Tabs 内有多个 NavDestination 子页面。坑在于 Tabs 切换时子页面的 NavDestination 生命周期会触发 onHidden/onShown------如果每个 Tab 的列表都在 onShown 时刷新数据,切换 Tab 就会频繁触发网络请求。解法是把数据刷新从 onShown 移到 Tab 的 onChange 事件里------只在用户主动切换 Tab 时刷新,页面回到前台(onShown)不刷新。生命周期的事件源要区分:是页面切换还是 Tab 切换还是应用前后台切换,每种事件的响应策略不同。

九、总结:Navigation 的五条迁移经验
收束成五条迁移经验:路由表用常量替代字符串路径(编译期检查);生命周期选 onShown/onHidden 而非 aboutToAppear(页面切换语义更准确);参数传递定义接口保证类型安全;转场动画用 NavDestination 自定义而非全局配置;嵌套 Tabs 时生命周期事件源要区分(Tab 切换 vs 页面切换 vs 前后台切换)。五条经验来自从 Router 到 Navigation 的完整迁移------迁移花了三天,但后续的路由相关 bug 降到了零。
参考资料:
- 华为开发者:Navigation 导航容器
- 华为开发者:NavDestination 页面管理
- 华为开发者:系统路由表 route_map
#鸿蒙 #HarmonyOS 7(API 26)
十、NavDestination 与 @Provide 的配合:路由参数的状态管理
Navigation 容器层的 @Provide 可以被所有 NavDestination 子页面 @Inject------这让路由参数之外的"页面间共享状态"有了一个干净的通道。菜谱助手的用法:Navigation 容器层 @Provide('currentUser') 传入当前用户信息,所有 NavDestination 页面 @Inject 消费------用户信息的变更(如设置页修改昵称)会自动同步到所有页面。这比通过路由参数在每个页面间传递用户信息要干净得多------路由参数只传"页面需要的业务数据",全局状态走 @Provide 通道。
ts
// Navigation 容器层
Navigation(this.navPathStack) { ... }
@Provide('currentUser') currentUser: UserInfo = loadUser()
// 任意 NavDestination 页面
NavDestination() { ... }
.aboutToAppear() {
@Inject('currentUser') userInfo: UserInfo // 自动拿到最新值
}
十一、Navigation 的模态交互:Dialog 模式的 NavDestination
Navigation 支持 NavDestination 以 Dialog 模式弹出------弹出的"页面"看起来像对话框,但拥有完整的 NavDestination 生命周期和路由栈管理。菜谱应用的"编辑菜谱名"就是一个 Dialog 模式的 NavDestination:从详情页弹出,输入新名字,确认后 pop 回来。Dialog 模式比 CustomDialog 的优势:弹出的内容可以通过路由栈管理(push 进来的可以 pop 回去)、可以传递参数、生命周期完整。CustomDialog 更适合纯 UI 展示的简单弹窗,需要路由能力的"弹窗页面"用 Dialog 模式的 NavDestination。
ts
// NavDestination 的 Dialog 模式
NavDestination() {
Column() {
TextInput({ text: this.currentName }).onChange(v => this.newName = v)
Button('确认').onClick(() => {
this.navPathStack.pop()
this.onNameChanged?.(this.newName)
})
}
}.mode(NavDestinationMode.DIALOG)
十二、Navigation 与 Tab 联动的路由设计
菜谱首页是 Navigation 内嵌 Tabs 的结构:底部 Tab 栏有"首页""分类""我的"三个页签,每个页签的内容区可以独立 push NavDestination。路由设计的关键决策:Tab 内的 push 应该把 NavDestination 放在哪个层级?放在 Tab 内容区内部(每个 Tab 有自己的子路由栈)意味着切换 Tab 后子路由栈保持------用户在"分类"Tab push 了详情页,切到"首页"再切回来,分类 Tab 的详情页还在路由栈里。放在 Navigation 顶层意味着切换 Tab 会清空子路由栈------用户永远从 Tab 根页面开始。菜谱应用选择了前者(子路由栈保持),因为"浏览多层级内容时切到别的 Tab 再回来"是高频操作,保留路由栈用户体验更好。
十三、Navigation 的性能:页面预加载与懒加载
Navigation 支持页面的预加载------prefersPullDownGesture 和 prefetch 相关配置可以控制即将显示的页面提前构建。但预加载也有代价:提前构建的 NavDestination 会占用内存。菜谱应用的策略:高频路由(详情页、搜索页)开启预加载,低频路由(设置页、关于页)关闭。加上 LazyForEach 的按需加载,首页的首帧渲染不包含任何 NavDestination 的构建成本------NavDestination 只在 push 时才创建。这个分层策略让首页的首帧不因路由系统的引入而变慢。
十四、总结:Navigation 迁移的投资回报
整个 Router→Navigation 的迁移投入约三天(路由表注册、14个页面改造、转场动画、嵌套Tabs处理)。回报是长期的路由相关 bug 归零、类型安全的参数传递、以及与 Tabs/Dialog 模式的良好配合。更重要的是:Navigation 是鸿蒙推荐的方向,新的特性(如服务卡片与路由的联动、跨设备路由同步)都基于 Navigation 构建------早迁移早受益。
参考资料:
- 华为开发者:Navigation 导航
- 华为开发者:NavDestination 生命周期
- 华为开发者:系统路由表配置
#鸿蒙 #HarmonyOS 7(API 26)