HarmonyOS NEXT 导航最佳实践:用 Navigation + NavPathStack 接管页面栈

前言

在 HarmonyOS NEXT(API 9+)中,系统官方推荐使用 Navigation 组件作为应用内的主导航容器,逐步取代旧版 @ohos.router 的全局跳转。相比 Router,Navigation 把页面栈(NavPathStack)收拢到组件内部,支持路由表、参数类型安全、拦截返回、转场动画等能力,更适合中大型应用的分层架构。本文从一次真实的多页面跳转改造入手,讲清 Navigation 的核心机制与常见坑。

问题描述

开发者在迁移到 NEXT 时常遇到下面几类问题:

  1. router.pushUrl 跳转后,目标页拿不到复杂参数,只能塞 params: { id: 1 } 这种扁平对象,类型完全丢失。
  2. 想在返回时把"编辑结果"回传给上一个页面,Router 没有官方回传通道,只能靠全局状态或 EventHub 凑。
  3. 用户在详情页点系统返回键,希望先弹"未保存,确认退出?",Router 很难拦截。
  4. 多模块并行开发时,页面路由散落在各处的 pushUrl({ url: 'pages/Detail' }) 字符串里,重构时一改路径全线崩溃。

这些痛点本质上都是"页面栈不在自己手里"导致的。Navigation 通过 NavPathStack 把栈暴露给业务层,上述问题都有了标准解法。

细节解析

1. Navigation 与 NavDestination 的关系

Navigation 是容器,NavDestination 是栈内每一个页面节点。容器持有一个 NavPathStack 对象,所有压栈/出栈操作都作用在它上面,而不是全局 Router。

// 根页面
Navigation(this.pageStack) {
  // 首页内容
}
.title('我的应用')

2. 路由表(route_map)解耦页面路径

module.json5 中声明 routerMap,把名字映射到 Builder,业务侧只 push 名字,不写文件路径:

// src/main/resources/base/profile/route_map.json
{
  "routerMap": [
    { "name": "Detail", "pageSourceFile": "src/main/ets/pages/Detail.ets", "buildFunction": "DetailBuilder" }
  ]
}
// module.json5
"routerMap": "$profile:route_map"

3. 参数传递与回传

NavPathStack.pushPathByName(name, param, callback?)param 可以是任意对象;目标页在 NavDestination@Builder 参数里直接拿到。callback 用于接收回传值。

4. 拦截系统返回

NavDestination 提供 onBackPressed 返回值:返回 true 表示"我已消费此次返回,系统不再默认出栈",你可以在里面弹确认框。

示例代码

根页面(持有 NavPathStack)

// src/main/ets/pages/Index.ets
import { DetailParam } from './Detail';

@Entry
@Component
struct Index {
  @Provide('pageStack') pageStack: NavPathStack = new NavPathStack()

  build() {
    Navigation(this.pageStack) {
      Column({ space: 16 }) {
        Text('首页')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)

        Button('打开详情(传参并等待回传)')
          .onClick(() => {
            const param: DetailParam = { id: 1001, from: 'home' }
            // 第三个参数为回传回调
            this.pageStack.pushPathByName('Detail', param, (result: string) => {
              console.info('[Index] 收到详情页回传:', result)
              promptAction.showToast({ message: '回传: ' + result })
            })
          })

        Button('直接跳转到详情(不等待回传)')
          .onClick(() => {
            this.pageStack.pushPathByName('Detail', { id: 2002, from: 'quick' } as DetailParam)
          })
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)
    }
    .title('Navigation 示例')
    .mode(NavigationMode.Stack) // 手机用 Stack,折叠屏/平板用 Split 可做侧栏
  }
}

详情页(NavDestination + 参数 + 回传 + 拦截返回)

// src/main/ets/pages/Detail.ets
export interface DetailParam {
  id: number
  from: string
}

// 路由表要求的 Builder,参数通过系统注入
@Builder
export function DetailBuilder() {
  // 从 NavPathStack 中取出当前页参数
  const stack: NavPathStack = (getContext(this) as any).pageStack
    ?? AppStorage.get('pageStack') as NavPathStack
  Detail(stack)
}

@Component
struct Detail {
  // 业务页面不直接声明 NavPathStack,由外部注入
  private stack: NavPathStack
  @State message: string = '未修改'

  // 通过 Navigation 的系统机制拿到参数
  @State param: DetailParam = (this.stack.getParamByName('Detail')[0] as DetailParam) ?? { id: 0, from: '' }

  aboutToAppear(): void {
    // 兜底:从注入的栈读取
    const p = this.stack.getParamByName('Detail')[0] as DetailParam
    if (p) { this.param = p }
    console.info('[Detail] 收到参数:', JSON.stringify(this.param))
  }

  build() {
    NavDestination() {
      Column({ space: 16 }) {
        Text(`详情页 id=${this.param.id} from=${this.param.from}`)
          .fontSize(20)
        TextInput({ placeholder: '修改内容', text: this.message })
          .onChange(v => this.message = v)
        Button('保存并返回(回传结果)')
          .onClick(() => {
            // 回传并出栈
            this.stack.pop(this.message)
          })
      }
      .width('100%')
      .height('100%')
      .justifyContent(FlexAlign.Center)
      .padding(24)
    }
    .title('详情')
    // 拦截系统返回:弹确认,避免误丢未保存内容
    .onBackPressed(() => {
      if (this.message.trim() !== '') {
        AlertDialog.show({
          message: '有未保存内容,确认退出?',
          primaryButton: { action: () => this.stack.pop(this.message) }, // 用户确认 -> 回传并退出
          secondaryButton: { action: () => {} } // 取消 -> 留在当前页
        })
        return true // 消费本次返回
      }
      return false // 无改动 -> 走默认返回
    })
  }
}

通用:封装一个跳转工具

// src/main/ets/nav/NavUtils.ets
export class NavUtils {
  static pushDetail(stack: NavPathStack, id: number, onResult?: (r: string) => void): void {
    stack.pushPathByName('Detail', { id, from: 'navutils' }, onResult)
  }

  static popWithResult(stack: NavPathStack, result: string): void {
    stack.pop(result)
  }

  static replace(stack: NavPathStack, name: string, param?: Object): void {
    stack.replacePathByName(name, param)
  }

  static clearToHome(stack: NavPathStack): void {
    stack.clear()
  }
}

总结

  1. 栈要收在自己手里:用 Navigation(this.pageStack) 注入 NavPathStack,所有跳转都是 stack.pushPathByName / pop,告别散落的 router.pushUrl 字符串。
  2. 路由表解耦路径module.json5 + route_map.json 把页面名映射到 Builder,重构只改一处。
  3. 参数类型安全 + 回传pushPathByName(name, param, callback) 既传参又收回传,比 Router 的全局事件干净得多。
  4. 拦截返回用 onBackPressed:返回 true 消费系统返回,可在此做"未保存确认",是 Router 做不到的。
  5. 多端适配NavigationMode.Stack(手机)/ Split(平板侧栏)一行切换,无需为不同设备写两套导航。

把页面栈标准化为 Navigation 后,跨模块协作、参数类型、返回拦截都变得可维护,是 NEXT 应用架构的第一步。

Logo

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

更多推荐