📝往期推文全新看点(文中附带最新·鸿蒙全栈学习笔记)
🚩 鸿蒙应用开发与鸿蒙系统开发哪个更有前景?
🚩 嵌入式开发适不适合做鸿蒙南向开发?看完这篇你就了解了~
🚩 对于大前端开发来说,转鸿蒙开发究竟是福还是祸?
🚩 鸿蒙岗位需求突增!移动端、PC端、IoT到底该怎么选?
🚩 记录一场鸿蒙开发岗位面试经历~
📃 持续更新中……
简介
大型应用开发中,应用可能包含不同的业务模块,每个模块由不同的业务团队负责开发。该场景采用一个 Navigation 下多个 har/hsp 的架构,其中一个模块对应一个har/hsp。当多个har/hsp的UI组件存在相互跳转的业务需求时,将出现模块间相互依赖的问题。如“A.har”、“B.har”和“C.har”模块拥有不同的组件,各组件间的路由跳转形成了一个环形链路,导致三个har模块相互耦合,如图所示:
图1 多har包间路由跳转耦合
针对该场景,本文提供了一套基于Navigation的路由设计方案实现多模块路由管理和模块间解耦。并在该基础上,通过动态注册路由的方式,解决页面加载多个UI组件时启动速度变慢问题。
场景描述
假定工程包含harA和harB两个业务模块,harA模块打包编译为A.har,harB模块打包编译为B.har。A.har归属团队A独立开发、编译交付。B.har归属团队B独立开发、编译交付。harA模块中含有页面级组件A1,harB模块中有页面级组件B1、B2、B3。在实际业务中,harA模块中的A1组件需要跳转到harB模块中的B1组件,项目关系如下图所示。例如购物场景,商品选购和结算支付是两个独立的模块,用户在选购完成后,需要进入结算页面进行地址填写和付款等操作。
**图2 **两个har模块间路由跳转
基本解决方案和问题
基本方案实现步骤如下:
- 在harA模块中的A1页面组件中开发Navigation组件,并关联与之对应的NavPathStack路由栈,示例代码如下:
@Component
struct A1 {
// 创建NavPathStack路由栈对象
@State harARouter: NavPathStack = new NavPathStack();
build() {
// Navigation组件关联NavPathStack对象
Navigation(this.harARouter) {
// ...
}
}
}
- 在harA模块的oh-package.json5文件中添加harB模块的依赖,并且把harB模块中需要跳转的B1组件添加到harA模块的Navigation组件路由表中,示例代码如下
"dependencies": {
// 添加对harB的依赖
"@ohos/harb": "file:../harB"
}
在harA模块的A1组件中的routerMap路由表中,添加harB模块的B1组件,示例代码如下:
import { B1 } from '@ohos/harb';
struct A1 {
@State harARouter: NavPathStack = new NavPathStack();
@Builder
routerMap(builderName: string, param: object) {
if (builderName === 'B1') {
B1() // 在routerMap中添加需要跳转的harB模块的B1页面
}
}
build() {
Navigation(this.harARouter) {
// ...
}
.navDestination(this.routerMap) // Navigation关联上routerMap路由表
}
}
- 在harA模块的Navigation组件中添加跳转到harB模块的B1页面的逻辑,完整示例代码如下:
import { B1 } from '@ohos/harb';
struct A1 {
// 创建NavPathStack路由栈
@State harARouter: NavPathStack = new NavPathStack();
@Builder
routerMap(builderName: string, param: object) {
if (builderName === 'B1') {
B1() // 在routerMap中添加需要跳转的harB模块的B1页面
}
}
build() {
// Navigation关联NavPathStack对象
Navigation(this.harARouter) {
Button('跳转到HarB的B1页面')
.onClick(() => {
// 跳转到已在路由表注册的harB模块的B1页面
this.harARouter.pushPathByName('B1', null);
})
}
.navDestination(this.routerMap) // Navigation关联上routerMap路由表
}
}
当前基本方案主要存在以下问题:
- 使用Navigation时,所有路由页面需要主动通过import方式逐个导入当前页面,并存入页面路由表routerMap中。
- 主动使用import的方式需显性指定加载路径,造成开发态模块耦合严重。
- 模块无法独立编译,且存在开发态模块间循环依赖问题。
为解决上述问题,推荐如下方案。
推荐方案
将路由功能抽取成单独的模块并以har包形式存在,命名为RouterModule。RouterModule内部对路由进行管理,对外暴露RouterModule对象供其他模块使用。由于Entry.hap是应用必备的主入口,利用该特性考虑将主入口模块作为其他业务模块的依赖注册中心,在入口模块中使用Navigation组件并依赖其他业务模块。业务模块仅依赖RouterModule,业务模块中的路由统一委托到RouterModule中管理,实现业务模块间的解耦。按照推荐方案,上述场景各模块依赖关系如下:
图3 推荐方案各模块关系
此方案中,各模块的依赖关系如下:
- Entry.hap、A.har和B.har均依赖了RouterModule.har;
- Entry.hap在工程配置中依赖了A.har和B.har;
- 对于业务开发团队之间,A.har在工程和源码上无需依赖B.har的库,实现了业务模块间的解耦。
路由管理模块实现
RouterModule模块包含全局的路由栈和路由表信息。路由栈是NavPathStack对象,该对象与Entry.hap的Navigation组件绑定,RouterModule通过持有NavPathStack管理Navigation组件的路由信息。路由表builderMap是Map结构,以key-vaule的形式存储了需要路由的页面组件信息,其中key是自定义的唯一路由名,value是 WrappedBuilder 对象,该对象包裹了路由名对应的页面组件。RouterModule模块结构如下:
**图4 **RouterModule模块结构
RouterModule模块的实现主要包含以下步骤:
1. 定义路由表和路由栈。
export class RouterModule {
// WrappedBuilder支持@Builder描述的组件以参数的形式进行封装存储
static builderMap: Map<string, WrappedBuilder<[object]>> = new Map<string, WrappedBuilder<[object]>>();
// 初始化路由栈,需要关联Navigation组件
static routerMap: Map<string, NavPathStack> = new Map<string, NavPathStack>();
// ...
}
2. 路由表增加路由注册和路由获取方法,业务har模块通过路由注册方法将需要路由的页面组件委托给RouterModule管理。
// 通过名称注册路由栈
public static registerBuilder(builderName: string, builder: WrappedBuilder<[object]>): void {
RouterModule.builderMap.set(builderName, builder);
}
// 获取路由表中指定的页面组件
public static getBuilder(builderName: string): WrappedBuilder<[object]> {
const builder = RouterModule.builderMap.get(builderName);
if (!builder) {
Logger.info('not found builder ' + builderName);
}
return builder as WrappedBuilder<[object]>;
}
3.路由表增加路由跳转方法,业务har模块通过调用该方法并指定跳转信息实现模块间路由跳转。
public static async push(router: RouterModel): Promise<void> {
const harName = router.builderName.split('_')[0];
await import(harName).then((ns: ESObject): Promise<void> => ns.harInit(router.builderName));
RouterModule.getRouter(router.routerName).pushPath({ name: router.builderName, param: router.param });
}
页面跳转实现
路由管理模块RouterModule实现之后,需要使用RouterModule模块实现业务模块harA的页面跳转到业务模块harB的页面功能。主要步骤如下:
- 在工程主入口模块Entry.hap中引入RouterModule模块和所有需要进行路由注册的业务har模块。
"dependencies": {
"@ohos/routermodule": "file:../RouterModule",
"@ohos/hara": "file:../harA",
"@ohos/harb": "file:../harB",
"@ohos/harc": "file:../harC"
}
- 在工程主入口模块Entry.hap中配置build-profile.json5文件,在该文件中修改packages字段,将需要进行路由注册的业务har模块写入配置。
{
// ...
"buildOption": {
"arkOptions": {
"runtimeOnly": {
"sources": [
],
"packages": [
"@ohos/hara",
"@ohos/harb",
"@ohos/harc"
]
}
}
},
}
- 在工程主入口模块的首页Navigation组件上关联RouterModule模块的路由栈和路由表。
@Entry
@Component
struct EntryHap {
@State entryHapRouter: NavPathStack = new NavPathStack();
aboutToAppear() {
if (!this.entryHapRouter) {
this.entryHapRouter = new NavPathStack();
}
RouterModule.createRouter(RouterNameConstants.ENTRY_HAP, this.entryHapRouter);
};
@Builder
routerMap(builderName: string, param: object) {
// Obtain the WrappedBuilder object based on the module name, create a page through the builder interface, and import the param parameter.
RouterModule.getBuilder(builderName).builder(param);
};
build() {
Navigation(this.entryHapRouter) {
// ...
}
.title('NavIndex')
.navDestination(this.routerMap);
}
}
- 在harB中声明需要跳转的页面,并且调用registerBuilder接口将页面注册到RouterModule模块的全局路由表上。以下注册逻辑会在harB的B1页面被首次加载时触发。
// harB模块的B1页面
@Builder
export function harBuilder(value: object) {
NavDestination() {
Column() {
// ...
}
// ...
}
// ...
}
// 在页面首次加载时触发执行
const builderName = BuilderNameConstants.HARB_B1;
// 判断表中是否已存在路由信息,避免重复注册
if (!RouterModule.getBuilder(builderName)) {
// 通过系统提供的wrapBuilder接口封装@Builder装饰的方法,生成harB1页面builder
let builder: WrappedBuilder<[object]> = wrapBuilder(harBuilder);
// 注册harB1页面到全局路由表
RouterModule.registerBuilder(builderName, builder);
}
- 在harA模块中的A1页面调用RouterModule模块的push方法实现跳转到harB的B1页面。当harB的B1页面被首次通过push方法跳转时,会动态加载B1页面,并且触发步骤3中B1页面的路由注册逻辑,把B1页面注册到RouterModule的全局路由表builderMap中。
@Builder
export function harBuilder(value: object) {
NavDestination() {
Column() {
// ...
Button($r("app.string.to_harb_pageB1"), { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
buildRouterModel(RouterNameConstants.ENTRY_HAP, BuilderNameConstants.HARB_B1);
})
}
.width('100%')
.height('100%')
}
.title('A1Page')
.onBackPressed(() => {
RouterModule.pop(RouterNameConstants.ENTRY_HAP);
return true;
})
}
上述方案,当在entry模块页面上点击跳转到harA模块的页面时序图如下:
图5 entry模块页面上点击跳转到harA模块的时序图
注意
上述内容主要介绍了HAR包,若开发者使用了HSP包或者混合使用了HAR与HSP,都需要按如下代码,配置项目根目录下的build-profile.json5文件。
{
"app": {
"signingConfigs": [],
"products": [
{
// ...
"buildOption": {
"strictMode": {
"useNormalizedOHMUrl": true
}
}
}
],
// ...
},
// ...
}
业务实现中的关键点
动态加载
上述方案实现解耦的关键是使用了动态加载的方式和自执行的函数。针对该解决方案可以根据业务需求进一步优化和封装。例如harB中需要注册多个页面:B1、B2、B3。改进方式如下:
1. 在harB的对外导出类Index.ets中定义加载时的初始化函数harInit,该函数对harB中需要注册路由的页面组件进行加载管理,被调用时将根据不同的路径动态加载不同的页面。
export function harInit(builderName: string): void {
// 根据routerModule中路由表的key值动态加载要跳转的页面的相对路径
switch (builderName) {
case BuilderNameConstants.HARB_B1:
import("./src/main/ets/components/mainpage/B1");
break;
case BuilderNameConstants.HARB_B2:
import("./src/main/ets/components/mainpage/B2");
break;
case BuilderNameConstants.HARB_B3:
import("./src/main/ets/components/mainpage/B3");
break;
default:
break;
}
}
2. 优化RouterModule模块中的push方法。为了便于路由跳转时能携带更多信息,增加路由信息类RouterModel作为push时的入参。在push方法中通过该参数获取跳转页面所在的包名、路由名和所需的参数信息。通过包名成功加载har 模块后,根据路由名builderName调用har 模块index页面上定义的harInit函数,实现har模块内多页面的动态加载。
// src/main/ets/utils/RouterModule.ets
public static async push(router: RouterModel): Promise<void> {
const harName = router.builderName.split('_')[0];
await import(harName).then((ns: ESObject): Promise<void> => ns.harInit(router.builderName));
RouterModule.getRouter(router.routerName).pushPath({ name: router.builderName, param: router.param });
}
// 路由信息类,便于跳转时传递更多信息
export class RouterModel {
// 路由页面别名,形式为${包名}_${页面名}
builderName: string = "";
// 路由栈名称
routerName: string = "";
// 需要传入页面的参数
param?: object = new Object();
}
// 创建路由信息,并放到路由栈表中
export function buildRouterModel(routerName: string, builderName: string, param?: object) {
let router: RouterModel = new RouterModel();
router.builderName = builderName;
router.routerName = routerName;
router.param = param;
RouterModule.push(router);
}
路由栈管理
有些业务场景中存在需要使用多个Navigation组件的情况,该场景下需要在RouterModule中管理多个与Navigation组件对应的路由栈NavPathStack对象。此时,可以在RouterModule模块中建立一个路由栈表,以key-value的形式存储多个Navigation组件对应的的路由栈,以此实现多路由栈的管理。增加路由栈后,RouterModule中的路由方法都需要先通过routerName获取到路由栈,再进行方法调用。代码如下:
export class RouterModule {
// ...
// 初始化路由栈,需要关联Navigation组件
static routerMap: Map<string, NavPathStack> = new Map<string, NavPathStack>();
// 通过名称注册路由栈
public static createRouter(routerName: string, router: NavPathStack): void {
RouterModule.routerMap.set(routerName, router);
}
// 通过名称获取路由栈
public static getRouter(routerName: string): NavPathStack {
return RouterModule.routerMap.get(routerName) as NavPathStack;
}
// 通过传入RouterModule跳转到指定页面组件,RouterModule中需要增加routerName字段用于获取路由栈
public static async push(router: RouterModel): Promise<void> {
const harName = router.builderName.split('_')[0];
await import(harName).then((ns: ESObject): Promise<void> => ns.harInit(router.builderName));
RouterModule.getRouter(router.routerName).pushPath({ name: router.builderName, param: router.param });
}
// ...
}
案例参考
场景描述
在实际开发中,除了路由跳转还有路由返回或跳转到指定路由等场景。针对上述路由设计方案,将以下述场景为例,介绍其他路由功能在该场景下的实现。假定场景包含3个业务模块,分别对应工程的3个har包A.har、B.har、C.har。A.har模块中包含A1组件。B.har模块中包含B1、B2、B3组件。C.har模块中包含C1、C2组件。各模块间路由跳转链路如下:
图6 3个har模块路由跳转场景
当从路由路径为A1->B1->B2->B3->C1->C2->B2时,此时在B2组件上需要返回到A1组件,有以下两种场景。
场景1:逐级返回到A1页面,路由链路为:B2->C2->C1->B3->B2->B1->A1。
场景2:跳过中间页面,直接返回到B1页面,路由链路为:B2->B1->A1。
实现方案
场景1:逐级返回到A1组件,代码实现如下:
- 在RouterModule中实现pop方法。
// 通过路由栈名routerName获取对应的NavPathStack对象,并使用该对象的pop方法实现返回上个路由
public static pop(routerName: string): void {
RouterModule.getRouter(routerName).pop();
}
- 引入RouterModule模块,并在对应的页面中调用pop接口,实现返回上一页面。如在B.har的B2组件中调用pop方法返回C.har中的C2组件。
@Builder
export function harBuilder(value: object) {
NavDestination() {
Column() {
// ...
Button($r("app.string.pop_to_pre_page"), { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
RouterModule.pop(RouterNameConstants.ENTRY_HAP);
})
// ...
}
// ...
}
// ...
}
场景2:跳过中间组件,直接返回到B1组件,再返回到A1组件,代码实现如下:
- 在RouterModule中实现popToName方法。该方法将根据路由名返回到当前路由栈中的与该路由名对应的最近路由。
// 直接跳转到指定路由
public static popToName(routerName: string, builderName: string): void {
RouterModule.getRouter(routerName).popToName(builderName);
}
- 在B.har的B2页面中调用popToName方法。点击跳转按钮后将跳过路由栈中间的其他页面,直接返回到B1页面。
@Builder
export function harBuilder(value: object) {
NavDestination() {
Column() {
// ...
Button($r("app.string.direct_to_harb_pageB1"), { stateEffect: true, type: ButtonType.Capsule })
.width('80%')
.height(40)
.margin(20)
.onClick(() => {
RouterModule.popToName(RouterNameConstants.ENTRY_HAP, BuilderNameConstants.HARB_B1)
})
}
// ...
}
// ...
}