
鸿蒙技术分享:🎉🎉🎉喜大普奔!!!鸿蒙官方开源路由管理组件HMRouter
🎉🎉🎉喜大普奔!鸿蒙官方开源发布了路由管理组件HMRouter
介绍
HarmonyOS官方对于鸿蒙应用中页面路由管理的方案是推荐使用Navigation
,但是Navigation
页面的注册向来麻烦,虽然目前增加了动态路由表,但其配置方法相较@ohos.router的@Entry注册方式还是麻烦很多。
除了系统方案之外,官方也给出了cases/dynamicRouter方案,但没有直接提供;另外还有类似的第三方方案。
不过在8月底,官方开源了一款高度封装的路由管理组件https://ohpm.openharmony.cn/#/cn/detail/@hadss%2Fhmrouter@hadss/HMRouter
我们先来看一下其功能:
# HMRouter简介
HMRouter作为HarmonyOS的页面跳转场景解决方案,聚焦解决应用内原生页面的跳转逻辑。
HMRouter底层对系统Navigation进行封装,集成了Navigation、NavDestination、NavPathStack的系统能力,提供了可复用的路由拦截、页面生命周期、自定义转场动画,并且在跳转传参、额外的生命周期、服务型路由方面对系统能力进行了扩展。
目的是让开发者在开发过程中无需关注Navigation、NavDestination容器组件的相关细节及模板代码,屏蔽跳转时的判断逻辑,降低拦截器、自定义转场动画实现复杂度,更好的进行模块间解耦。
# 特性
- 基于注解声明路由信息
- 注解中页面路径支持使用字符串常量定义
- 支持Har、Hsp、Hap
- 支持Navigation路由栈嵌套
- 支持服务型路由
- 支持路由拦截器(包含全局拦截、单页面拦截、跳转时一次性拦截)
- 支持生命周期回调(包含全局生命周期、单页面生命周期、跳转时一次性生命周期、NavBar生命周期)
- 内置转场动画(页面、Dialog),可配置方向、透明度、缩放,支持交互式转场动画,同时支持配置某个页面的转场动画、跳转时的一次性动画
- 支持Dialog类型页面、支持单例页面
注解声明、服务路由、生命周期回调、Dialog页面、交互动画,常见的业务场景问题都覆盖了。
单从功能上来说,确实很强大,虽然我自己也封装开源了路由组件https://gitee.com/FantasyWind/fwrouter@fw/router,但不得不承认,官方的这一套HMRouter,已经很全面了,第三方的路由组件除了在便捷性上加以封装,可拓展的空间已经不多了。
使用体验
Hvigor插件
1.修改项目的hvigor/hvigor-config.json文件,加入路由编译插件
2.项目根目录创建路由编译插件配置文件hmrouter_config.json(推荐)
3.在模块中引入路由编译插件,修改hvigorfile.ts
果然,该组件还是使用了Hvigor插件来实现路由页面注册。
使用插件虽然不能说有问题,但毕竟不像@ohos.router
的@Entry
那样方便直接,如果鸿蒙官方能直接把这套逻辑做进系统SDK才是最好的。
按官方README文档操作,第2步写的是(推荐),看似不强制,但实际运行会报错:
这里要注意:@hadss/hmrouter-plugin
给har/hsp/hap分别提供了不同的插件harPlugin/hspPlugin/hapPlugin,如果类型不对会报错。
运行时配置
文档里要求依赖的模块名称需要与模块的moduleName保持一致。
,这里需要注意。
因为IDE中创建Har或Hsp时,模块名都可以写大写字母,但是oh-package.json5
里面的模块名不支持大写字母,会强制转小写。
entry注册页面
import { HMRouter, HMRouterMgr } from '@hadss/hmrouter'
import { JSON } from '@kit.ArkTS'
@HMRouter({ pageUrl: 'pageB' })
@Component
export struct PageB {
@State param: Object | null = HMRouterMgr.getCurrentParam()
build() {
Column() {
Text(`${JSON.stringify(this.param)}`)
Button() {
Text('popWithResult')
}
.onClick(() => {
HMRouterMgr.pop({ param: { 'resultFrom': 'PageB' } })
})
}
}
}
@HMRouter({ pageUrl: 'pageB' })
这种类@Entry的页面注册方式确实很方便。
不过我在这里有一个疑惑点:
写过Navigation的同学应该都知道,Navigation页面必须嵌套一层NavDestination。
但是这里面没有,不知道是否是Hvigor插件动态包裹了一层。
打开页面
import { HMPopInfo, HMRouter, HMRouterMgr } from '@hadss/hmrouter'
import { promptAction } from '@kit.ArkUI'
import { JSON } from '@kit.ArkTS'
const PAGE_URL: string = 'pageA'
@HMRouter({ pageUrl: PAGE_URL})
@Component
export struct PageA {
build() {
Column() {
Button('Push')
.onClick(() => {
HMRouterMgr.push({pageUrl: 'pageB', param: { 'from': 'PageA' }}, {
onResult: (popInfo: HMPopInfo) => {
promptAction.showDialog({ message: JSON.stringify(popInfo.result)})
},
onArrival: () => {
},
onLost: () => {
}
})
})
}
}
}
打开页面传参、获取返回值也都很方便。
har注册页面
import { HMRouter, HMRouterMgr } from '@hadss/hmrouter'
import { JSON } from '@kit.ArkTS'
@HMRouter({ pageUrl: 'libraryhar/pageB' })
@Component
export struct PageB {
@State param: Object | null = HMRouterMgr.getCurrentParam()
build() {
Column() {
Text(`${JSON.stringify(this.param)}`)
Button() {
Text('popWithResult')
}
.onClick(() => {
HMRouterMgr.pop({ param: { 'resultFrom': 'libraryhar/PageB' } })
})
}
}
}
和在entry中注册页面一样,只需要@HMRouter({ pageUrl: 'libraryhar/pageB' })
就可以正常打开了。
Button() {
Text('libraryhar/PageA')
}
.onClick(() => {
HMRouterMgr.push({pageUrl: 'libraryhar/pageA'})
})
这是真有点东西!!!
为什么呢?在我封装@fw/router时,为了解决entry和har包代码解耦踩了很多坑,其核心点在于动态import。
先看下之前遇到的问题:
#### 动态导入问题
解释:
动态导入是指应用开始运行时不import页面路由或者服务路由所在的har包,而是在业务代码调用指定路由时,先import指定har包,然后再执行相关代码。
##### 开启动态导入能力
修改`entry/build-profile.json5`,增加相关设置:
``json5
{
"buildOption": {
"arkOptions": {
"runtimeOnly": {
"sources": [],
"packages": [
"@fw/router",
'libraryHar',
"libraryHarDemo",
]
}
}
},
}
``
这里核心的runtimeOnly
设置,HMRouter并没有要求。
排除使用了黑科技的情况下,那只能是Hvigor插件进行编译动态修改。
不过HMRouter默认不保留编译生成的代码,解压.har包也看不到动态生成的代码,就很神奇。
理论上通过hmrouter_config.json
:
{
"scanDir": [],
"builderDir": "src/main/ets/generated",
"saveGeneratedFile": true
}
saveGeneratedFile
设置为true应该可以保留生成的代码,但是我尝试开启后反而会导致无法正常push页面。😂😂😂
后面再研究研究实现原理。
服务路由
export class CustomService {
@HMService({ serviceName: 'testConsole' })
testConsole(): void {
console.log('调用服务 testConsole')
}
@HMService({ serviceName: 'testFunWithReturn' })
testFunWithReturn(param1: string, param2: string): string {
return `调用服务 testFunWithReturn:${param1} ${param2}`
}
@HMService({ serviceName: 'testAsyncFun', singleton: true })
async asyncFunction(): Promise<string> {
return new Promise((resolve) => {
resolve('调用异步服务 testAsyncFun')
})
}
}
HMRouter的服务路由是通过@HMService
注册,但除此之外不需要任何设置。
之所以要提这么一句,是因为@HMService
和@HMRouter
不同,@HMRouter
装饰在struct上面,不会真正执行,需要依靠Hvigor插件来解析实现。
但是@HMService
是真正的类方法装饰器,理论上能够正常触发执行。
我在@fw/router
中实现服务路由时就是通过类装饰器进行服务路由注册的。
不过看@HMService
代码:
export function HMService(param: HMServiceParam) {
return (target: ESObject, propertyKey: string, descriptor: PropertyDescriptor) => {}
}
@HMService
没有真正的实现代码,看来@HMService
也是通过Hvigor进行处理的。
拦截器
import { HMInterceptor, HMInterceptorAction, HMInterceptorInfo, IHMInterceptor } from '@hadss/hmrouter';
@HMInterceptor({ interceptorName: 'JumpInfoInterceptor', global: true })
export class JumpInfoInterceptor implements IHMInterceptor {
handle(info: HMInterceptorInfo): HMInterceptorAction {
let connectionInfo: string = info.type === 'push' ? 'jump to' : 'back to';
console.log(`${info.srcName} ${connectionInfo} ${info.targetName}`)
return HMInterceptorAction.DO_NEXT;
}
}
拦截器同样是通过@HMInterceptor
注解实现,使用方便。
对于实际的项目开发来说,拦截器的作用非常重要,拦截器使用方便的话真的能省很多事。
生命周期
说是生命周期,但是在看到生命周期也用注解来实现时还有点莫明其妙。
看了文档和示例代码才明白过来,HMRouter的生命周期有两种使用场景:
- 全局生命周期:页面生命周期拦截器,拦截所有页面的生命周期,用来处理基于页面生命周期的公共逻辑;
import { HMLifecycle, HMLifecycleContext, IHMLifecycle } from '@hadss/hmrouter';
@HMLifecycle({ lifecycleName: 'PageDurationLifecycle', global: true })
export class PageDurationLifecycle implements IHMLifecycle {
private time: number = 0;
onShown(ctx: HMLifecycleContext): void {
this.time = new Date().getTime();
}
onHidden(ctx: HMLifecycleContext): void {
const duration = new Date().getTime() - this.time;
console.log(`Page ${ctx.navContext?.pathInfo.name} stay ${duration}`);
}
}
- 非全局生命周期:管理页面代码,可以将不同维度的代码从页面主代码中抽离。
@HMLifecycle({ lifecycleName: 'PageBLifecycle' })
export class PageBLifecycle implements IHMLifecycle {
onAppear(ctx: HMLifecycleContext): void {
console.log(`onAppear`)
}
}
@HMRouter({ pageUrl: 'libraryhar/pageB', lifecycle: 'PageBLifecycle' })
@Component
export struct PageB
{
}
过场动画
import { HMAnimator, HMAnimatorHandle, IHMAnimator, OpacityOption, ScaleOption, TranslateOption } from '@hadss/hmrouter'
@HMAnimator({ animatorName: 'liveCommentsAnimator' })
export class liveCommentsAnimator implements IHMAnimator {
effect(enterHandle: HMAnimatorHandle, exitHandle: HMAnimatorHandle): void {
// 入场动画
enterHandle.start((translateOption: TranslateOption, scaleOption: ScaleOption,
opacityOption: OpacityOption) => {
translateOption.y = '100%'
})
enterHandle.finish((translateOption: TranslateOption, scaleOption: ScaleOption,
opacityOption: OpacityOption) => {
translateOption.y = '0'
})
enterHandle.duration = 500
// 出场动画
exitHandle.start((translateOption: TranslateOption, scaleOption: ScaleOption,
opacityOption: OpacityOption) => {
translateOption.y = '0'
})
exitHandle.finish((translateOption: TranslateOption, scaleOption: ScaleOption,
opacityOption: OpacityOption) => {
translateOption.y = '100%'
})
exitHandle.duration = 500
}
}
@HMRouter({ pageUrl: 'libraryhar/pageB', animator: 'liveCommentsAnimator' })
@Component
export struct PageB {
}
使用@HMAnimator
定义自定义过场动画,在@HMRouter
中指定animator: 'liveCommentsAnimator'
参数进行使用。
总结
HMRouter的几个主要功能,都是使用注解来完成的,不得不说这样的实现方式在使用时体验比较统一,确实不错。
HMRouter相比系统的Navigation好用了很多,能满足大部分的实际开发场景。
HMRouter的开源组织名为HADSS
,全称是HarmonyOS应用开发场景套件Application Development Scenario
,可以看出该组织就是为了解决实际开发场景中的各种需求而设立的。
HMRouter的出现真的很棒,毕竟系统路由太难用了🤣🤣🤣
更多推荐
所有评论(0)