从零开始开发纯血鸿蒙应用之跨模块路由
跨模块路由
- 〇、前言
- 一、设计思路
- 1、支撑模块化开发
- 2、支持路由传参
- 3、模块解耦
- 二、API 选型
- 1、import 动态加载
- 2、NavPathStack 路由栈
- 3、wrapBuilder 全局封装
- 三、实现路由模块
- 1、定义 RouterInfo
- 2、定义路由注册器
- 3、定义 EasyRouter
- 3.1、RegisterRoutePage
- 3.2、back
- 3.3、pushUrl
- 四、体验跨模块路由
- 1、创建 feature_test 模块
- 2、配置路由
- 3、注册路由
- 4、设置 Navigation 组件
- 5、定义 RouteParam
- 6、使用路由
- 6.1、跳转页面并传入数据
- 6.2、读取数据并展示
- 6.3、页面返回
- 五、总结
在开始本章内容之前,先说一些题外话,就是之前利用 ohpm 公开发布到中心仓库的 lib_log 已经通过了审核,现在已经可以直接利用 ohpm 进行安装。
〇、前言
如果你也是紧跟 API 版本去升级 DevEco Studio 的人,那么你就会发现在 2025 年 7 月 4 日发布的 DevEco Studio 5.1.0 Release 中,又有很多 API 用法被 deprecated,比如 promptAction.showToast,也比如 router.pushUrl。
对于 router 模块,如果你打开最新的 API 文档,就会发现它一整个都被 deprecated 掉了:
当然了,对于直接用 Navigation 组件重构应用成本很高的项目来说,还可以折中使用下面的方式:
也就是下面这样:
但这总归是缓兵之计,并非长治久安之策,稳妥的方式还是使用 Navigation 组件实现自定义路由功能,这也是本文所要向大家介绍的。
一、设计思路
直接使用 router 模块,或者是从 UIContext 中获取 Router 实例对象,有个很明显的弊端,就是只能路由那些使用了 @Entry
装饰的 Page Component,使用限制比较大,现在,既然官方开始废弃 router 模块了,那么,也该是时候采用 Navigation 组件设计自定义路由了。
1、支撑模块化开发
既然要搞自定义路由,那么就必须实现成充分支持模块化开发的,也就是所有页面可以根据复杂程度和从属关系,或独自成模块或搭配成模块,也就是新的路由功能要支持跨模块的页面跳转。
2、支持路由传参
路由页面的同时,传入目标页面需要的数据,是应用开发过程中的典型场景,一个称得上好用的路由模块,就必须支持路由传参。
3、模块解耦
考虑到新开发的路由模块,会像 lib_log 一样用 ohpm 公开发布,那么该路由模块里面就不能有本地依赖,只能使用同样存在于 ohpm 中心仓的依赖,否则就无法迁移到其他项目中使用。
二、API 选型
要实现上述设计思想,那么,就需要在鸿蒙SDK 中选择合适的 API 或者 API 特性,根据我的实践,最终选择了如下内容:
- import 的动态加载特性
- NavPathStack 路由栈
- wrapBuilder 全局封装
1、import 动态加载
在 ArkTS 脚本开头处所编写的 import 语句,实际上,属于 import 的静态加载,而这也是 import 函数的常规用法,而 import 函数的高级用法,便是动态加载。
import 动态加载的实现代码,类似如下:
import(moduleName).then((result: ESObject) => {// 成功加载模块后的处理代码}, (error: ESObject) => {// 加载失败的处理代码}) ;
没错,import 动态加载的实质,就是异步回调。动态import能够支持的形式有如下:
特别注意的:
DevEco Studio中模块间的依赖关系通过oh-package.json5中的dependencies进行配置。dependencies列表中所有模块默认都会进行安装(本地模块)或下载(远程模块),但是不会默认参与编译。HAP/HSP编译时会以入口文件(一般为Index.ets/ts)开始搜索依赖关系,搜索到的模块或文件才会加入编译。
在编译期,静态import和常量动态import可以被打包工具rollup及其插件识别解析,加入依赖树中,参与编译流程,最终生成方舟字节码。但是,如果是变量动态import,该变量值可能需要进行运算或外部传入才能得到,在编译态无法解析其内容,也就无法加入编译。为了将这部分模块/文件加入编译,还需要额外增加一个runtimeOnly的buildOption配置,用于配置动态import的变量实际的模块名或文件路径。
2、NavPathStack 路由栈
NavPathStack 并不是一个独立的 API 模块,而是 Navigation 组件的一部分:
它是 Navigation 组件实现页面路由的核心,也是我们实现自定义路由管理功能的基础。
路由栈也是栈,是栈就能进栈和出栈;NavPathStack 的进栈方法,这里主要选用:
而利用 pushPath 方法的第一个参数,即 NavPathInfo 我们可以轻松实现路由传参功能:
而对应的页面返回,实际上就是出栈操作,也就是 pop 方法,此外还可以利用清栈 clear 方法,去实现直接回到应用起始页。
3、wrapBuilder 全局封装
wrapBuilder 接口,实际上是为了配合动态import,去实现跨模块加载页面的实现代码:
wrapBuilder 接口的使用必须遵循如下限制:
1、wrapBuilder方法只能传入全局@Builder方法。
2、wrapBuilder方法返回的WrappedBuilder对象的builder属性方法只能在struct内部使用。
三、实现路由模块
1、定义 RouterInfo
由于页面分散在不同模块中,页面名称也各不相同,页面 url 更是各自独一份,所有的这些构成路由信息,必须用适当的对象进行跟踪,因此,我使用如下代码定义了一个 RouterInfo:
2、定义路由注册器
基于 RouterInfo,定义一个 RouterRegister,从而方便进行动态的路由注册,具体实现代码如下:
在这个 RouterRegister 里面,用一个私有的 RouterInfo 数组去记录每一个所注册的 RouterInfo,同时基于该 RouterInfo 数组提供一些查找方法,比如根据 url 查找完整的 RouterInfo。
3、定义 EasyRouter
这一步是整个路由模块的核心,因为 EasyRouter 承担着实际的路由管理,它的实现代码基本如下:
考虑文章篇幅,这里只重点讲解 RegisterRoutePage、pushUrl 和 back 方法
3.1、RegisterRoutePage
这个方法,实际上就在上面讲解 wrapBuilder 的时候出镜过,它的具体实现如下:
通过该方法,可以传入一个包含具体页面实现代码的全局 builder,而该全局 builder 又会通过 registerBuilder 方法,添加到 EasyRouter 的 builderMap 中,从而令 Navigation 组件获得目标页面的“房间平面图”和“装修方案”。
3.2、back
由于 pushUrl 方法的代码更为复杂,所以,遵循由简入繁的叙述顺序,先讲解 back 方法的实现:
进行页面返回的时候,只需对路由栈进行出栈操作,而记录着页面具体构建方式的 builderMap 是不用进行清理的,因为被返回的页面还会第二次打开,而一旦对 builderMap 进行清理,那么下一次打开相同页面,Navigation 组件就不知道要怎么渲染该页面了。
退栈的时候,需要判断一下是否还能进行退栈操作,避免过度退栈引发程序奔溃。
3.3、pushUrl
pushUrl 方法,核心就是使用动态加载也即动态import,将目标页面从目标模块从加载出来:
当目标模块被成功加载进来后,将相应的路由信息添加到路由栈,也即 Navigation 的 NavPathStack 中,最后记录当前引用的页面,即更新 EasyRouter.referrer 数组。
详细的 EasyRouter 代码,可以访问EasyRouter 进行浏览。
四、体验跨模块路由
接下来,就可以使用上面的路由模块,进行跨模块路由的体验了。
1、创建 feature_test 模块
这个模块提供一个目标页:
并且,配合动态import,在 feature_test 模块入口文件Index.ets 文件中,定义一个 harInit 方法:
export function harInit(name: string) {switch(name) {case RouterRegister.getPageNameByUrl("/feature_test/main_page"):import("./src/main/ets/components/MainPage");Logger.info(`${RouterRegister.getPageNameByUrl("/feature_test/main_page")}被动态加载`, '[feature_test:harInit]')break;default:break;}
}
由于 Navigation 组件放在 entry 模块中,所以,feature_test 模块需要在 entry 模块中进行 dependences:
并在build-profile.json5 文件中,相应设置 runtimeOnly 选项:
2、配置路由
其次,需要对路由进行配置,也就是注册路由信息。
对于路由信息的注册,可以考虑创建一个专门放置只读常量的模块,并在下面的代码放在合适文件中:
import { RouterInfo } from "lib_easyrouter";export class RouterInfoConstant {static readonly FeatureTestMainPage: RouterInfo =new RouterInfo("/feature_test/main_page", "FeatureTestMainPage", "feature_test");
}
我这里,只是为了演示跨模块路由,所以,上述代码是放在 entry 模块下。
3、注册路由
接下来,利用 RouterInfoConstant 将具体的路由信息,注册到 RouterRegister 中,我这里是使用一个 EntryUtil 来实现的:
EntryUtil.initRouterInfoRegister 方法,会在 EntryAbility 的 onCreate 方法中进行调用:
4、设置 Navigation 组件
接下来,需要结合 EasyRouter 去设置 Navigation 组件,而 Navigation 组件,我是直接放在 entry 模块自动创建的 Index 页面中:
5、定义 RouteParam
为了方便体验通过路由进行数据传递,需要定义一个 RouteParam 对象,这是因为我目前只对对象类型的数据做了路由传参支持:
6、使用路由
6.1、跳转页面并传入数据
在 Navigation 组件中,简单的使用 Column 组件添加一个按钮:
6.2、读取数据并展示
在 feature_test 模块的目标页面,通过 aboutToAppear 函数,将数据从路由栈中取出:
6.3、页面返回
页面返回比较简单,就是用一个按钮去触发 EasyRouter.back()
。
五、总结
经过本篇的学习,我相信屏幕前的你,不仅学会了 import 的另一种使用方式,还学会使用动态 import 结合 Navigation 的 NavPathStack 实现跨模块的页面路由。