基于Navigation的路由管理(下篇)

科技   2024-09-13 17:31   广东  

一、基于Navigation的路由管理——常见业务功能场景

基于Dialog类型NavDestination,实现弹窗页面跳转返回后弹窗不关闭

NavDestination有两个类型,通过mode属性进行配置,前文介绍的NavDestination均是STANDARD类型。

名称描述
STANDARD标准类型NavDestination的生命周期跟随NavPathStack栈中标准Destination变化而改变。
DIALOG默认透明,进出页面栈不影响下层NavDestination的生命周期。
DIALOG类型的NavDestination背景透明,且不会影响其他NavDestination生命周期,即前面的页面不会隐藏,因此比较适合开发类似地图导航场景的应用,此类应用特点是:底层一个固定的页面,其余页面都是覆盖在底层页面之上,但是底层页面始终可见。mode为DIALOG的NavDestination在转入和转出时,默认不支持动画,可以通过自定义动画的方式配置动画。

弹窗可以通过Dialog来实现。Dialog实现的弹窗可以实现解耦,还可以实现弹窗跳转页面返回,弹窗不关闭等效果(比如隐私弹窗,该弹窗还可以接续跳转到隐私条款页)。

构建Dialog类型的NavDestination,因为Dialog类型页面背景是透明的,为了更好的效果,可以增加一层蒙层。如果要对弹窗组件增加类似移动出现效果,需要在组件中自行实现。

@Componentexport struct PrivacyDialog {  @Consume('pageInfo') pageStack: NavPathStack;  @State isAgree: string = 'Not Agree';
build() { NavDestination() { Stack({ alignContent: Alignment.Center }) { // 蒙层 Column() .width('100%') .height('100%') .backgroundColor('rgba(0,0,0,0.5)')
// 隐私弹窗 Column() { Text('注册应用账号') .fontSize(30) .height('20%') Text('请您仔细阅读一下协议并同意,我们将全力保护您的个人信息安全,您可以使用账号登录APP。') .height('40%') Divider() Row() { // 点击隐私条款,跳转到隐私条款页面,并接受隐私条款的返回值,用来刷新页面的同意状态。 Text('《应用隐私政策》') .onClick(() => { let pathInfo: NavPathInfo = new NavPathInfo('PrivacyItem', null, (popInfo: PopInfo) => { this.isAgree = popInfo.result.toString(); }); this.pageStack.pushDestination(pathInfo, true); }) Text(this.isAgree) } .height('20%')
Divider() // 点击同意/不同意按钮,将状态返回登录页 Row() { Button('不同意') .onClick(() => { this.pageStack.pop('Not Agree', true); }) .width('30%') Button('同意') .onClick(() => { this.pageStack.pop('Agree', true); }) .width('30%') } .height('20%') } .backgroundColor(Color.White) .height('50%') .width('80%') } } .hideTitleBar(true) // 设置Dialog类型 .mode(NavDestinationMode.DIALOG) }}

基于页面生命周期监听,实现页面埋点

在应用运维时,需要了解哪些业务功能用户使用频率较高,为了实现此能力,可以在页面显示/隐藏的的时候进行相应的埋点操作,以方便后台进行统计分析。此类诉求可以通过DevNavigation的状态监听实现。

可以在Ability中的onWindowStageCreate方法中通过

uiObserver.on('navDestinationUpdate',(info) => {})方法注册DevNavigation的状态监听,样例代码通过打样日志的方式记录各个DevNavigation显示/隐藏时的状态,在真实业务中可以相应进行替换。

export default class EntryAbility extends UIAbility {  ...  onWindowStageCreate(windowStage: window.WindowStage): void {    ...    windowStage.getMainWindow((err: BusinessError, data) => {      ...      windowClass = data;      // 获取UIContext实例      let uiContext: UIContext = windowClass.getUIContext();      // 获取UIObserver实例      let uiObserver: UIObserver = uiContext.getUIObserver();      // 注册DevNavigation的状态监听.      uiObserver.on('navDestinationUpdate', (info) => {        // NavDestinationState.ON_SHOWN = 0, NavDestinationState.ON_HIDE = 1        if (info.state == 0) {          // NavDestination组件显示时操作          console.info('page ON_SHOWN: ' + info.name.toString());        } else if (info.state === 1) {          // NavDestination组件隐藏时操作          console.info('page ON_HIDE' + info.name.toString());        } else {          // NavDestination组件其他操作          console.info('page state: ' + info.state);        }      });    });  }}

基于路由拦截实现页面返回弹窗确认

业务场景:要针对页面进行统一的逻辑处理,比如在页面返回时根据页面参数配置规则匹配是否需要给出弹窗提示,若规则匹配弹框,否则默认走返回流程。
为了实现此功能,需要依赖路由拦截能力,通过路由跳转拦截,实现控制路由跳转,弹窗或阻止路由跳转等操作。通过setInterception方法,即可设置Navigation页面跳转拦截回调。该方法需要传入一个NavigationInterception对象,该对象包含三个回调函数:
名称描述
willShow页面跳转前拦截,允许操作栈,在当前跳转中生效。
didShow页面跳转后回调。在该回调中操作栈在下一次跳转中刷新。
modeChangeNavigation单双栏显示状态发生变更时触发该回调。

如上所述,willShow会在from页面动效完成之前回调;didiShow会在from页面动效完成之后回调。此处需要注意的是无论哪个回调,在进入回调时页面栈都已经发生了变化。

案例中,由于在弹出确认框时,不能出现登录页面返回的转场动画,因此需要在willShow回调中执行相关业务。代码中使用了系统路由表框架,具体实现细节参考下文中路由框架。

  1. 代码判断srcPage为登录页面LoginPageView,targetPage为主页面MainPage是执行弹窗逻辑。
  2. 由于LoginPageView此时已经出栈,需要重新将LoginPageView重新入栈。此处需要注意需要将参数重新传递给LoginPageView,否则无法顺利完成页面初始化。如果需要保持页面之前的输入值,则需要为组件独立注册LocalStorage以保持状态。
  3. 弹窗ConfirmDialog通过Dialog类型的NavDestination实现,启动ComfirmDialog时,将点击OK和Cancel的回调一并传入,从而提高ComfirmDialog的独立封装能力。
  4. 当点击‘OK’确认返回时,需要将ConfirmDialog和LoginPageView同时出栈。此时需要注意这一帧的起点栈为Dialog类型,因此不会有转场动画,如果需要转场动画,需要通过自定义动画实现。
export function registerInterception() {  RouterManager.setInterception({    willShow: (from: NavDestinationContext | 'navBar', to: NavDestinationContext | 'navBar',      operation: NavigationOperation, animated: boolean) => {      if (typeof to === 'string') {        console.log('target page is navigation home');        return;      }      if (typeof from === 'string') {        console.log('target page is navigation home');        return;      }      // redirect target page.Change pageTwo to pageOne.      let target: NavDestinationContext = to as NavDestinationContext;      let srcPage: NavDestinationContext = from as NavDestinationContext;      console.log('==== setInterception target.pathInfo.name = ' + target.pathInfo.name);      if (target.pathInfo.name === 'MainPage' && srcPage.pathInfo.name === 'LoginPage') {        RouterManager.pushPath('LoginPage', srcPage.pathInfo.param, srcPage.pathInfo.onPop, true);        let confirmDialogData = new ConfirmDialogData();        confirmDialogData.onCancelClick = () => {          RouterManager.popWithoutParam(false);        }        confirmDialogData.onConfirmClick = () => {          RouterManager.popWithoutParam(true);          RouterManager.popWithoutParam(true);        }        RouterManager.pushPath('confirmDialog', confirmDialogData, () => {        }, false)      }    },    didShow: (from: NavDestinationContext | 'navBar', to: NavDestinationContext | 'navBar',      operation: NavigationOperation, isAnimated: boolean) => {    },    modeChange: (mode: NavigationMode) => {    }  });}

补充一些有用的知识

从案例中可以看出,LoginPage经历了出栈再入栈两个动作,即使再次入栈时,将初始的入参重新传递了回去,但是页面表单之前填入的信息仍然无法恢复,也就是如果之前表单填入了'abc',再次入栈时显示的时初始值,不是'abc'。为了解决这个问题,此时可以采用组件级别的LocalStorage来解决。
let localStorageLoginPage: LocalStorage = new LocalStorage();localStorageLoginPage.setOrCreate('userName', '');
@Componentexport struct LoginPageView { @LocalStorageLink('userName') userName: string = ''; @State initUserName: string = '';
build() { NavDestination() { TextInput({ placeholder: 'User Name', text: this.initUserName }) .onChange((value: string) => { this.userName = value; }) .width('80%') }
.hideTitleBar(true) .onReady((navDestinationContext) => { this.loginParam = navDestinationContext.pathInfo.param as LoginParamInHAR; // 判断页面初始输入框初始阶段显示的指是来自传参还是localStorage if (this.userName === '') { this.initUserName = this.loginParam.userName === 'not login' ? '' : this.loginParam.userName; } else { this.initUserName = this.userName; } }) }}
@Builderexport function getLoginPage(): void { LoginPageView({}, localStorageLoginPage);}

二、路由框架

大型应用为了实现更好的模块间解耦,往往会设计一套路由框架,用于解耦各个模块间的路由关系(A模块不感知路由具体通过哪个模块实现、如何实现,只通过路由名称实现路由跳转,闭环业务功能)

系统跨模块路由框架

目前,系统已支持跨模块的路由表方案,整体设计是基于模块路由表的思想。即在需要路由的各个业务模块(HSP/HAR)中独立配置router_map.json文件,在触发路由跳转时,应用只需要通过NavPactStack进行路由跳转。此时系统会完成路由模块的动态加载、组件构建,并完成路由跳转功能,从而实现了开发层面的模块接口。
Step1:在需要配置路由表的模块的module.json5中添加路由表配置
{  "module" : {    ...    "routerMap": "$profile:router_map"  }}

Step2:在src\main\resources\base\profile(若之前没有profile目录,则需要新建)目录下新建router_map.json文件,此处文件名称需要与module.json5中配置的文件名称一致。

配置项说明
name跳转页面名称,用于路由跳转时使用,如this.pageStack.pushPathByName('MainPage', null, false);
pageSourceFile跳转目标页在包内的路径,相对src目录的相对路径。
buildFunction跳转目标页的入口函数名称,必须以@Builder修饰。
data应用自定义字段。可以通过配置项读取接口getConfigInRouteMap获取。
{  "routerMap": [    {      "name": "MainPage",      "pageSourceFile": "src/main/ets/components/mainpage/MainPage.ets",      "buildFunction": "getMainPageRouterMap",      "data": {        "description" : "this is mainPage"      }    },    {      "name": "PersonDetail",      "pageSourceFile": "src/main/ets/components/mainpage/PersonDetail.ets",      "buildFunction": "getPersonDetailRouterMap",      "data": {        "description" : "this is PersonDetailPage"      }    }  ]}

Step3:Navigation主页开发:使用系统路由表方案可以不配置.navDestination()属性,如若配置.navDestination()属性,则会先查询.navDestination()中配置的路由,再查询router_map中的路由。

@Entry@Componentstruct IndexRouterMap {  @Provide('pageInfo') pageStack: NavPathStack = new NavPathStack();
build() { Navigation(this.pageStack) { ... } .onAppear(() => { this.pageStack.pushPathByName('MainPage', null, false); }) .hideNavBar(true) }}

更进一步,可以进行封装,将pageStack封装到路由管理对象中,避免通过Provide的方式传递pageStack。

@Entry@Componentstruct IndexRouterMap {  private pageStack: NavPathStack = new NavPathStack();
aboutToAppear(): void { RouterManager.createNavPathStack(this.pageStack); ... }
build() { Navigation(this.pageStack) { ... } .onAppear(() => { RouterManager.pushPath('MainPage', null, () => {}, false); }) .hideNavBar(true) }}

Step4:NavDestination页面开发:与不使用路由框架对比,采用路由框架后,需要注意一下两点:

  1. 页面需要增加@Builder函数,函数名称与router_map.json中的buildFunction属性必须一致。
  2. 在router_map.json中配置的data信息,通过onReady回调中执行navDestinationContext.getConfigInRouteMap()获取。
@Componentexport struct MainPage {  ...  build() {    NavDestination() {      ...    }    .hideTitleBar(true)    .onReady(navDestinationContext => {      let config: RouteMapConfig | undefined = navDestinationContext.getConfigInRouteMap();      if (config) {        let value: string = config.data['description'];        ...      }    })  }}
@Builderexport function getMainPageRouterMap(): void { MainPage();}

特别注意

由于路由表框架涉及SDK和编译构建多个阶段,在工程整体迁移到新SDK版本后,需要手动更新oh-package.json5的devDependencies项下下依赖信息,具体版本号建议通过新建工程的方法确认。
{  ...  "devDependencies": {    "@ohos/hypium": "1.0.19",    "@ohos/hamock": "1.0.1-rc2"  }}

自定义跨模块路由框架

应用可以自定义实现路由框架,整体方案如下:
  1. 定义个路由管理模块,各个提供路由页面的模块均依赖此模块。

  2. 构建Navgation组件时,将NavPactStack注入路由管理模块,路由管理模块对NavPactStack进行封装,对外提供路由能力。

  3. 各个路由页面不再提供组件,转为提供@Builder封装的构建函数,并通过WrappedBuilder封装后,实现全局封装。

  4. 各个路由页面将模块名称、路由名称、WrappedBuilder封装后构建函数注册如路由模块。

  5. 当路由需要跳转到指定路由时,路由模块完成对指定路由模块的动态导入,并完成路由跳转。

三、Router切换Navigation

对于原先使用Router的应用,建议尽快切换Navigation,避免由于页面的持续增加导致迁移工作量变大。

Navigation基础容器构建

在应用路由根节点新增Navigation节点,定义好pageStack,pageMap及Navigation,将页面原先的内容封装入Navigation容器。若不使用路由框架,则此时需要将全量路由在pageMap中声明,也可以在切换时新增使用路由框架。
具体步骤可以参考页面间跳转

页面切换为NavDestination组件

业务模块页面新增NavDestination组件,删除@Entry装饰器,并export导出。如果切换时新增使用路由框架,此时需要同时注册路由

转场动画切换

Router通过pageTransition设置页面间转场动画,Navigation中的页面是组件级的,都隶属于一个页面,因此需要通过Navigation的customNavContentTransition事件实现自定义转场。

Router相关路由能力替换

Router 能力Navigation 能力
pushUrlpushDestination
pushNamedRoutepushDestination
replaceUrlreplacePath
replaceNamedRoutereplacePath
backpop
showAlertBeforeBackPage\hideAlertBeforeBackPage可自定义 ,参考生命周期监听
clearclear
getLengthsize
getStategetAllPathName
自定义转场动画参考自定义转场动画
共享元素动画参考共享元素转场

Router路由配置信息从main_pages.json中移除

模块中的main_pages.json文件删除转换为NavDestination的页面路由信息。此处初始页面需要保留,计划仍然使用router的页面也需要保留(并不推荐这样做)。

生命周期切换

Router路由页面切换为NavDestination路由后,之前每个页面的onPageShow、onPageHide生命周期函数不再生效,需要替换为NavDestination路由相关生命周期。

路由框架切换

部分应用如果之前基于Router构建了路由框架,则需要对路由框架就行Navigation化处理。具体过程可以参考系统跨模块路由框架。
Step1:完成各个页面的Navigation/NavDestination改造。
Step2:页面切换为@Builder构建函数。
Step3:完成系统路由配置,并将NavPathSatck传入路由模块。
Step4:路由框架router相关方法通过NavPathSatck相关方法进行替换。
Step5:给涉及路由的模块配置路由表router_map.json。

四、Navigation常见问题

Q:Router和Navigation是否可以混用?
A:可以混用,但是推荐使用Navigation进行开发。
Q:Navigation页面层级是否有限制?
A:无限制。
Q:Navigation传递参数时,参数对象里面是否可以有方法?
A:参数对象可以包含方法,目标页面可以调用对象方法。
Q:如何查询一个组件属于哪个页面?
A:在组件中执行
this.queryNavDestinationInfo()?.name方法,获取NavDestination名称。
Q:如何在页面获取NavPathStack?
A:在组件中执行
this.queryNavigationInfo()?.pathStack方法,获取pathStack路由栈对象。
更多HarmonyOS示例代码详情见官网:
https://developer.huawei.com/consumer/cn/samples/
最佳实践:
https://developer.huawei.com/consumer/cn/doc/best-practices-V5/bpta-harmonyos-features-V5?catalogVersion=V5
Codelabs:
https://developer.huawei.com/consumer/cn/codelabsPortal/serviceTypes?serviceId=43

最佳实践官网链接:

https://developer.huawei.com/consumer/cn/doc/best-practices-V5/bpta-harmonyos-features-V5?catalogVersion=V5

FAQ链接:

https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs-V5/faqs-business-scenarios-and-solutions-V5

开发指南链接:

https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/application-dev-guide-V5?catalogVersion=V5

API参考链接:

https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/development-intro-api-V5?catalogVersion=V5

更多推荐
点击下方图片链接,查看更多栏目内容

HarmonyOS开发者技术
HarmonyOS开发者提供HarmonyOS关键技术解析、版本更新、Codelabs实践和活动资讯,欢迎各位开发者加入鸿蒙生态,一起创造无限可能!
 最新文章