勿忘初心做自己 发表于 2024-11-17 09:42:09

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

在鸿蒙应用开发中,Navigation 组件是实现界面间导航的重要工具。本文将介绍怎样利用 Navigation 组件实现页面跳转及参数传递,确保你能轻松构建具有良好用户体验的应用。
当前HarmonyOS支持两套路由机制(Navigation和Router),Navigation作为后续长期演进及推荐的路由选择方案,其与Router比较有不少优势。发起后续直接利用Navigation作为内部的路由方案。
Navigation介绍

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

Router路由的页面是一个@Entry修饰的Component,每一个页面都需要在main_page.json中声明。而基于Navigation的路由页面分为导航页和子页,导航页又叫Navbar,是Navigation包含的子组件,子页是NavDestination包含的子组件。
业务场景NavigationRouter一多能力支持,Auto模式自适应单栏跟双栏表现不支持跳转指定页面pushPath & pushDestinationpushUrl & pushNameRoute跳转HSP中页面支持支持跳转HAR中页面支持支持跳转传参支持支持获取指定页面参数支持不支持传参类型传参为对象形式传参为对象形式,对象中暂不支持方法变量跳转结果回调支持支持跳转单例页面不支持支持页面返回支持支持页面返回传参支持支持返回指定路由支持支持页面返回弹窗支持,通过路由拦截实现showAlertBeforeBackPage路由更换replacePath & replacePathByNamereplaceUrl & replaceNameRoute路由栈清算clearclear清算指定路由removeByIndexes & removeByName不支持转场动画支持支持自定义转场动画支持支持,动画类型受限屏蔽转场动画支持全局和单次支持 设置pageTransition方法duration为0geometryTransition共享元素动画支持(NavDestination之间共享)不支持页面生命周期监听UIObserver.on('navDestinationUpdate')UIObserver.on('routerPageUpdate')获取页面栈对象支持不支持路由拦截支持通过setInercption做路由拦截不支持路由栈信息查询支持getState() & getLength()路由栈move操作moveToTop & moveIndexToTop不支持沉浸式页面支持不支持,需通过window配置设置页面标题栏(titlebar)和工具栏(toolbar)支持不支持模态嵌套路由支持不支持 Navigation简单示例

主页面(首页面)示例:

@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')
}
} // 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")
}
}  子页示例:
// 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中(参考 系统路由配置 ):
// 工程配置文件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介绍

NavRouter是Navigation组件中的特别子组件,通常用于与Navigation组件共同利用,它默认提供了点击响应处理,不需要开发者自定义点击事件逻辑。NavRouter组件只有两个子组件,其中第二个子组件必须是NavDestination。而NavDestination则是Navigation组件中的特别子组件,用于表现内容页的内容。当开发者点击NavRouter组件时,会跳转到对应的NavDestination内容区。
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 来实现页面跳转的确可以达到相似的结果,但它们在计划理念和利用场景上有一些区别。以下是这两者之间的一些关键点和它们各自的用途:
1. NavRouter 的用途



[*] 封装与简化:NavRouter 是一个封装了导航逻辑的组件,专注于处理页面间的跳转和路由管理。它提供了一个更简洁的接口以实现页面跳转,可以让开发者更轻易进行组件化开发,从而提高代码的可读性和可维护性。
[*] 动态路由:在 NavRouter 中,可以方便地将导航逻辑和目标页面的内容结合在一起。通过 NavRouter,不必手动管理目标页面的具体情况,可以直接利用组件的声明式来定义导航内容。
2. this.pageStack.pushDestinationByName



[*] 底层控制:利用 this.pageStack.pushDestinationByName 更靠近底层的 API,得当需要更细粒度控制的情况。它通常适用于需要直接处理路由堆栈的场所。
[*] 灵活性:虽然提供了更多的灵活性,但相应的也要求开发者自行管理跳转的状态、参数传递等,代码可能相对繁琐,尤其是在处理复杂的导航场景时。
3. 何时利用 NavRouter



[*] 如果你的应用需要在多个组件之间频繁进行导航,并且希望维护清楚的结构和可读性,利用 NavRouter 是一个不错的选择。
[*] 对于简单的情况,如果你的跳转逻辑不复杂,且需要直接控制页面堆栈的举动,利用 this.pageStack.pushDestinationByName 也完全可以。
总结而言,虽然两者都可以实现页面跳转,但 NavRouter 提供了更高层次的抽象,更得当构建组件化的、可维护的导航结构,而 this.pageStack.pushDestinationByName 更得当需要底层控制的场景。选择利用哪一种方式,主要依赖于具体的开发需求和代码结构计划。简言之,NavRouter 有点儿类似于折叠结果,所有内容已经拿到了,不需要再跳转到其他页面去哀求,只需睁开表现即可。则利用NavRouter 可以完成在一个页面里实现想要的结果。
Navigation利用步调

一、基本利用

将Navigation 组件作为底子页面的根容器,它可以或许管理整个页面的结构和导航。定义NavPathStack 实例,反面需要用它实现路由跳转和传参。Navigation通过页面栈对象 NavPathStack 提供的方法来操作页面,需要创建一个栈对象并传入Navigation中。
以下是一个基本的 Navigation 组件结构示例:
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[] = ;

//重要,定义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,手动注册跳转的页面。内容示例:
{
"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 内置的接口,系统将自动处理模块的动态加载和组件构建,从而实现路由跳转功能。
鸿蒙的系统路由表方案为应用开发提供了更加灵活和高效的路由管理方式。通过将路由表下沉到模块层,系统支持模块的独立配置和动态加载,不光实现了开发层面的模块解耦,也大大简化了开发流程。对于正在进行鸿蒙应用开发来说,掌握这一机制将显著提升开发服从和应用性能。
配置路由表注意事项


[*]注册正确:确保 name 和 buildFunction 与要跳转的页面一致。
[*]参数获取:在目标页面中,开发者可以利用 this.pageStack.getParamByName 方法获取传递的参数。
路由模块解耦的优势



[*]独立性:每个业务模块可独立管理自己的路由信息,减少各模块间的相互依赖,提高开发服从。
[*]动态加载:通过系统自动处理模块的加载和组件构建,提升了应用的性能和响应速度。
[*]代码可维护性:在日后的维护中,开发者只需关注自身模块的路由配置和实现,对于系统的团体架构影响较小。
路由跳转的实现

利用 NavPathStack 进行路由跳转时,只需调用相应的方法并传递参数,系统会自动实验动态装载和跳转。例如:
this.pageStack.pushDestinationByName("DetailPage", { id: "9773231" })
.catch((error) => {
    console.error(`路由跳转失败: ${JSON.stringify(error)}`);
})
.then(() => {
    console.log("路由跳转成功");
}); 在上述代码中,调用 pushDestinationByName 方法完成页面跳转,同时也可以传递须要的参数。其他一些操作还有:
@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()

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

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

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

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

下面是目标页面 DetailPage 的一个简单示例:
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);
    });
}
} 组件生命周期与传递参数获取

在 DetailPage 的 onReady 方法中,开发者可以获取到传递的参数。这里利用 this.pageStack.getParamByName("PageOne") 获取指定页面的参数。这样可以灵活应对数据的传递和利用。
在导航页面中传递参数:
利用 this.pageStack.pushDestinationByName 方法跳转到目标页面,并传递参数。例如:
this.pageStack.pushDestinationByName("TargetPage", { id: "12345", name: "示例数据" })
.catch((error) => {
    console.error(`路由跳转失败: ${JSON.stringify(error)}`);
}); 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
页: [1]
查看完整版本: HarmonyOS NEXT 应用开发实战(六、组件导航Navigation利用详解)