鸿蒙原生 ArkTS 布局(一):Navigation + NavRouter + NavDestination 导航布局实战


一、引言

在 HarmonyOS NEXT(API 24,对应 SDK 6.1.0)应用开发中,页面导航是每个应用的核心课题。鸿蒙 ArkUI 提供了两套导航体系:

  • 页面级导航:基于 router 模块(router.pushUrlrouter.back),页面完全独立,需要逐个在 main_pages.json 注册。适合独立模块间跳转。
  • 组件级导航:基于 Navigation + NavPathStack + NavDestination,在同一个页面容器内管理多个视图切换,支持栈式管理、手势返回、标题联动,适合多级下钻和详情展示。

本文以一个完整的"Navigation 导航布局演示"应用为主线,深入讲解组件级导航的核心 API、ArkTS 严格模式下的编码陷阱,以及从废弃 API 到新写法的迁移路径。


二、API 24 的 ArkTS 严格模式

API 24 引入了更严格的静态检查规则,许多传统 TypeScript 写法会直接编译报错:

规则 说明
arkts-no-any-unknown 禁止 anyunknown 类型
arkts-no-untyped-obj-literals 禁止未绑定显式类型的内联对象字面量
arkts-strict-prop-init 禁止通过构造函数初始化 private 属性

这三大规则几乎贯穿了本应用开发的全过程,是我们踩坑最多的根源。


三、项目结构

entry/src/main/ets/pages/
├── Index.ets          ← 入口:Navigation 根容器 + 导航列表
├── HomePage.ets       ← 首页详情 NavDestination
├── DetailPage.ets     ← 详情页 NavDestination(支持多层嵌套)
└── ProfilePage.ets    ← 个人中心 NavDestination

只有 Index.etsmain_pages.json 中注册,其他三个页面是 @Component 级组件,在 Navigation 内部通过 NavDestination 加载。


四、核心 API 详解

4.1 Navigation

Navigation 是容器级组件,接收 NavPathStack 实例,管理所有子页面的压栈和出栈。

Navigation(this.navPathStack) { /* 内容区 */ }
  .title('导航演示')
  .mode(NavigationMode.Stack)
  .hideBackButton(false)
  .navDestination(this.PageMap)
  • mode(NavigationMode.Stack):栈式导航,每次 pushPath 将新页面压入栈顶
  • hideBackButton(false):非根页面时自动显示返回箭头
  • navDestination(this.PageMap):绑定路由名与目标页的 @Builder 映射

4.2 NavPathStack

路由栈管理引擎,类似于浏览器中的 history 栈。

方法 说明
pushPath(info: NavPathInfo) 将新页面压入导航栈
pop() 弹出栈顶页面
clear() 清空整个导航栈,回到根页面
getIndexByName(name): number[] 获取指定路由名在栈中的所有索引
size(): number 当前栈深度

注意popToTop() 在 API 24 中不存在,请用 clear() 替代。

4.3 NavPathInfo 接口

interface NavPathInfo {
  name: string;
  param?: Object;  // 在 ArkTS 严格模式下,Object 类型不可用
}

由于 param 字段类型为 Object,而 Object 在严格模式下被禁止,因此无法通过 param 传参。替代方案:使用 getIndexByName() 获取栈索引,动态生成页面内容。

4.4 NavDestination

目标页面的容器,提供标题栏和返回事件:

NavDestination() { HomePage() }
  .title('首页详情')
  .onBackPressed(() => { this.navPathStack.pop(); return true; })

返回 true 表示消费返回事件,不再冒泡。

4.5 导航映射 @Builder

@Builder
PageMap(name: string, param: object) {
  if (name === 'HomePage') {
    NavDestination() { HomePage() }.title('首页详情')
  } else if (name === 'DetailPage') {
    NavDestination() { DetailPage({ navPathStack: this.navPathStack }) }.title('详情页')
  } else if (name === 'ProfilePage') {
    NavDestination() { ProfilePage() }.title('个人中心')
  }
}

五、ArkTS 严格模式实战陷阱(重中之重)

我们在构建这个示例应用时,编译器拦截了 7 类错误。以下是逐一剖析。

5.1 NavRouter 和 onStateChange 已废弃

报错'NavRouter' has been deprecated'onStateChange' has been deprecated

原因:API 24 中,NavRouter 组件已经废弃,Navigation 不再需要它包裹触发路由。

解决方案:使用普通 RowButton,在 onClick 中直接调用 pushPath

// ❌ 旧写法(已废弃)
NavRouter() { Row() { ... } }
  .onStateChange((a) => {})
  .onClick(() => this.navPathStack.pushPath({ name: 'Page' }))

// ✅ 新写法
Row() { ... }
  .onClick(() => {
    const info: NavPathInfo = { name: 'Page' };
    this.navPathStack.pushPath(info);
  })

5.2 private 属性不可通过构造函数初始化

报错Property 'navPathStack' is private and can not be initialized through the component constructor

原因:ArkTS 禁止父组件通过 Child({ privateField: value }) 给子组件的 private 属性赋值。

解决方案:将传入属性改为 @Prop

// ❌ 错误
@Component export struct DetailPage {
  private navPathStack: NavPathStack = new NavPathStack();
}

// ✅ 正确
@Component export struct DetailPage {
  @Prop navPathStack: NavPathStack = new NavPathStack();
}

@Prop 允许父组件在构造时注入初始值,且值变化时子组件 UI 会响应式更新。

5.3 Object 类型被禁止

报错Use explicit types instead of "any", "unknown" (arkts-no-any-unknown)

原因Object 被视为 unknown 的等价形式,不允许在用户代码中出现。

解决方案:使用自定义接口替代 Object

// ❌ 错误
const param = pathInfo[0] as Record<string, Object>;

// ✅ 正确
interface DetailPageParam { title: string; description: string; }
const param = pathInfo[0] as DetailPageParam;

虽然由于 NavPathInfo.param 本身也是 Object 类型,最终我们没有选择通过 param 传参,但自定义接口依然是 ArkTS 开发的基础技能。

5.4 内联对象字面量被禁止

报错Object literal must correspond to some explicitly declared class or interface (arkts-no-untyped-obj-literals)

原因:编译器无法确认匿名对象是否符合目标类型约束。

解决方案:预声明类型明确的变量:

// ❌ 错误
this.navPathStack.pushPath({ name: 'HomePage' });

// ✅ 正确
const pathInfo: NavPathInfo = { name: 'HomePage' };
this.navPathStack.pushPath(pathInfo);

例外shadowmarginpadding 等方法的对象参数通常可以从方法签名中推断类型,内联字面量可接受。

5.5 getIndexByName 返回 number[]

报错Type 'number[]' is not assignable to type 'number'

原因getIndexByName() 返回 number[](同一路由名可能在栈中出现多次)。

解决方案

// ❌ 错误
const index: number = this.navPathStack.getIndexByName('DetailPage');

// ✅ 正确
const indices: number[] = this.navPathStack.getIndexByName('DetailPage');
if (indices && indices.length > 0) {
  this.detailIndex = indices[indices.length - 1]; // 取栈顶索引
}

5.6 popToTop() 不存在

报错Property 'popToTop' does not exist on type 'NavPathStack'

原因:API 24 的 NavPathStack 没有 popToTop() 方法。

解决方案

// ❌ 错误
this.navPathStack.popToTop();

// ✅ 正确
this.navPathStack.clear();

5.7 路由参数传递的整体方案

getParamByName() 返回 Array<Object>Object 又不可用。最终的解决方案是:放弃通过 param 传参,改用栈索引推断页面状态

aboutToAppear(): void {
  const indices: number[] = this.navPathStack.getIndexByName('DetailPage');
  if (indices && indices.length > 0) {
    this.detailIndex = indices[indices.length - 1];
  }
  if (this.detailIndex > 0) {
    this.detailTitle = '子详情页 - 第 ' + (this.detailIndex + 1) + ' 层';
    this.detailDescription = '这是第 ' + (this.detailIndex + 1) + ' 层详情页面...';
  }
}

这种模式类似 Web 开发的 URL 路径推断——页面状态与栈位置一一对应,更可预测。


六、各页面组件详解

6.1 Index.ets 完整流程

build()
├── Navigation(navPathStack)
│   ├── Column
│   │   ├── 标题区
│   │   ├── 首页导航项 (Row + onClick → pushPath)
│   │   ├── 详情页导航项 (Row + onClick → pushPath)
│   │   ├── 个人中心导航项 (Row + onClick → pushPath)
│   │   └── 布局说明区
│   ├── .title('导航演示')
│   ├── .mode(NavigationMode.Stack)
│   ├── .hideBackButton(false)
│   └── .navDestination(PageMap)
│
└── @Builder PageMap(name, param)
    ├── name='HomePage'    → NavDestination { HomePage() }
    ├── name='DetailPage'  → NavDestination { DetailPage(navPathStack) }
    └── name='ProfilePage' → NavDestination { ProfilePage() }

6.2 HomePage.ets

特性列表展示 Navigation 的四大优势:一体化导航、NavPathStack 路由栈、NavDestination 目标页、栈式管理。每个特性通过 FeatureItem 子组件渲染,使用 @Prop 接收外部数据。

6.3 DetailPage.ets — 多层导航核心

展示三个关键能力:

  1. 自动感知层级:通过 getIndexByName() 获取索引,动态生成标题。
  2. 多层嵌套跳转:按钮反复 push 同一路由名,Navigation 自动管理栈增长和返回箭头。
  3. 导航控制pop() 逐层返回,clear() 一次性回根页面。

页面生命周期与栈的关系:

pushPath → aboutToAppear() → build() → [页面可见]
                                          ↓
                                  pop() → aboutToDisappear() → [页面销毁]

同一路由名多次 push 时,旧实例保持在栈中,不会触发 aboutToDisappear

6.4 ProfilePage.ets

展示更丰富的 UI 组合:用户信息卡片、三列统计数据(StatItem 子组件,使用 @Prop)、ForEach 循环渲染的 5 个设置项,带条件分隔线。

ForEach(this.settingList, (item: SettingItem, index: number) => {
  Row() { /* 设置项内容 */ }
  if (index < this.settingList.length - 1) {
    Divider().height(1).color('#F0F0F0').margin({ left: 50 })
  }
})

七、从 NavRouter 到 pushPath 迁移对照

旧写法 新写法 原因
NavRouter() { row } Row() { row } NavRouter 已废弃
onStateChange((a) => {}) 删除,改用生命周期 废弃
内联 pushPath({ name }) 预声明 NavPathInfo 变量 禁止内联对象字面量
pushPath({ name, param: {...} }) 不传 param,用栈索引替代 Object 类型被禁止
popToTop() clear() 方法不存在
getParamByName() 返回 Array<Object> 避免使用,改用 getIndexByName() Object 类型被禁止
private 传入属性 @Prop 装饰器 严格模式限制

八、最佳实践

架构层面

  1. 优先使用组件级导航。相比于 router.pushUrlNavigation + NavPathStack 更高效,天然支持手势返回和标题联动。
  2. 一个 Navigation 根容器。在 @Entry 页面放置一个 Navigation 即可覆盖所有子页面,避免嵌套 Navigation。
  3. 按功能模块拆分 @Builder 分支。每个 name 对应一个独立模块,模块增多时可将 @Builder 方法体抽离到单独文件。

编码层面

  1. 所有子组件传参用 @Prop,不要用 private
  2. 避免 Objectanyunknown,统一用自定义接口。
  3. 对象字面量先声明为类型变量再传入 API,尤其是在 pushPathForEach 场景。
  4. clear() 替代 popToTop()
  5. getIndexByName() 的栈索引感知页面层级,代替路由参数传层级。

调试层面

  1. aboutToAppear 添加日志,观察栈行为:
aboutToAppear(): void {
  console.info('[Page] 出现,栈深度: ' + navPathStack.size());
}
  1. 利用返回按钮和手势测试栈。确保 onBackPressed 返回 true 来消费事件。

九、展望

从 API 24 的变化可以看出鸿蒙团队的方向:

  • 类型安全第一。ArkTS 向完全类型安全的方言演进,anyObject、内联字面量都会被约束。
  • 声明式 + 命令式混合NavRouter 废弃与 pushPath 强化,标志导航从"组件声明"走向"内容声明式,导航命令式"。
  • API 持续收敛popToTop() 移除,clear() 语义统一,API 设计更简洁一致。

开发者应紧跟官方文档,在 API 升级时主动审视废弃用法,养成 ArkTS 严格模式的编码习惯。


十、完整代码关键片段

Index.ets — 根容器与导航映射

@Entry
@Component
struct Index {
  private navPathStack: NavPathStack = new NavPathStack();

  @Builder
  PageMap(name: string, param: object) {
    if (name === 'HomePage') {
      NavDestination() { HomePage() }
        .title('首页详情')
        .onBackPressed(() => { this.navPathStack.pop(); return true; })
    } else if (name === 'DetailPage') {
      NavDestination() { DetailPage({ navPathStack: this.navPathStack }) }
        .title('详情页')
        .onBackPressed(() => { this.navPathStack.pop(); return true; })
    } else if (name === 'ProfilePage') {
      NavDestination() { ProfilePage() }
        .title('个人中心')
        .onBackPressed(() => { this.navPathStack.pop(); return true; })
    }
  }

  build() {
    Navigation(this.navPathStack) {
      Column() { /* 导航列表项 */ }
    }
    .title('导航演示')
    .mode(NavigationMode.Stack)
    .hideBackButton(false)
    .navDestination(this.PageMap)
  }
}

main_pages.json

{ "src": ["pages/Index"] }

其他三个页面是组件级 NavDestination,不需要注册。


十一、总结

本文围绕 HarmonyOS NEXT API 24 下的一个完整 Navigation 导航布局应用,系统讲解了:

  1. Navigation 根容器 + NavPathStack 路由栈 + NavDestination 目标页 的核心用法
  2. ArkTS 严格模式的 7 类编译陷阱及其解决方案
  3. NavRouter 废弃后的迁移路径:从组件包裹到 pushPath 命令式触发
  4. 组件化拆分@Prop 传参、ForEach 循环、@Builder 映射的实战模式

掌握组件级导航是鸿蒙原生应用开发的基础能力。实际项目中可能遇到更复杂场景——Tab 切换、WebView 联动、跨页面状态共享、路由守卫等,这些将在后续系列文章中继续探讨。


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐