鸿蒙技能分享:Navigation页面管理-鸿蒙@fw/router框架源码解析(二) ...

打印 上一主题 下一主题

主题 984|帖子 984|积分 2952

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x

theme: smartblue

本文是系列文章,其他文章见:
鸿蒙@fw/router框架源码解析(一)-Router页面管理




鸿蒙@fw/router框架源码解析

介绍

@fw/router是在HarmonyOS鸿蒙体系中开辟应用所使用的开源模块化路由框架。
该路由框架基于模块化开辟思想设计,支持页面路由和服务路由,支持自定义装饰器主动注册,与体系路由相比使用更便捷,功能更丰富。
具体功能介绍见https://harmonyosdev.csdn.net/67484183522b003a5471c3f3.html@fw/router:鸿蒙模块化路由框架,助力开辟者实现高效模块化开辟!
基于模块化的开辟需求,本框架支持以下功能:


  • 支持页面路由和服务路由;
  • 页面路由支持多种模式(router模式,Navigation模式,混合模式);
  • router模式支持打开非命名路由页面;
  • 页面打开支持多种方式(push/replace),参数转达;关闭页面,返回指定页面,获取返回值,跨页面获取返回值;
  • 支持服务路由,可使用路由url调用公共方法,达到跨技能栈调用以及代码解耦的目的;
  • 支持页面路由/服务路由通过装饰器主动注册;
  • 支持动态导入(在打开路由时才import对应har包),支持自定义动态导入逻辑;
  • 支持添加拦截器(打开路由,关闭路由,获取返回值);
  • Navigation模式下支持自定义Dialog对话框;
详见gitee传送门
代码解析

Navigation页面

页面注册@NavigationRoute

  1. @NavigationRoute({ routeName: "testPage", hasParams: true })
  2. @Component
  3. export struct TestDestination {
  4.   @Prop params?: Record<string, ESObject>
  5.   build() {
  6.     Column() {
  7.       NavDestination() {
  8.         TestPageContent({ pageName: 'TestDestination', params: this.params })
  9.       }
  10.     }
  11.   }
  12. }
复制代码
Navigation页面注册使用了自定义的类装饰器@NavigationRoute。我们来看一下实在现:  
  1. export function NavigationRoute(options: RouteRegisterOptions) {
  2.   return (target: ESObject) => {
  3.   }
  4. }
复制代码
我们发现,该装饰器的实当代码是个空方法,空方法的话怎样实现页面注册呢?
这是因为在ArkTS中,struct无法使用自定义的装饰器,虽然IDE编译不会报错,但是这个装饰器代码根本不会实行。
那么,Navigation页面到底是怎么完成注册的?
答案是:FWRouterHvigorPlugin。
在这个hvigor插件中,插件代码扫描模块中的.eta文件,解析ts语法,当发现装饰器@NavigationRoute时,就会将它所装饰的类名提取出来,然后生成对应的builder和主动注册代码。具体如下:  
  1. @Builder
  2. function testDestinationBuilder(params: ESObject) {
  3.   TestDestination({ params: params });
  4. }
  5. @RouterClassProvider({ routeName: 'testPage', builder: wrapBuilder(testDestinationBuilder) })
  6. export class TestDestinationProvider {
  7. }
复制代码
我们看到插件生成了两部分代码,testDestinationBuilder是对Navigation页面TestDestination的包装,这是ArkTS的要求。
具体缘故原由可以检察鸿蒙应用开辟从入门到入魔:Navigation路由管理为什么这么麻烦?
这里有一个细节,就是TestDestination({ params: params })的参数params。因为不是所有的页面都是有入参的,那理论上params是有时间需要传值,有时间不需要传值。
虽然我们可以简化逻辑,强制所有页面都转达params,但这样就导致了即便是不需要参数的页面也需要增加定义@Prop params?: Record<string, ESObject>。
这种处理方法无疑有点粗暴,以是我们选择给NavigationRoute的入参增加hasParams参数,当参数值为true时,传值params参数,当值为false时,不传值,比如TestDestination()。
插件生成的代码中尚有一个TestDestinationProvider,它的作用是什么?
实在,testDestinationBuilder只是必须的代码模板,不是我们自己想要的。@RouterClassProvider({ routeName: 'testPage', builder: wrapBuilder(testDestinationBuilder) })这是焦点逻辑。
我们来看@RouterClassProvider的实当代码:  
  1. export function RouterClassProvider(options: RouterClassProviderOptions) {
  2.   return (target: ESObject) => {
  3.     RouterManagerForNavigation.getInstance().registerBuilder(options.routeName, options.builder)
  4.   }
  5. }
复制代码
我们看到这个装饰器真正调用了RouterManagerForNavigation中的注册方法,将路由名和页面builder的匹配关系注册到了管理器中。
那么,为什么要这样实现呢?
我们想要的实在就只有一个@RouterClassProvider装饰器,但装饰器不能单独使用,必须装饰在一个类上,以是我们定义了TestDestinationProvider类。
而TestDestination不能直接拿来注册,必须包装进@builder,以是我们定义了testDestinationBuilder。
除此之外,@RouterClassProvider装饰器的触发时机是其所在的文件被import的时间。
因此,在har包中,我们需要将生成的代码文件主动添加到模块的index.ets中去。
  1. export * from './src/main/ets/generated/RouterBuilder';
复制代码
在entry中,我们需要在EntryAbility.ets中导入。
  1. import('../generated/RouterBuilder');
复制代码
以上是hvigor插件为了完成Navigation页面所做的事情,至于方案为什么是这样,建议详细检察具体缘故原由可以检察鸿蒙应用开辟从入门到入魔:Navigation路由管理为什么这么麻烦?。
打开页面

对于@fw/router而言,打开router页面和Navigation页面都是一样,因此使用完全雷同的api,所从前面的openWithRequest、_realOpen、open等方法逻辑完全一致,此处不再赘述。
RouterManagerForNavigation.open

  1.   open(request: RouterRequestWrapper): Promise<RouterResponse> {
  2.     return new Promise((resolve, reject) => {
  3.       if (!this.currentNavPathStack) {
  4.         resolve(RouterResponseError.RequestNotFoundResponsor)
  5.         return
  6.       }
  7.       if (!this.canOpen(request.routeName)) {
  8.         resolve(RouterResponseError.RequestNotFoundResponsor)
  9.         return
  10.       }
  11.       switch (request.rawRequest.openMode) {
  12.         case PageRouteOpenMode.replace:
  13.           this.currentNavPathStack!.replacePath({
  14.             name: request.routeName, param: request.params
  15.           })
  16.           request.resolve = resolve;
  17.           this.inject(request)
  18.           break;
  19.         default:
  20.           this.currentNavPathStack!.pushDestination({
  21.             name: request.routeName, param: request.params
  22.           }).then(() => {
  23.             request.resolve = resolve;
  24.             this.inject(request)
  25.           }).catch((e: ESObject) => {
  26.             console.log(`${e}`)
  27.             if (e.code == 100005) {
  28.               resolve(RouterResponseError.RequestNotFoundResponsor)
  29.             } else {
  30.               resolve(RouterResponseError.UnknownError)
  31.             }
  32.           })
  33.           break;
  34.       }
  35.     })
  36.   }
复制代码
RouterManagerForNavigation.open方法,重要是处理了replace和push两种不同的打开模式。
页面返回值

体系的返回监听

我们可以看到,在push页面时,我们调用了pushDestination方法,而它的入参实在是支持获取页面返回值的。
  1. declare class NavPathInfo {
  2.     constructor(name: string, param: unknown, onPop?: import('../api/@ohos.base').Callback<PopInfo>);
  3.     name: string;
  4.     param?: unknown;
  5.     /**
  6.      * The callback when next page returns.
  7.      *
  8.      * @type { ?import('../api/@ohos.base').Callback<PopInfo> }
  9.      * @syscap SystemCapability.ArkUI.ArkUI.Full
  10.      * @crossplatform
  11.      * @atomicservice
  12.      * @since 12
  13.      */
  14.     onPop?: import('../api/@ohos.base').Callback<PopInfo>;
  15. }
复制代码
onPop会在页面关闭时被处罚,而且支持返回值。
但是,我们并没有使用该参数,因为它在跨页面返回值存在逻辑问题。
当页面A打开页面B,页面B打开页面C,然后页面C直接返回页面A并转达返回值时,我们盼望的效果是页面A拿到页面C的返回值。
比如,课程详情页打开付出中间页,然后打开付款页面;付款成功或失败后返回课程详情页;课程详情页需要通过付款是否成功来判断页面是否刷新页面。
但是,onPop目前的逻辑是页面C直接返回页面A并带返回值时,页面B的onPop会被触发,页面A的onPop并不会被触发。
以是,虽然onPop用起来非常方便,但为了功能的完备性,我们还是放弃了使用该参数。
返回值实现逻辑

终极的实现逻辑和router类似,即通过监听页面生命周期来手动触发回调。
  1.   close(options?: RouterBackOptionsWrapper | undefined): boolean {
  2.     // ...
  3.     this.resultStrategy = RouterResultStrategy.onPagePop
  4.     // `NavPathStack.pop/popToName`方法`result`参数为undefined时无法触发其push方法的onPop回调;
  5.     if (options && options.routeName && options.routeName.length > 0) {
  6.       let routeInfo = this.getRequest(options.routeName)
  7.       if (routeInfo?.destinationInfo) {
  8.         this.resultStrategy = RouterResultStrategy.onPageShow
  9.         this.backToRouteName = options.routeName
  10.         this.backToIndex = routeInfo?.destinationInfo.index
  11.       }
  12.       let result = this.currentNavPathStack!.popToName(options.routeName, backParams, true)
  13.       if (result == -1) {
  14.         // 失败后清空,防止影响其他场景的返回值取值(比如侧滑返回,点系统返回按钮等)
  15.         this.backParams = undefined
  16.       }
  17.       return result != -1
  18.     } else {
  19.       let result = this.currentNavPathStack!.pop(backParams, true)
  20.       if (result == undefined) {
  21.         // 失败后清空,防止影响其他场景的返回值取值(比如侧滑返回,点系统返回按钮等)
  22.         this.backParams = undefined
  23.       }
  24.       return result != undefined
  25.     }
  26.   }
复制代码
首先看一下close方法,因为返回上一页和返回指定页面在返回值处理逻辑上存在很大差异,以是我们单独定义了resultStrategy返回值策略属性。
这是为了加强代码的可读性,否则无论是使用者还是开辟者都容易在各种条件判断中迷失。
  1. /**
  2. * 页面路由返回值处理策略
  3. */
  4. export enum RouterResultStrategy {
  5.   /**
  6.    * 在被打开的页面pop出栈时,触发打开该页面对应的回调方法。
  7.    */
  8.   onPagePop,
  9.   /**
  10.    * 返回指定页面routeName时,当routeName onShow时,触发最后获取到的回调方法(即routeName打开页面时传入的回调方法)。
  11.    */
  12.   onPageShow,
  13. }
复制代码
然后我们看一下最焦点的生命周期监听逻辑:
  1. observerPageLifecycle(uiAbility: UIAbility) {
  2.     observer.on("navDestinationUpdate", (navDestinationInfo: observer.NavDestinationInfo) => {
  3.       const name = navDestinationInfo.name.toString()
  4.       const id = navDestinationInfo.navDestinationId
  5.       // 通过监听页面生命周期方法,将系统堆栈和routes保持一致,用来处理返回值回调
  6.       switch (navDestinationInfo.state) {
  7.         case observer.NavDestinationState.ON_APPEAR:
  8.           if (!this.hasRequest(name, id)) {
  9.             let request = this.hasUndefinedRequest(name)
  10.             if (request) {
  11.               request.destinationInfo = navDestinationInfo
  12.             } else {
  13.               this.inject(new RouterRequestWrapper({ url: "other/" + name }), navDestinationInfo)
  14.             }
  15.           }
  16.           break
  17.         case observer.NavDestinationState.ON_SHOWN: {
  18.           if (this.resultStrategy == RouterResultStrategy.onPageShow && this.backToRouteName === name) {
  19.             this.lastResolve?.({
  20.               code: RouterResponseError.Success.code,
  21.               msg: RouterResponseError.Success.msg,
  22.               data: this.backParams
  23.             })
  24.             // 使用后清空,防止影响其他场景的返回值取值(比如侧滑返回,点系统返回按钮等)
  25.             this.backParams = undefined
  26.           }
  27.           break
  28.         }
  29.         case observer.NavDestinationState.ON_WILL_DISAPPEAR: {
  30.           if (this.resultStrategy == RouterResultStrategy.onPagePop) {
  31.             this.getRequest(name, id)?.request?.resolve?.({
  32.               code: RouterResponseError.Success.code,
  33.               msg: RouterResponseError.Success.msg,
  34.               data: this.backParams
  35.             })
  36.             // 使用后清空,防止影响其他场景的返回值取值(比如侧滑返回,点系统返回按钮等)
  37.             this.backParams = undefined
  38.           } else {
  39.             if (this.backToIndex != undefined && navDestinationInfo.index == this.backToIndex + 1) {
  40.               this.lastResolve = this.getRequest(name, id)?.request?.resolve
  41.             }
  42.           }
  43.           this.removeRequest(name, id)
  44.           break
  45.         }
  46.         case observer.NavDestinationState.ON_BACKPRESS: {
  47.           // api12.beta2 该状态不会被触发
  48.           this.getRequest(name, id)?.request?.resolve?.({
  49.             code: RouterResponseError.Success.code,
  50.             msg: RouterResponseError.Success.msg
  51.           })
  52.           this.removeRequest(name, id)
  53.           break
  54.         }
  55.       }
  56.     })
  57.   }
复制代码

  • 监听ON_APPEAR状态,将页面与open方法的request参数(inject方法)绑定;
  • 监听ON_SHOWN状态,处理RouterResultStrategy.onPageShow策略,当指定页面触发该状态,则找到该页面发起的哀求(这实在是在ON_WILL_DISAPPEAR状态中完成),并触发其revolve回调,回传参数;
  • 监听ON_WILL_DISAPPEAR状态,处理RouterResultStrategy.onPagePop策略,在本页面消散时,获取到打开本页面的哀求,并触发其resolve回调,回传参数;
  • 监听ON_BACK_PRESS状态,api12.beta2该状态不会被触发,实在是无效逻辑;因此,当页面侧滑返回或者点击导航栏返回按钮时,实际走的还是ON_WILL_DISAPPEAR状态的逻辑。
总结

我们可以看到,Navigation的页面封装实在router更为复杂,重要是其相比router页面,体系并没有给与原生的主动注册逻辑,从而导致了巨大的复杂性。
除此之外,动态导入也增加了很多复杂度。
如果官方可以自己解决掉主动注册和动态导入两个问题,我相信对于绝大多数人而言,路由框架就没有封装的必要了。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

风雨同行

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表