本文献给:

已掌握 Stage 模型与 Want 跳转机制、希望进一步提升页面导航体验的鸿蒙开发者。ArkUI 中的 Navigation 组件是官方推荐的页面路由与导航解决方案,相比传统的 router 提供了更清晰的页面栈管理、更灵活的转场动画以及更强大的跨模块导航能力。本文将深入解析 Navigation 的整体架构,重点讲解 NavDestination 的生命周期回调,帮助你构建结构清晰、交互流畅的多页面应用。


你将学到:

  1. Navigation 组件的核心架构:NavPathStack、NavDestination、导航栏
  2. Navigation 的基本使用方式与页面跳转配置
  3. NavDestination 的生命周期回调(onShown、onHidden、onBackPressed 等)
  4. NavDestination 生命周期与组件生命周期的区别和联系
  5. 页面间参数传递与结果返回的推荐方式
  6. 常见错误与注意事项



一、为什么选择 Navigation

在 HarmonyOS 早期版本中,页面跳转主要依赖 router.pushUrlrouter.replaceUrl。这种方式虽然简单,但存在一些不足:

  • 页面栈是全局的、隐式的,难以精确控制和获取栈状态。
  • 跨模块跳转配置繁琐,需要手动管理路径字符串。
  • 自定义转场动画和共享元素动画支持有限。
  • 随着应用复杂度增加,路由表维护成本高。

Navigation 组件从 API 9 开始引入,作为官方推荐的导航容器,它将页面栈(NavPathStack)与导航视图(Navigation)显式绑定,带来了以下优势:

  • 显式页面栈:开发者可以随时获取和操作栈内容。
  • 灵活的转场:支持自定义入场/出场动画,以及页面共享元素过渡。
  • 页面生命周期细化:NavDestination 提供了专属的页面级生命周期,与组件生命周期配合使用更加清晰。
  • 与 NavRouter 联动:可以在任何地方触发导航,不依赖当前页面上下文。
  • 跨模块支持:通过系统路由表实现模块间解耦导航。

二、Navigation 核心架构

2.1 三大核心概念

  • Navigation:导航容器组件,用于包裹页面内容区域,并自动展示标题栏。通常作为应用根布局或某功能模块的根布局。
  • NavPathStack:导航栈对象,保存当前 Navigation 中所有页面的路由信息。开发者可以手动操作栈(push、pop、replace 等)。
  • NavDestination:页面内容组件,每个需要被导航到的页面都需要包裹在 NavDestination 中。NavDestination 自带与 Navigation 标题栏的联动,并拥有独立的页面级生命周期。

2.2 架构关系图

┌─────────────────────────────────┐
│          Navigation             │
│  ┌───────────────────────────┐  │
│  │     NavDestination A      │  │
│  │   (当前显示页面)           │  │
│  └───────────────────────────┘  │
│  ┌───────────────────────────┐  │
│  │     NavDestination B      │  │
│  │   (栈中页面,不可见)        │  │
│  └───────────────────────────┘  │
│          NavPathStack            │
│   [ pageA, pageB ]              │
└─────────────────────────────────┘

Navigation 内部维护一个 NavPathStack,通过 pushPathpopToName 等方法改变栈内容,界面上对应的 NavDestination 会自动切换显示。


三、Navigation 的基本使用

3.1 创建 Navigation 并绑定栈

通常在入口页面或 Ability 加载的首页中使用 Navigation 作为根容器:

@Entry
@Component
struct MainPage {
  // 创建并持有导航栈
  private navPathStack: NavPathStack = new NavPathStack();

  build() {
    Navigation(this.navPathStack) {
      // 初始显示的页面
      HomeContent()
    }
    .title('主页')
    .mode(NavigationMode.Stack)
  }
}

3.2 定义页面(NavDestination)

每个需要被推入栈的页面都需要用 NavDestination 包裹:

@Component
struct HomeContent {
  build() {
    NavDestination() {
      Column() {
        Text('这是主页内容')
        Button('跳转到详情')
          .onClick(() => {
            // 获取 Navigation 上下文,推入新页面
            this.getNavigation()?.pushPath({ name: 'DetailPage' });
          })
      }
    }
    .title('主页')
  }
}

注意this.getNavigation() 可以获取当前组件所属的 Navigation 控制器,用于操作导航栈。该方法在 NavDestination 内部或任何被 Navigation 包裹的组件中可用。

3.3 路由页面注册

推荐使用系统路由表(router_map.json)进行页面注册,这样可以解耦页面路径:

src/main/resources/base/profile/ 下创建 router_map.json

{
  "routerMap": [
    {
      "name": "DetailPage",
      "pageSourceFile": "src/main/ets/pages/DetailPage.ets",
      "buildFunction": "buildDetailPage"
    }
  ]
}

对应的 DetailPage.ets 必须导出一个 @Builder 函数:

@Builder
export function buildDetailPage() {
  DetailContent()
}

@Component
struct DetailContent {
  build() {
    NavDestination() {
      Text('详情页内容')
    }
    .title('详情')
  }
}

配置完成后,直接通过 pushPath({ name: 'DetailPage' }) 即可实现页面跳转,无需手动导入组件。这种方式也称为命名路由


四、NavDestination 生命周期

NavDestination 拥有一套完整的页面级生命周期,与 Navigation 栈的推入、弹出行为紧密相关。

4.1 生命周期回调总览

回调方法 触发时机
onShown() 当 NavDestination 被推入栈并成为可见时(入场动画结束后)
onHidden() 当 NavDestination 被其他页面覆盖、变为不可见时(出场动画开始前)
onBackPressed() 当用户按下返回键、试图从该页面返回时
onReady() 当 NavDestination 构建完成、可以安全地进行节点操作时(API 11+)

这些生命周期回调是 NavDestination 特有的,独立于组件的 aboutToAppear / aboutToDisappear。组件生命周期关注的是节点的创建与销毁,而页面生命周期关注的是在导航栈中的可见性变化。

4.2 onShown 与 onHidden

这两个回调通常成对出现,用于处理页面进入前台和离开前台时的逻辑。

@Component
struct ProfilePage {
  build() {
    NavDestination() {
      Text('个人中心')
    }
    .onShown(() => {
      console.log('ProfilePage 可见,加载数据');
      // 刷新数据、恢复定时器、重新注册监听等
    })
    .onHidden(() => {
      console.log('ProfilePage 隐藏,暂停操作');
      // 暂停动画、取消网络请求、保存草稿等
    })
  }
}

执行顺序示例:

  • 从主页 A 推入页面 B:

    1. B 的组件 aboutToAppear
    2. B 的 onReady(如果有)
    3. A 的 onHidden
    4. B 的 onShown
  • 从 B 返回到 A:

    1. B 的 onBackPressed(如果被重写)
    2. B 的 onHidden
    3. A 的 onShown
    4. B 的组件 aboutToDisappear

注意:onHidden 早于 onShown 的调用,遵循“先隐藏旧页面,再显示新页面”的顺序。

4.3 onBackPressed —— 拦截返回

onBackPressed 默认返回 false,表示不拦截,执行系统默认行为(即弹出当前页面)。如果返回 true,则拦截返回事件,开发者可以自行处理(如弹出对话框、跳转到其他页面)。

NavDestination() {
  // 页面内容...
}
.onBackPressed(() => {
  // 返回 true 拦截默认返回
  AlertDialog.show({
    message: '确定要离开吗?未保存的数据将丢失',
    primaryButton: {
      value: '留下',
    },
    secondaryButton: {
      value: '离开',
      action: () => {
        // 手动从栈中移除当前页面
        this.getNavigation()?.pop();
      }
    }
  });
  return true;
})

注意:调用 this.getNavigation()?.pop() 时,会触发当前页面的 onHidden,然后触发前一个页面的 onShownonBackPressed 内部拦截后,系统不再自动弹栈。

4.4 onReady —— 构建完成回调(API 11+)

onReady 在 NavDestination 构建完成、其下的组件树全部就绪时触发,可用于执行需要获取组件尺寸、位置或访问子组件的操作。

NavDestination() {
  Text('内容')
}
.onReady((context: NavDestinationContext) => {
  console.log('NavDestination 已就绪,可以安全操作子组件');
  // 例如:获取某个节点的屏幕坐标
})

由于 aboutToAppear 触发时子组件可能尚未渲染,若需要做 DOM 相关操作,onReady 是更安全的选择。


五、导航栈操作与参数传递

5.1 常用栈操作方法

通过 NavPathStack 实例,可以进行各种导航操作:

// 推入新页面(命名路由)
this.navPathStack.pushPath({ name: 'DetailPage', param: { id: 42 } });

// 推入新页面(直接传入组件构建函数)
this.navPathStack.pushPathByName('DetailPage', { id: 42 });

// 弹出当前页面
this.navPathStack.pop();

// 返回到指定页面,同时传递结果
this.navPathStack.popToName('HomePage', { result: 'success' });

// 替换当前页面
this.navPathStack.replacePath({ name: 'LoginPage' });

// 清空栈并推入新首页
this.navPathStack.clear();
this.navPathStack.pushPath({ name: 'NewHome' });

5.2 参数接收

在目标页面中,可以通过 NavDestinationonShown 回调中的上下文,或者通过路由参数获取。

方式一:在 NavDestination 的构建函数中接收

使用命名路由时,buildFunction 可以通过闭包获取参数:

@Builder
export function buildDetailPage(name: string, param: Object) {
  DetailContent({ id: param['id'] as number })
}

系统会自动将 pushPathparam 传入 buildFunction 的参数。

方式二:在页面组件内使用 NavDestination 的上下文

@Component
struct DetailContent {
  @State id: number = 0;

  build() {
    NavDestination() {
      Text(`详情ID: ${this.id}`)
    }
    .onShown((context: NavDestinationContext) {
      // 从 context 中获取路由参数
      let param = context.pathInfo?.param;
      if (param) {
        this.id = param['id'] as number;
      }
    })
  }
}

NavDestinationContext 包含 pathInfo,其中有当前路由的 nameparam 等完整信息。

5.3 返回结果

Navigation 推荐使用 popToNamepop 结合栈事件来传递返回结果。

// 在详情页保存后,返回列表页并传递结果
this.getNavigation()?.popToName('ListPage', { refresh: true });

// 或者弹出当前页时传递结果(需要配合栈事件监听)
this.getNavigation()?.pop({ saved: true });

在列表页的 onShown 中检查参数:

.onShown((context: NavDestinationContext) {
  let refresh = context.pathInfo?.param?.refresh;
  if (refresh) {
    // 刷新列表数据
  }
})

推荐实践:对于复杂的返回结果,可以使用应用级状态管理(如 AppStorage)或自定义事件总线,而不必完全依赖路由参数。


六、常见错误与注意事项

6.1 忘记包裹 NavDestination

所有需要加入 Navigation 栈的页面组件外层必须有 NavDestination,否则无法成为栈中的一页,且生命周期回调不会触发。

6.2 混淆组件生命周期与页面生命周期

  • 组件生命周期的 aboutToAppear / aboutToDisappear 在组件创建和销毁时触发,页面首次推入和最终弹出都会触发。
  • onShown / onHidden 在每次页面显示/隐藏时触发,切后台再回来也会触发(与页面可见性相关)。
  • 不要在 aboutToAppear 中执行依赖 onShown 数据的逻辑,可能导致时序问题。

6.3 onBackPressed 中忘记返回布尔值

onBackPressed 必须显式返回 truefalse。如果返回 true 但未手动处理弹出逻辑,用户将被困在当前页面。

6.4 直接修改 NavPathStack 而不通过 Navigation 控制器

虽然 navPathStack 实例可在外部访问,但推荐统一通过 this.getNavigation() 提供的方法操作栈,保证生命周期回调的完整触发。

6.5 页面栈过深导致内存压力

避免无限制的 pushPath,对于重复打开的页面(如商品详情),可使用 replacePath 或自定义返回逻辑。使用 clear() 方法前注意保存必要状态。


七、小结

概念 关键点
Navigation 导航容器,需绑定 NavPathStack,提供标题栏与栈管理
NavPathStack 显式页面栈,支持 push、pop、replace、popToName 等操作
NavDestination 页面组件容器,提供独立页面生命周期:onShownonHiddenonBackPressedonReady
onShown / onHidden 页面可见性变化时触发,适合数据刷新/暂停
onBackPressed 拦截返回事件,可返回 true 阻止默认行为
参数传递 通过 pushPathparam 传递,在 onShown 上下文或 buildFunction 中接收
返回结果 推荐 popToName 携带结果参数,在目标页的 onShown 中处理

掌握 Navigation 的架构与 NavDestination 的生命周期,是构建多页面应用、实现复杂导航逻辑的基石。配合状态管理与组件生命周期,你的应用将具备清晰的数据流向和流畅的用户体验。




觉得文章有帮助?别忘了:

👍 点赞 👍 – 给我一点鼓励
⭐ 收藏 ⭐ – 方便以后查看
🔔 关注 🔔 – 获取更新通知



标签: #HarmonyOS #ArkUI #Navigation #NavDestination #页面生命周期 #学习笔记 #鸿蒙开发

Logo

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

更多推荐