鸿蒙 HarmonyOS 6 | Navigation 路由栈绑定与主页 NavDestination 开发实战
以前写 Navigation 首页时,常见做法是把首页内容直接放在 Navigation 的闭包里。这个写法简单,但页面变复杂以后会出现一些不舒服的地方。首页内容和子页面结构不一致,生命周期也不完全一致;手机端、平板端、折叠屏展开态再叠加进来,首页会变成一个特殊页面,很多逻辑要单独处理。
前言
做鸿蒙应用页面结构时,我现在更倾向于把 Navigation 当成应用内部的主路由容器来设计。页面跳转、返回、替换、跨模块加载、分栏展示,这些能力放在同一套路由栈里处理,后续维护会轻很多。
以前写 Navigation 首页时,常见做法是把首页内容直接放在 Navigation 的闭包里。这个写法简单,但页面变复杂以后会出现一些不舒服的地方。首页内容和子页面结构不一致,生命周期也不完全一致;手机端、平板端、折叠屏展开态再叠加进来,首页会变成一个特殊页面,很多逻辑要单独处理。
HarmonyOS 6.0 API 20 给 Navigation 增加了新的重载接口:
Navigation(pathInfos: NavPathStack, homeDestination: HomePathInfo)
官方 API 参考中,Navigation(pathInfos: NavPathStack, homeDestination: HomePathInfo) 从 API version 20 开始支持,homeDestination 用来指定主页 NavDestination,HomePathInfo 通过 name 和 param 组织主页信息。
这个变化对页面架构影响很大。首页不再直接写在 Navigation 闭包里,而是变成一个标准的 NavDestination。首页、详情页、设置页、列表页都可以放进同一套 NavDestination 体系里管理,页面跳转和生命周期处理也更统一。
一、首页变成 NavDestination 后,页面结构会更干净
Navigation 本身是路由导航的根视图容器,通常作为 Page 页面根容器使用,内部包含标题栏、内容区和工具栏。内容区既可以显示首页内容,也可以显示通过 NavDestination 管理的非首页页面。
传统写法一般是这样:
@Entry
@Component
struct Index {
private stack: NavPathStack = new NavPathStack();
build() {
Navigation(this.stack) {
Column() {
Text('首页')
Button('进入详情页')
.onClick(() => {
this.stack.pushPath({ name: 'PageDetail' });
})
}
.width('100%')
.height('100%')
}
.title('主页')
}
}
这个写法适合小项目。首页内容直接放在 Navigation 里,按钮点击后把详情页压入路由栈。
问题会在项目变复杂后出现。详情页是 NavDestination,首页却是 Navigation 里的普通子组件。页面生命周期、标题控制、返回行为、参数接收方式都容易分叉。很多开发者会在首页和子页面之间写两套逻辑,短期能跑,后续改起来很麻烦。
API 20 的新写法把首页也改成 NavDestination:
@Entry
@Component
struct Index {
private stack: NavPathStack = new NavPathStack();
build() {
Navigation(this.stack, { name: 'PageHome' }) {
}
.width('100%')
.height('100%')
}
}
这里的 { name: 'PageHome' } 对应路由表里的主页配置。Navigation 会根据 homeDestination 找到对应的 NavDestination 构建函数,创建主页内容。
这时候 Navigation 里的闭包不要再写首页 UI。主页 UI 放到 PageHome 对应的 NavDestination 里。这样一来,首页和子页面都走同一套路由表、同一个 NavPathStack、同一类生命周期回调。
二、路由表配置要先稳定下来
使用主页 NavDestination 时,路由表配置是基础。官方文档里,Navigation 页面路由操作基于 NavPathStack 提供的方法实现,每个 Navigation 都需要创建并传入一个 NavPathStack 对象。系统路由表通过 router_map.json 里的 name 找到对应页面信息并入栈。
模块配置文件里先声明路由表:
{
"module": {
"routerMap": "$profile:route_map"
}
}
然后在 resources/base/profile 目录下创建 route_map.json:
{
"routerMap": [
{
"name": "PageHome",
"pageSourceFile": "src/main/ets/pages/HomePage.ets",
"buildFunction": "PageHomeBuilder",
"data": {
"description": "应用主页"
}
},
{
"name": "PageDetail",
"pageSourceFile": "src/main/ets/pages/DetailPage.ets",
"buildFunction": "PageDetailBuilder",
"data": {
"description": "详情页"
}
}
]
}
这里有几个地方要保持一致。
name 是路由名称,后续 Navigation(this.stack, { name: 'PageHome' }) 和 pushPath({ name: 'PageDetail' }) 都会用到它。
pageSourceFile 是页面源文件路径。路径写错时,路由表能加载,但跳转时找不到目标页面,调试时很容易误以为是 NavPathStack 的问题。
buildFunction 要和页面文件里的 @Builder 函数名称一致。官方跨包路由文档也提醒,跳转目标页面的入口 Builder 函数名称需要和 router_map.json 里的 buildFunction 保持一致,否则编译时会报错。
data 不是必须字段。它适合放一些描述性配置,或者后续做路由元信息管理。常规页面跳转参数还是放在 NavPathInfo.param 里更自然。
三、主页 NavDestination 里获取 NavPathStack
主页变成 NavDestination 后,跳转逻辑也放到主页组件内部处理。这个时候不建议从外部硬传一堆回调进去,直接在 onReady 里拿 NavDestinationContext 更清楚。
官方资料里提到,NavDestination 可以通过 onReady 事件拿到对应的 NavPathInfo 和所属的 NavPathStack;onReady(callback: Callback<NavDestinationContext>) 从 API version 11 开始支持,会在 NavDestination 即将构建子组件前触发。
主页可以这样写:
@Component
struct PageHome {
private stack?: NavPathStack;
build() {
NavDestination() {
Column({ space: 16 }) {
Text('首页')
.fontSize(24)
.fontWeight(FontWeight.Medium)
Button('进入详情页')
.onClick(() => {
this.stack?.pushPath({
name: 'PageDetail',
param: {
id: 1001,
source: 'home'
}
});
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
.title('主页')
.onReady((ctx: NavDestinationContext) => {
this.stack = ctx.pathStack;
})
.onShown(() => {
console.info('PageHome shown');
})
}
}
@Builder
export function PageHomeBuilder() {
PageHome();
}
这里不需要把 stack 声明成 @State。NavPathStack 负责页面栈操作,官方资料里也提到,NavPathStack 无需声明为状态变量,也可以实现页面栈操作。
主页里拿到 ctx.pathStack 后,就可以直接执行 pushPath、pop、replacePath 等操作。首页和子页面统一放在 NavDestination 体系里以后,页面跳转的写法会更稳定,后续抽公共路由工具也更方便。
详情页也按同样的方式取栈和参数:
interface DetailParam {
id: number;
source: string;
}
@Component
struct PageDetail {
private stack?: NavPathStack;
@State private detailId: number = 0;
@State private source: string = '';
build() {
NavDestination() {
Column({ space: 16 }) {
Text(`详情 ID:${this.detailId}`)
.fontSize(20)
Text(`来源:${this.source}`)
.fontSize(14)
.fontColor('#666666')
Button('返回')
.onClick(() => {
this.stack?.pop();
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
}
.title('详情页')
.onReady((ctx: NavDestinationContext) => {
this.stack = ctx.pathStack;
const param = ctx.pathInfo.param as DetailParam;
this.detailId = param.id;
this.source = param.source;
})
.onBackPressed(() => {
this.stack?.pop();
return true;
})
}
}
@Builder
export function PageDetailBuilder() {
PageDetail();
}
参数读取放在 onReady 里处理,能保证 NavDestination 上下文已经准备好。这里也顺手修掉了原稿里一个代码问题:详情页里使用 this.pathStack.pop(),但组件中没有定义 pathStack 字段。实际代码里要先从 ctx.pathStack 拿到栈引用,再调用 pop()。
四、生命周期不要再按普通 Page 理解
Navigation 接入后,很多页面状态问题都和生命周期理解有关。NavDestination 有自己的生命周期回调,例如 onWillShow、onShown、onWillHide、onHidden、onWillDisappear 等。官方 NavDestination 资料也能看到这些生命周期名称,页面显示、隐藏、销毁等阶段需要用 NavDestination 自身回调处理。
我通常会这样分工:
NavDestination() {
// 页面内容
}
.onReady((ctx: NavDestinationContext) => {
this.stack = ctx.pathStack;
this.initParams(ctx);
})
.onWillShow(() => {
console.info('page will show');
})
.onShown(() => {
this.refreshVisibleData();
})
.onWillHide(() => {
this.saveDraftIfNeeded();
})
.onHidden(() => {
console.info('page hidden');
})
onReady 更适合做上下文初始化。比如保存 pathStack,读取 pathInfo.param,初始化页面级配置。
onShown 更适合做可见后的刷新。比如从详情页返回首页后刷新列表,或者重新读取当前选中的 Tab 状态。
onWillHide 更适合处理离开前保存。比如表单草稿、筛选条件、滚动位置。
onHidden 可以处理一些轻量级清理。比如暂停页面内的动画、视频、轮询。
主页 NavDestination 也会走这些回调。应用启动时会进入主页显示流程;从主页进入详情页时,主页会进入隐藏状态;返回主页时,主页再次进入显示流程。这个机制比普通 Page 生命周期函数更适合 Navigation 内部的多页面切换。
五、复杂路由场景先统一入口,再谈封装
NavPathStack 提供了很多操作方法,常用的有 pushPath、pop、replacePath、popToName、popToIndex、moveToTop 等。官方 Navigation 路由文档也明确,Navigation 路由相关操作都基于导航控制器 NavPathStack 提供的方法实现。
在业务里我会先收敛三个常用入口。
class AppRouter {
constructor(private stack: NavPathStack) {}
pushDetail(id: number) {
this.stack.pushPath({
name: 'PageDetail',
param: {
id,
source: 'app_router'
}
});
}
back() {
this.stack.pop();
}
backToHome() {
this.stack.popToName('PageHome');
}
}
这样页面里就不需要到处写路由名称。路由名称集中在一个地方,后续改 PageDetail 的命名、参数结构、跳转动画,都不会散落到每个页面组件里。
路由拦截也可以放在统一入口里处理。setInterception 可以监听页面切换、模式变化等事件,适合做埋点、登录判断、导航状态同步。
this.stack.setInterception({
didShow: (from, to, operation, isAnimated) => {
console.info(`from: ${from.pathInfo?.name}, to: ${to.pathInfo?.name}`);
},
modeChange: (mode) => {
console.info(`Navigation mode changed: ${mode}`);
}
});
多端适配时,Navigation 的价值会更明显。手机端通常是 Stack 模式,用户一层一层进入页面;平板和折叠屏展开态更适合 Split 或 Auto 模式,左侧保留列表,右侧展示详情。首页变成 NavDestination 后,手机端和大屏端可以尽量复用同一批页面组件,只在布局和展示模式上做差异处理。
模块化项目也适合尽早使用路由表。Navigation 支持系统路由表和自定义路由表两种实现方式,路由表配置可以完成本包和跨包页面跳转。
如果项目后面要拆 HAR 或 HSP,业务模块可以维护自己的页面构建函数和路由配置,主工程只负责聚合和调用。这样团队协作会轻很多,页面之间的依赖也更容易控制。

总结
API 20 的 Navigation(pathInfos: NavPathStack, homeDestination: HomePathInfo) 适合用来整理应用的根导航结构。首页通过 HomePathInfo 指定为 NavDestination 后,首页和子页面可以进入同一套路由管理体系。
落地时我会优先处理这几个点。
入口页面只负责创建 NavPathStack 并绑定主页;首页和子页面都写成 NavDestination;route_map.json 的 name、pageSourceFile、buildFunction 保持一致;页面内通过 onReady 获取 ctx.pathStack 和 ctx.pathInfo.param;生命周期逻辑迁移到 NavDestination 对应回调里;复杂跳转尽量收敛到统一路由入口。
这套写法对中大型鸿蒙应用更友好。首页不再是特殊结构,子页面也不用单独维护一套路由习惯。后续要做平板适配、折叠屏展开态、跨模块页面加载、路由拦截和状态恢复,都会有更稳定的基础。
更多推荐


所有评论(0)