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

devtools/2024/11/30 8:27:21/

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
@NavigationRoute({ routeName: "testPage", hasParams: true })
@Component
export struct TestDestination {@Prop params?: Record<string, ESObject>build() {Column() {NavDestination() {TestPageContent({ pageName: 'TestDestination', params: this.params })}}}
}

Navigation页面注册使用了自定义的类装饰器@NavigationRoute。我们来看一下其实现:

export function NavigationRoute(options: RouteRegisterOptions) {return (target: ESObject) => {}
}

我们发现,该装饰器的实现代码是个空方法,空方法的话如何实现页面注册呢?

这是因为在ArkTS中,struct无法使用自定义的装饰器,虽然IDE编译不会报错,但是这个装饰器代码根本不会执行。

那么,Navigation页面到底是怎么完成注册的?

答案是:FWRouterHvigorPlugin。

在这个hvigor插件中,插件代码扫描模块中的.eta文件,解析ts语法,当发现装饰器@NavigationRoute时,就会将它所装饰的类名提取出来,然后生成对应的builder和自动注册代码。具体如下:

@Builder
function testDestinationBuilder(params: ESObject) {TestDestination({ params: params });
}@RouterClassProvider({ routeName: 'testPage', builder: wrapBuilder(testDestinationBuilder) })
export class TestDestinationProvider {
}

我们看到插件生成了两部分代码,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的实现代码:

export function RouterClassProvider(options: RouterClassProviderOptions) {return (target: ESObject) => {RouterManagerForNavigation.getInstance().registerBuilder(options.routeName, options.builder)}
}

我们看到这个装饰器真正调用了RouterManagerForNavigation中的注册方法,将路由名和页面builder的匹配关系注册到了管理器中。

那么,为什么要这样实现呢?

我们想要的其实就只有一个@RouterClassProvider装饰器,但装饰器不能单独使用,必须装饰在一个类上,所以我们定义了TestDestinationProvider类。
TestDestination不能直接拿来注册,必须包装进@builder,所以我们定义了testDestinationBuilder

除此之外,@RouterClassProvider装饰器的触发时机是其所在的文件被import的时候。

因此,在har包中,我们需要将生成的代码文件自动添加到模块的index.ets中去。

export * from './src/main/ets/generated/RouterBuilder';

在entry中,我们需要在EntryAbility.ets中导入。

import('../generated/RouterBuilder');

以上是hvigor插件为了完成Navigation页面所做的事情,至于方案为什么是这样,建议详细查看具体原因可以查看鸿蒙应用开发从入门到入魔:Navigation路由管理为什么这么麻烦?

打开页面

对于@fw/router而言,打开router页面和Navigation页面都是一样,因此使用完全相同的api,所以前面的openWithRequest_realOpenopen等方法逻辑完全一致,此处不再赘述。

RouterManagerForNavigation.open
  open(request: RouterRequestWrapper): Promise<RouterResponse> {return new Promise((resolve, reject) => {if (!this.currentNavPathStack) {resolve(RouterResponseError.RequestNotFoundResponsor)return}if (!this.canOpen(request.routeName)) {resolve(RouterResponseError.RequestNotFoundResponsor)return}switch (request.rawRequest.openMode) {case PageRouteOpenMode.replace:this.currentNavPathStack!.replacePath({name: request.routeName, param: request.params})request.resolve = resolve;this.inject(request)break;default:this.currentNavPathStack!.pushDestination({name: request.routeName, param: request.params}).then(() => {request.resolve = resolve;this.inject(request)}).catch((e: ESObject) => {console.log(`${e}`)if (e.code == 100005) {resolve(RouterResponseError.RequestNotFoundResponsor)} else {resolve(RouterResponseError.UnknownError)}})break;}})}

RouterManagerForNavigation.open方法,主要是处理了replace和push两种不同的打开模式。

页面返回值
系统的返回监听

我们可以看到,在push页面时,我们调用了pushDestination方法,而它的入参其实是支持获取页面返回值的。

declare class NavPathInfo {constructor(name: string, param: unknown, onPop?: import('../api/@ohos.base').Callback<PopInfo>);name: string;param?: unknown;/*** The callback when next page returns.** @type { ?import('../api/@ohos.base').Callback<PopInfo> }* @syscap SystemCapability.ArkUI.ArkUI.Full* @crossplatform* @atomicservice* @since 12*/onPop?: import('../api/@ohos.base').Callback<PopInfo>;
}

onPop会在页面关闭时被处罚,而且支持返回值。
但是,我们并没有使用该参数,因为它在跨页面返回值存在逻辑问题。

当页面A打开页面B,页面B打开页面C,然后页面C直接返回页面A并传递返回值时,我们期望的效果是页面A拿到页面C的返回值。

比如,课程详情页打开支付中间页,然后打开付款页面;付款成功或失败后返回课程详情页;课程详情页需要通过付款是否成功来判断页面是否刷新页面。

但是,onPop目前的逻辑是页面C直接返回页面A并带返回值时,页面B的onPop会被触发,页面A的onPop并不会被触发。

所以,虽然onPop用起来非常方便,但为了功能的完整性,我们还是放弃了使用该参数。

返回值实现逻辑

最终的实现逻辑和router类似,即通过监听页面生命周期来手动触发回调。

  close(options?: RouterBackOptionsWrapper | undefined): boolean {// ...this.resultStrategy = RouterResultStrategy.onPagePop// `NavPathStack.pop/popToName`方法`result`参数为undefined时无法触发其push方法的onPop回调;if (options && options.routeName && options.routeName.length > 0) {let routeInfo = this.getRequest(options.routeName)if (routeInfo?.destinationInfo) {this.resultStrategy = RouterResultStrategy.onPageShowthis.backToRouteName = options.routeNamethis.backToIndex = routeInfo?.destinationInfo.index}let result = this.currentNavPathStack!.popToName(options.routeName, backParams, true)if (result == -1) {// 失败后清空,防止影响其他场景的返回值取值(比如侧滑返回,点系统返回按钮等)this.backParams = undefined}return result != -1} else {let result = this.currentNavPathStack!.pop(backParams, true)if (result == undefined) {// 失败后清空,防止影响其他场景的返回值取值(比如侧滑返回,点系统返回按钮等)this.backParams = undefined}return result != undefined}}

首先看一下close方法,因为返回上一页和返回指定页面在返回值处理逻辑上存在很大差异,所以我们单独定义了resultStrategy返回值策略属性。
这是为了加强代码的可读性,否则无论是使用者还是开发者都容易在各种条件判断中迷失。

/*** 页面路由返回值处理策略*/
export enum RouterResultStrategy {/*** 在被打开的页面pop出栈时,触发打开该页面对应的回调方法。*/onPagePop,/*** 返回指定页面routeName时,当routeName onShow时,触发最后获取到的回调方法(即routeName打开页面时传入的回调方法)。*/onPageShow,
}

然后我们看一下最核心的生命周期监听逻辑:

observerPageLifecycle(uiAbility: UIAbility) {observer.on("navDestinationUpdate", (navDestinationInfo: observer.NavDestinationInfo) => {const name = navDestinationInfo.name.toString()const id = navDestinationInfo.navDestinationId// 通过监听页面生命周期方法,将系统堆栈和routes保持一致,用来处理返回值回调switch (navDestinationInfo.state) {case observer.NavDestinationState.ON_APPEAR:if (!this.hasRequest(name, id)) {let request = this.hasUndefinedRequest(name)if (request) {request.destinationInfo = navDestinationInfo} else {this.inject(new RouterRequestWrapper({ url: "other/" + name }), navDestinationInfo)}}breakcase observer.NavDestinationState.ON_SHOWN: {if (this.resultStrategy == RouterResultStrategy.onPageShow && this.backToRouteName === name) {this.lastResolve?.({code: RouterResponseError.Success.code,msg: RouterResponseError.Success.msg,data: this.backParams})// 使用后清空,防止影响其他场景的返回值取值(比如侧滑返回,点系统返回按钮等)this.backParams = undefined}break}case observer.NavDestinationState.ON_WILL_DISAPPEAR: {if (this.resultStrategy == RouterResultStrategy.onPagePop) {this.getRequest(name, id)?.request?.resolve?.({code: RouterResponseError.Success.code,msg: RouterResponseError.Success.msg,data: this.backParams})// 使用后清空,防止影响其他场景的返回值取值(比如侧滑返回,点系统返回按钮等)this.backParams = undefined} else {if (this.backToIndex != undefined && navDestinationInfo.index == this.backToIndex + 1) {this.lastResolve = this.getRequest(name, id)?.request?.resolve}}this.removeRequest(name, id)break}case observer.NavDestinationState.ON_BACKPRESS: {// api12.beta2 该状态不会被触发this.getRequest(name, id)?.request?.resolve?.({code: RouterResponseError.Success.code,msg: RouterResponseError.Success.msg})this.removeRequest(name, id)break}}})}
  1. 监听ON_APPEAR状态,将页面与open方法的request参数(inject方法)绑定;
  2. 监听ON_SHOWN状态,处理RouterResultStrategy.onPageShow策略,当指定页面触发该状态,则找到该页面发起的请求(这其实是在ON_WILL_DISAPPEAR状态中完成),并触发其revolve回调,回传参数;
  3. 监听ON_WILL_DISAPPEAR状态,处理RouterResultStrategy.onPagePop策略,在本页面消失时,获取到打开本页面的请求,并触发其resolve回调,回传参数;
  4. 监听ON_BACK_PRESS状态,api12.beta2该状态不会被触发,其实是无效逻辑;因此,当页面侧滑返回或者点击导航栏返回按钮时,实际走的还是ON_WILL_DISAPPEAR状态的逻辑。
总结

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


http://www.ppmy.cn/devtools/138139.html

相关文章

【第十课】Rust并发编程(一)

目录 前言 Fork和Join 前言 本节会介绍Rust中的并发编程&#xff0c;并发编程在编程中是提升cpu使用率的一大利器&#xff0c;通过多线程技术提升效率&#xff0c;Rust的并发和其他编程语言的并发不同的地方在于&#xff0c;Rust号称无畏并发。更重要的一点是安全。Rust中所有…

# issue 6 网络编程基础

一、网络的物理结构和光纤千兆网络 首先&#xff0c;我们需要知道网络的物理结构——数据是如何从一台机器传输到另外一台机器的 这个过程是非常重要的。现在很多人做软件开发&#xff0c;只会软件角度&#xff0c;这导致讲软件原理头头是道&#xff0c;但是连数据线都不会接&a…

基于OpenCV视觉库让机械手根据视觉判断物体有无和分类抓取的例程

项目实例&#xff0c;在一个无人封闭的隔绝场景中&#xff0c;根据视觉判断物件的有无&#xff0c;通过机械手 进行物件分类提取&#xff0c;并且返回状态结果&#xff1b; 实际的场景是有一个类似采血的固件支架盘&#xff0c;上面很多采血管&#xff0c;采血管帽颜色可能不同…

深入探索Flax:一个用于构建神经网络的灵活和高效库

深入探索Flax&#xff1a;一个用于构建神经网络的灵活和高效库 在深度学习领域&#xff0c;TensorFlow 和 PyTorch 作为主流的框架&#xff0c;已被广泛使用。不过&#xff0c;Flax 作为一个较新的库&#xff0c;近年来得到了越来越多的关注。Flax 是一个由Google Research团队…

租赁小程序|租赁系统搭建|租赁系统需求

随着信息技术的高速发展&#xff0c;租赁行业逐渐向智能化、便捷化方向迈进。一款优秀的租赁小程序&#xff0c;旨在为用户提供一站式的租赁服务体验&#xff0c;同时帮助租赁企业优化管理流程&#xff0c;提高业务效率。 一、用户需求精准把握 在开发任何软件产品时&#xff0…

2024年11月28日Github流行趋势

项目名称&#xff1a;OpenInterpreter 项目维护者&#xff1a;KillianLucas, Notnaton, MikeBirdTech, CyanideByte, ericrallen项目介绍&#xff1a;一个自然语言计算机接口&#xff0c;允许用户通过自然语言与计算机交互。项目star数&#xff1a;56,695项目fork数&#xff1a…

TypeScript 命名空间与模块

在 TypeScript 中&#xff0c;命名空间和模块是两种不同的代码组织方式&#xff0c;它们都旨在帮助你管理和维护大型代码库。命名空间提供了一种将相关功能组织在一起的方式&#xff0c;而模块则允许你将代码分解成可重用的单元。在本文中&#xff0c;我们将探讨命名空间和模块…

2024年11月29日Github流行趋势

项目名称&#xff1a;aisuite 项目维护者&#xff1a;ksolo, standsleeping, rohitprasad15, jeffxtang, andrewyng项目介绍&#xff1a;为多个生成式AI供应商提供简单、统一的接口。项目star数&#xff1a;4,302项目fork数&#xff1a;368 项目名称&#xff1a;screenshot-to…