HarmonyOS NEXT 应用开发实战(六、组件导航Navigation使用详解)

在鸿蒙应用开发中,Navigation 组件是实现界面间导航的重要工具。本文将介绍如何使用 Navigation 组件实现页面跳转及参数传递,确保你能轻松构建具有良好用户体验的应用。

当前HarmonyOS支持两套路由机制(Navigation和Router),Navigation作为后续长期演进及推荐的路由选择方案,其与Router比较有不少优势。建议后续直接使用Navigation作为内部的路由方案。

Navigation组件通常作为页面的根容器,支持单页面、分栏和自适应三种显示模式。开发者可以使用Navigation组件提供的属性来设置页面的标题栏、工具栏、导航栏等。

Router路由的页面是一个@Entry修饰的Component,每一个页面都需要在main_page.json中声明。而基于Navigation的路由页面分为导航页和子页,导航页又叫Navbar,是Navigation包含的子组件,子页是NavDestination包含的子组件。

业务场景 Navigation Router
一多能力 支持,Auto模式自适应单栏跟双栏显示 不支持
跳转指定页面 pushPath & pushDestination pushUrl & pushNameRoute
跳转HSP中页面 支持 支持
跳转HAR中页面 支持 支持
跳转传参 支持 支持
获取指定页面参数 支持 不支持
传参类型 传参为对象形式 传参为对象形式,对象中暂不支持方法变量
跳转结果回调 支持 支持
跳转单例页面 不支持 支持
页面返回 支持 支持
页面返回传参 支持 支持
返回指定路由 支持 支持
页面返回弹窗 支持,通过路由拦截实现 showAlertBeforeBackPage
路由替换 replacePath & replacePathByName replaceUrl & replaceNameRoute
路由栈清理 clear clear
清理指定路由 removeByIndexes & removeByName 不支持
转场动画 支持 支持
自定义转场动画 支持 支持,动画类型受限
屏蔽转场动画 支持全局和单次 支持 设置pageTransition方法duration为0
geometryTransition共享元素动画 支持(NavDestination之间共享) 不支持
页面生命周期监听 UIObserver.on('navDestinationUpdate') UIObserver.on('routerPageUpdate')
获取页面栈对象 支持 不支持
路由拦截 支持通过setInercption做路由拦截 不支持
路由栈信息查询 支持 getState() & getLength()
路由栈move操作 moveToTop & moveIndexToTop 不支持
沉浸式页面 支持 不支持,需通过window配置
设置页面标题栏(titlebar)和工具栏(toolbar) 支持 不支持
模态嵌套路由 支持 不支持

主页面(首页面)示例:

TypeScript 复制代码
 
@Entry
@Component
struct NavigationExample {
  build() {
    Column() {
      Navigation() {
        // 中间主区域
      }
      .title("主标题") // 页面标题
      .mode(NavigationMode.Auto) // 显示模式:Auto(自适应)、Stack(单页显示)、Split(分栏显示)、Full(强调型标题栏)、Mini(普通型标题栏)
      .menus([
        // 顶部菜单栏
      ])
      .toolBar({items: [
        // 底部工具栏
      ]})
    }
    .height('100%')
    .width('100%')
    .backgroundColor('#F1F3F5')
  }
}
TypeScript 复制代码
// index.ets
@Entry
@Component
struct Index {
  pathStack: NavPathStack = new NavPathStack()

  build() {
    Navigation(this.pathStack) {
      Column() {
        Button('Push PageOne', { stateEffect: true, type: ButtonType.Capsule })
          .width('80%')
          .height(40)
          .margin(20)
          .onClick(() => {
            this.pathStack.pushPathByName('pageOne', null)
          })
      }.width('100%').height('100%')
    }
    .title("Navigation")
  }
}

子页示例:

TypeScript 复制代码
// PageOne.ets

@Builder
export function PageOneBuilder() {
  PageOne()
}

@Component
export struct PageOne {
  pathStack: NavPathStack = new NavPathStack()

  build() {
    NavDestination() {
      Column() {
        Button('回到首页', { stateEffect: true, type: ButtonType.Capsule })
          .width('80%')
          .height(40)
          .margin(20)
          .onClick(() => {
            this.pathStack.clear()
          })
      }.width('100%').height('100%')
    }.title('PageOne')
    .onReady((context: NavDestinationContext) => {
      this.pathStack = context.pathStack
    })
  }
}

注意:每个子页也需要配置到系统配置文件route_map.json中(参考 系统路由配置 ):

javascript 复制代码
// 工程配置文件module.json5中配置 {"routerMap": "$profile:route_map"}
// route_map.json
{
  "routerMap": [
    {
      "name": "pageOne",
      "pageSourceFile": "src/main/ets/pages/PageOne.ets",
      "buildFunction": "PageOneBuilder",
      "data": {
        "description": "this is pageOne"
      }
    }
  ]
}

NavRouter是Navigation组件中的特殊子组件,通常用于与Navigation组件配合使用,它默认提供了点击响应处理,不需要开发者自定义点击事件逻辑。NavRouter组件只有两个子组件,其中第二个子组件必须是NavDestination。而NavDestination则是Navigation组件中的特殊子组件,用于显示内容页的内容。当开发者点击NavRouter组件时,会跳转到对应的NavDestination内容区。

TypeScript 复制代码
  Navigation() {
    TextInput({ placeholder: '请输入...' })
      .width('90%')
      .height(40)
      .backgroundColor('#ffffff')

    List({ space: 12 }) {
      ForEach(this.arr, item => {
        ListItem() {
          NavRouter() {
            Text("NavRouter" + item)
              .width('100%')
              .height(72)
              .backgroundColor(Color.White)
              .borderRadius(36)
              .fontSize(16)
              .fontWeight(500)
              .textAlign(TextAlign.Center)
            NavDestination() {
              Text(`NavDestinationContent${item}`)
            }
            .title(`NavDestinationTitle${item}`)
          }
        }
      })
    }
  }
  .title('主标题')
  .mode(NavigationMode.Stack)//导航模式
  .titleMode(NavigationTitleMode.Mini)//标题模式
  .menus([  //设置菜单
    { value: "", icon: './../../../resources/base/media/icon.png', action: () => {
    } },
    { value: "", icon: './../../../resources/base/media/icon.png', action: () => {
    } }
  ])
  .toolBar({ items: [  //设置工具栏
    { value: 'func', icon: './../../../resources/base/media/icon.png', action: () => {
    } },
    { value: 'func', icon: './../../../resources/base/media/icon.png', action: () => {
    } }
  ] })

在鸿蒙开发中,NavRouter 和直接使用 this.pageStack.pushDestinationByName 来实现页面跳转的确可以达到相似的效果,但它们在设计理念和使用场景上有一些区别。以下是这两者之间的一些关键点和它们各自的用途:

  • 封装与简化NavRouter 是一个封装了导航逻辑的组件,专注于处理页面间的跳转和路由管理。它提供了一个更简洁的接口以实现页面跳转,可以让开发者更容易进行组件化开发,从而提高代码的可读性和可维护性。

  • 动态路由 :在 NavRouter 中,可以方便地将导航逻辑和目标页面的内容结合在一起。通过 NavRouter,不必手动管理目标页面的具体情况,可以直接利用组件的声明式来定义导航内容。

2. this.pageStack.pushDestinationByName

  • 底层控制 :使用 this.pageStack.pushDestinationByName 更接近底层的 API,适合需要更细粒度控制的情况。它通常适用于需要直接处理路由堆栈的场合。

  • 灵活性:虽然提供了更多的灵活性,但相应的也要求开发者自行管理跳转的状态、参数传递等,代码可能相对繁琐,尤其是在处理复杂的导航场景时。

  • 如果你的应用需要在多个组件之间频繁进行导航,并且希望维护清晰的结构和可读性,使用 NavRouter 是一个不错的选择。

  • 对于简单的情况,如果你的跳转逻辑不复杂,且需要直接控制页面堆栈的行为,使用 this.pageStack.pushDestinationByName 也完全可以。

总结而言,虽然两者都可以实现页面跳转,但 NavRouter 提供了更高层次的抽象,更适合构建组件化的、可维护的导航结构,而 this.pageStack.pushDestinationByName 更适合需要底层控制的场景。选择使用哪一种方式,主要依赖于具体的开发需求和代码结构设计。简言之,NavRouter 有点儿类似于折叠效果,所有内容已经拿到了,不需要再跳转到其他页面去请求,只需展开显示即可。则使用NavRouter 可以完成在一个页面里实现想要的效果。

一、基本使用

将Navigation 组件作为基础页面的根容器,它能够管理整个页面的布局和导航。定义NavPathStack 实例,后面需要用它实现路由跳转和传参。Navigation通过页面栈对象 NavPathStack 提供的方法来操作页面,需要创建一个栈对象并传入Navigation中。

以下是一个基本的 Navigation 组件结构示例:

TypeScript 复制代码
import { getZhiHuNews } from '../../common/api/zhihu';
import { BaseResponse,ErrorResp } from '../../common/bean/ApiTypes';
import { Log } from '../../utils/logutil'


@Component
export default struct ZhiHu{
  @State message: string = 'Hello World';
  private arr: number[] = [1, 2, 3];

  //重要,定义NavPathStack 实例,后面需要用它实现路由跳转和传参
  pageStack: NavPathStack = new NavPathStack()

  // 组件生命周期
  aboutToAppear() {
    Log.info('ZhiHu aboutToAppear');

    getZhiHuNews("20241017").then((res) => {
      Log.debug(res.data.message)
      Log.debug("request","res.data.code:%{public}d",res.data.code)
    }).catch((err:BaseResponse<ErrorResp>) => {
      Log.debug("request","err.data.code:%d",err.data.code)
      Log.debug("request",err.data.message)
    });
    let list: number[] = []
    for (let i = 1; i <= 10; i++) {
      list.push(i);
    }
  }

  // 组件生命周期
  aboutToDisappear() {
    Log.info('ZhiHu aboutToDisappear');
  }

  build() {
    Navigation(this.pageStack){
      Row() {
        Column({ space: 0 }) {
          // 标题栏
          Text("日报")
            .size({ width: '100%', height: 50 })
            .backgroundColor("#28bff1")
            .fontColor("#ffffff")
            .textAlign(TextAlign.Center)
            .fontSize("18fp")

          // 内容项
            Swiper(this.swiperController) {
              LazyForEach(this.data, (item: string) => {
                Stack({ alignContent: Alignment.Center }) {
                  Text(item.toString())
                    .width('100%')
                    .height(160)
                    .backgroundColor(0xAFEEEE)
                    .textAlign(TextAlign.Center)
                    .fontSize(30)
                    .zIndex(1)
                    .onClick(() => {
                      //this.pageStack.pushPathByName("PageOne", item)
                      this.pageStack.pushDestinationByName("PageOne", { id:"9773231" }).catch((e:Error)=>{
                        // 跳转失败,会返回错误码及错误信息
                        console.log(`catch exception: ${JSON.stringify(e)}`)
                      }).then(()=>{
                        // 跳转成功
                      });
                    })

                  // 显示轮播图标题
                  Text("特立独行的猫哥")
                    .padding(5)
                    .margin({ top:60 })
                    .width('100%').height(50)
                    .textAlign(TextAlign.Center)
                    .maxLines(2)
                    .textOverflow({overflow:TextOverflow.Clip})
                    .fontSize(20)
                    .fontColor(Color.White)
                    .opacity(100) // 设置标题的透明度 不透明度设为100%,表示完全不透明
                    .backgroundColor('#808080AA') // 背景颜色设为透明
                    .zIndex(2)

                }
              }, (item: string) => item)
            }
            .cachedCount(2)
            .index(1)
            .autoPlay(true)
            .interval(4000)
            .loop(true)
            .indicatorInteractive(true)
            .duration(1000)
            .itemSpace(0)
            .curve(Curve.Linear)
            .onChange((index: number) => {
              console.info(index.toString())
            })
            .onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {
              console.info("index: " + index)
              console.info("current offset: " + extraInfo.currentOffset)
            })
            .height(160) // 设置高度

          List({ space: 12 }) {
            ForEach(this.arr, (item:string) => {
              ListItem() {
                NavRouter() {
                  Text("NavRouter" + item)
                    .width("100%")
                    .height(72)
                    .backgroundColor('#FFFFFF')
                    .borderRadius(24)
                    .fontSize(16)
                    .fontWeight(500)
                    .textAlign(TextAlign.Center)
                  NavDestination() {
                    Text("NavDestinationContent" + item)
                  }
                  .title("NavDestinationTitle" + item)
                }
              }
            }, (item:string) => item)
          }

        }.size({ width: '100%', height: '100%' })
      }
    }
    .mode(NavigationMode.Stack)
    .width('100%').height('100%')

  }
}

注意: 主页面使用 Navigation 组件作为根容器。而待跳转的子页面(目标页面),则是使用NavDestination作为根容器。

二、配置系统路由表

在进行页面跳转之前,需要在目录src/main/resources/base/profile中,创建文件route_map.json,手动注册跳转的页面。内容示例:

TypeScript 复制代码
{
  "routerMap": [
    {
      "name": "PageOne",
      "pageSourceFile": "src/main/ets/pages/zhihu/detail/Detail.ets",
      "buildFunction": "DetailPageBuilder",
      "data": {
        "description": "this is Page Detail"
      }
    }
  ]
}

上面这个配置其实是系统路由表,从API version 12版本开始,Navigation支持系统跨模块的路由表方案,整体设计是将路由表方案下沉到系统中管理,即在需要路由的各个业务模块(HSP/HAR)中独立配置router_map.json文件,在触发路由跳转时,应用只需要通过NavPactStack进行路由跳转,此时系统会自动完成路由模块的动态加载、组件构建,并完成路由跳转功能,从而实现了开发层面的模块解耦。

在这个示例中,name 定义了路由名称,pageSourceFile 指向页面的具体实现文件,buildFunction 则指定了构建函数。开发者需确保这些字段的正确性和一致性,以便系统能正确识别和加载目标页面。

路由表的基本概念

在新的设计中,每个业务模块(HSP / HAR)都可以独立配置 router_map.json 文件。当应用触发路由跳转时,使用 NavPathStack 内置的接口,系统将自动处理模块的动态加载和组件构建,从而实现路由跳转功能。

鸿蒙的系统路由表方案为应用开发提供了更加灵活和高效的路由管理方式。通过将路由表下沉到模块层,系统支持模块的独立配置和动态加载,不仅实现了开发层面的模块解耦,也大大简化了开发流程。对于正在进行鸿蒙应用开发来说,掌握这一机制将显著提升开发效率和应用性能。

配置路由表注意事项
  1. 注册正确 :确保 namebuildFunction 与要跳转的页面一致。
  2. 参数获取 :在目标页面中,开发者可以使用 this.pageStack.getParamByName 方法获取传递的参数。
路由模块解耦的优势
  • 独立性:每个业务模块可独立管理自己的路由信息,减少各模块间的相互依赖,提高开发效率。
  • 动态加载:通过系统自动处理模块的加载和组件构建,提升了应用的性能和响应速度。
  • 代码可维护性:在日后的维护中,开发者只需关注自身模块的路由配置和实现,对于系统的整体架构影响较小。
路由跳转的实现

使用 NavPathStack 进行路由跳转时,只需调用相应的方法并传递参数,系统会自动执行动态装载和跳转。例如:

TypeScript 复制代码
this.pageStack.pushDestinationByName("DetailPage", { id: "9773231" })
  .catch((error) => {
    console.error(`路由跳转失败: ${JSON.stringify(error)}`);
  })
  .then(() => {
    console.log("路由跳转成功");
  });

在上述代码中,调用 pushDestinationByName 方法完成页面跳转,同时也可以传递必要的参数。其他一些操作还有:

TypeScript 复制代码
@Entry
@Component
struct Index {
  pathStack: NavPathStack = new NavPathStack()

  build() {
    // 设置NavPathStack并传入Navigation
    Navigation(this.pathStack) {
        ...
    }.width('100%').height('100%')
  }
  .title("Navigation")
}

// push page
this.pathStack.pushPath({ name: 'pageOne' })

// pop page
this.pathStack.pop()
this.pathStack.popToIndex(1)
this.pathStack.popToName('pageOne')

// replace page
this.pathStack.replacePath({ name: 'pageOne' })

// clear all page
this.pathStack.clear()

// 获取页面栈大小
let size = this.pathStack.size()

// 删除栈中name为PageOne的所有页面
this.pathStack.removeByName("pageOne")

// 删除指定索引的页面
this.pathStack.removeByIndexes([1,3,5])

// 获取栈中所有页面name集合
this.pathStack.getAllPathName()

// 获取索引为1的页面参数
this.pathStack.getParamByIndex(1)

// 获取PageOne页面的参数
this.pathStack.getParamByName("pageOne")

// 获取PageOne页面的索引集合
this.pathStack.getIndexByName("pageOne")
...

目标页面的实现

下面是目标页面 DetailPage 的一个简单示例:

TypeScript 复制代码
import { Log } from '../../../utils/logutil';

@Builder
export function DetailPageBuilder() {
  DetailPage()
}

@Component
export default struct DetailPage {
  @State message: string = 'Hello World';
  pageStack: NavPathStack = new NavPathStack();
  private pathInfo: NavPathInfo | null = null;

  // 组件生命周期
  aboutToAppear() {
    Log.info('Detail aboutToAppear');
  }

  // 组件生命周期
  aboutToDisappear() {
    Log.info('Detail aboutToDisappear');
  }

  build() {
    NavDestination() {
      Column({ space: 0 }) {
        Text("内容").width('100%').height('100%').textAlign(TextAlign.Center).fontSize("25fp")
      }
    }
    .title("日报详情")
    .width('100%')
    .height('100%')
    .onReady(ctx => {
      this.pageStack = ctx.pathStack;
      const params = this.pageStack.getParamByName("PageOne");
      Log.info('接收到的参数:', params);
    });
  }
}

组件生命周期与传递参数获取

DetailPageonReady 方法中,开发者可以获取到传递的参数。这里使用 this.pageStack.getParamByName("PageOne") 获取指定页面的参数。这样可以灵活应对数据的传递和使用。

在导航页面中传递参数

使用 this.pageStack.pushDestinationByName 方法跳转到目标页面,并传递参数。例如:

TypeScript 复制代码
this.pageStack.pushDestinationByName("TargetPage", { id: "12345", name: "示例数据" })
  .catch((error) => {
    console.error(`路由跳转失败: ${JSON.stringify(error)}`);
  });
TypeScript 复制代码
import { Log } from '../../../utils/logutil';

@Builder
export function TargetPageBuilder() {
  TargetPage();
}

@Component
export default struct TargetPage {
  @State message: string = 'Hello from Target Page';
  pageStack: NavPathStack = new NavPathStack();
  private pathInfo: NavPathInfo | null = null;

  // 组件生命周期方法,页面即将出现
  aboutToAppear() {
    Log.info('TargetPage aboutToAppear');
  }

  // 组件生命周期方法,页面即将消失
  aboutToDisappear() {
    Log.info('TargetPage aboutToDisappear');
  }

  build() {
    NavDestination() {
      Column({ space: 0 }) {
        Text("内容").width('100%').height('100%').textAlign(TextAlign.Center).fontSize("25fp");
      }
    }
    .title("目标页面")
    .width('100%')
    .height('100%')
    .onReady(ctx => {
      this.pageStack = ctx.pathStack; // 获取当前的路径栈
      const params = this.pageStack.getParamByName("TargetPage"); // 获取传递的参数
      Log.info('接收到的参数:', params);

      // 例如,如果传递的数据为 { id: "12345", name: "示例数据" }
      if (params) {
        const id = params.id; // 获取具体的参数值
        const name = params.name;
        Log.info(`获取到的 ID: ${id}, 名称: ${name}`);
      }
    });
  }
}

总结

通过使用 Navigation 组件及其相关接口,开发者可以方便地实现页面间的跳转和数据传递,从而构建丰富且流畅的用户界面。将页面逻辑与导航结构紧密结合,可以极大提升应用的可维护性和用户体验。希望本篇文章能帮助到您在鸿蒙应用开发中的导航实现。

写在最后

最后,推荐下笔者的业余开源app影视项目"爱影家",推荐分享给与我一样喜欢免费观影的朋友。【注:该项目仅限于学习研究使用!请勿用于其他用途!】

开源地址: 爱影家app开源项目介绍及源码

https://gitee.com/yyz116/imovie

💖技术交流

​​​​​​​

其他资源

文档中心--组件导航 (Navigation)(推荐)

HarmonyOS:NavPathStack的详细使用说明以及示例-CSDN博客

HarmonyOS Next开发学习手册------组件导航 (Navigation) (推荐)_鸿蒙 navigation-CSDN博客

「HarmonyNextOS」页面路由跳转Router更换为Navigation_鸿蒙 routermap-CSDN博客

CommonAppDevelopment/common/routermodule/README_AUTO_GENERATE.md · HarmonyOS-Cases/Cases - Gitee.com

SystemRouterMap: 本项目提供系统路由的验证,运用系统路由表的方式,跳转到模块(HSP/HAR)的页面,可以不用配置不同跳转模块间的依赖。

HarmonyOS ArkUI实战开发-页面导航(Navigation)_arkui navigation-CSDN博客

【鸿蒙实战开发】基于Navigation的路由管理_navigation组件关联的路由栈提供了入栈方法-CSDN博客 OpenHarmony三方库中心仓--HMRouter

https://juejin.cn/post/7372488623944630281

如何开发一个OpenHarmony购物app导航页面-鸿蒙开发者社区-51CTO.COM

鸿蒙HarmonyOS实战-ArkUI组件(Navigation)_harmonyos navigation-CSDN博客

【HarmonyOS NEXT 】应用开发:ArkTS导航组件一(Navigation)_arkts navigation-CSDN博客

https://zhuanlan.zhihu.com/p/1076639693

相关推荐
Freerain991 小时前
鸿蒙Next ArkTS语法适配背景概述
华为·harmonyos
他的猫哎1 小时前
鸿蒙 Navigation组件下的组件获取pageStack问题
harmonyos·鸿蒙
雨汨1 小时前
鸿蒙之路的坑
华为·harmonyos
轻口味3 小时前
【每日学点鸿蒙知识】沙箱目录、图片压缩、characteristicsArray、gm-crypto 国密加解密、通知权限
pytorch·华为·harmonyos
xo198820116 小时前
鸿蒙人脸识别
redis·华为·harmonyos
洗发水很好用7 小时前
uniApp打包H5发布到服务器(docker)
uni-app
塞尔维亚大汉7 小时前
【OpenHarmony】 鸿蒙 UI开发之CircleIndicator
harmonyos·arkui
BisonLiu7 小时前
华为仓颉鸿蒙HarmonyOS NEXT仓颉原生数据网络HTTP请求(ohos.net.http)
harmonyos
BisonLiu7 小时前
华为仓颉鸿蒙NEXT原生加解密算法库框架
harmonyos
变色龙云7 小时前
网页生成鸿蒙App
华为·harmonyos