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


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.mzph.cn/news/887919.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【博主推荐】C#的winfrom应用中datagridview常见问题及解决方案汇总

文章目录 1.datagridview绘制出现鼠标悬浮数据变空白2.datagridview在每列前动态添加序号2.1 加载数据集完成后绘制序号2.2 RowPostPaint事件绘制 3.datagridview改变行样式4.datagridview后台修改指定列数据5.datagridview固定某个列宽6.datagridview某个列的显示隐藏7.datagr…

宠物领养平台构建:SpringBoot技术路线图

摘 要 如今社会上各行各业&#xff0c;都在用属于自己专用的软件来进行工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。互联网的发展&#xff0c;离不开一些新的技术&#xff0c;而新技术的产生往往是为了解决现有问题而产生的。针对于宠物领养…

使用经典的Java,还是拥抱新兴的Rust?

在当代互联网时代的企业级开发中&#xff0c;技术栈的选择往往牵动着每个团队的神经。随着Rust语言的崛起&#xff0c;许多开发团队开始重新思考&#xff1a;是继续坚持使用经典的Java&#xff0c;还是拥抱新兴的Rust&#xff1f;这个问题背后&#xff0c;折射出的是对技术演进…

springboot学习-jdbc,jdbc-client,spring-data-jdbc

晚上又看了Dan Vega的视频&#xff0c;讲解了jdbc在spring 的发展史。 jdbc: sql语句&#xff0c;手工梳理result&#xff0c;并循环。最原始的JAVA API&#xff0c;从1998年JAVA1.0就有了。jdbc template: sql语句&#xff0c;手工处理result ,不用循环了。--从spring诞生就有…

卸载snap docker一直卡住:Save data of snap “docker“ in automatic snapshot set #3

在卸载 Snap 安装的 Docker 时卡住&#xff0c;通常是因为 Snap 在执行卸载时会先尝试保存一些快照&#xff08;自动或手动创建的&#xff09;&#xff0c;并且该过程可能因某些原因而卡住。为了解决这个问题&#xff0c;你可以按照以下步骤强制删除 Snap 安装的 Docker&#x…

Java项目运行报错“java: -source 1.5 中不支持 diamond 运算符“解决办法windows/linux系统踩坑实录

文章目录 一、问题描述二、解决方案 一、问题描述 在接手同事的Java项目时&#xff0c;依赖和打包都能正常操作&#xff0c;但一点击运行项目&#xff0c;就会报错&#xff1a; java: -source 1.5 中不支持 diamond 运算符 (请使用 -source 7 或更高版本以启用 diamond 运算符…

SQL基础入门 —— SQL概述

目录 1. 什么是SQL及其应用场景 SQL的应用场景 2. SQL数据库与NoSQL数据库的区别 2.1 数据模型 2.2 查询语言 2.3 扩展性 2.4 一致性与事务 2.5 使用场景 2.6 性能与扩展性 总结 3. 常见的SQL数据库管理系统&#xff08;MySQL, PostgreSQL, SQLite等&#xff09; 3.…

开源项目:纯Python构建的中后台管理系统

来源&#xff1a;Python大数据分析 费弗里 大家好我是费老师&#xff0c;目前市面上有很多开源的「中后台管理系统」解决方案&#xff0c;复杂如「若依」那种前端基于Vue&#xff0c;后端基于Java的框架&#xff0c;虽然其提供了较为完善的一整套前后端分离权限管理系统解决方…

视频video鼠标移入移除展示隐藏(自定义控件)

效果图 代码 <template><div class"video-container" mouseover"showControls" mouseleave"hideControlsAfterDelay"><videoref"video"loadedmetadata"initializePlayer"timeupdate"updateProgress&qu…

【连接池】.NET开源 ORM 框架 SqlSugar 系列

.NET开源 ORM 框架 SqlSugar 系列 【开篇】.NET开源 ORM 框架 SqlSugar 系列【入门必看】.NET开源 ORM 框架 SqlSugar 系列【实体配置】.NET开源 ORM 框架 SqlSugar 系列【Db First】.NET开源 ORM 框架 SqlSugar 系列【Code First】.NET开源 ORM 框架 SqlSugar 系列【数据事务…

ubuntu24.04安装Kubernetes1.31.0(k8s1.30.0)高可用集群

ubuntu24.04安装Kubernetes1.30.0(kubernetes1.30.0)高可用集群 一、总体概览 目前最新版的K8S版本应该是1.31.0,我们安装的是第二新的版本1.30.0,因为有大神XiaoHH Superme指路,所以基本上没踩坑,很顺利就搭建完成了。所有的机器都采用的最新版Ubuntu-Server-24.04长期支…

Ubuntu中的apt update 和 apt upgrade

apt update 和 apt upgrade 是 Debian 及其衍生发行版&#xff08;如 Ubuntu&#xff09;中常用的两个 APT 包管理命令&#xff0c;它们各自执行不同的任务&#xff1a; apt update: 这个命令用于更新本地软件包列表。当你运行 apt update 时&#xff0c;APT 会从配置的源&…

鸿蒙HarmonyOS vs Android系统对比

鸿蒙系统 (HarmonyOS) vs Android 系统对比 鸿蒙操作系统&#xff08;HarmonyOS&#xff09;是华为推出的多终端操作系统&#xff0c;旨在构建一个 跨设备、跨平台、智能化 的生态系统。与 Android 系统相比&#xff0c;鸿蒙有其独特的设计理念和技术架构。以下是它们在多个关…

Hbase 部署

HBase是一个分布式的、面向列的开源数据库&#xff0c;它是Apache Hadoop项目的子项目。为了成功部署HBase&#xff0c;可以按照以下步骤进行&#xff1a; 主机部署 一、准备环节 设备基本要求&#xff1a; Hadoop和ZooKeeper&#xff1a;HBase集群需要依赖Hadoop和Zookeepe…

微软要求 Windows Insider 用户试用备受争议的召回功能

拥有搭载 Qualcomm Snapdragon 处理器的 Copilot PC 的 Windows Insider 计划参与者现在可以试用 Recall&#xff0c;这是一项臭名昭著的快照拍摄 AI 功能&#xff0c;在今年早些时候推出时受到了很多批评。 Windows 营销高级总监 Melissa Grant 上周表示&#xff1a;“我们听…

脉冲动画效果

js实现脉冲动画效果&#xff1a; 鼠标点击时&#xff0c;添加动画类&#xff0c;进而实现动画效果&#xff0c;鼠标离开时&#xff0c;移除动画类&#xff0c;回归静态图效果。 <!DOCTYPE html> <html lang"en"> <head><meta charset"UT…

Linux—进程学习—04(进程地址空间学习)

目录 Linux—进程学习—41.程序地址空间1.1虚拟地址空间的现象1.2虚拟地址空间的理解(感性) 2.进程地址空间2.0 mm_struct结构体2.1 mm_struct结构体的源代码2.2分页&虚拟地址空间解释前面的实验现象 2.3进程地址空间存在的原因2.3.1第一个原因2.3.2第二个原因2.3.3第三个原…

Java集成Sa-Token进行认证与授权

引言 软件开发过程中都必须要有的一个功能&#xff0c;那就是认证与授权&#xff0c;经过大佬们的不断更新迭代&#xff0c;使得如今实现认证与授权功能变得相对简单&#xff0c;也许你并不能真正的接触到认证与授权这一功能&#xff0c;除非你接触的项目是从0到1的&#xff0c…

uni-app获取到的数据如何保留两位小数

<view><text class"daily_r">{{ (chartD.selfPowerCount || 0).toFixed(2) }}</text>度</view> 1&#xff0c;在模板中&#xff0c;所有需要保留两位小数的数值都使用了 toFixed(2) 方法&#xff0c;例如 {{ (chartD.selfPowerCount || 0).…

图论入门编程

卡码网刷题链接&#xff1a;98. 所有可达路径 一、题目简述 二、编程demo 方法①邻接矩阵 from collections import defaultdict #简历邻接矩阵 def build_graph(): n, m map(int,input().split()) graph [[0 for _ in range(n1)] for _ in range(n1)]for _ in range(m): …