【HarmonyOS实战开发】鸿蒙@fw/router框架源码解析

31 篇文章 0 订阅
31 篇文章 0 订阅

介绍

@fw/router是在HarmonyOS鸿蒙系统中开发应用所使用的开源模块化路由框架。
该路由框架基于模块化开发思想设计,支持页面路由和服务路由,支持自定义装饰器自动注册,与系统路由相比使用更便捷,功能更丰富。

基于模块化的开发需求,本框架支持以下功能:

  • 支持页面路由和服务路由;
  • 页面路由支持多种模式(router模式,Navigation模式,混合模式);
  • router模式支持打开非命名路由页面;
  • 页面打开支持多种方式(push/replace),参数传递;关闭页面,返回指定页面,获取返回值,跨页面获取返回值;
  • 支持服务路由,可使用路由url调用公共方法,达到跨技术栈调用以及代码解耦的目的;
  • 支持页面路由/服务路由通过装饰器自动注册;
  • 支持动态导入(在打开路由时才import对应har包),支持自定义动态导入逻辑;
  • 支持添加拦截器(打开路由,关闭路由,获取返回值);
  • Navigation模式下支持自定义Dialog对话框;

代码结构

@fw/router代码结构

├── FWNavigation.ets // 封装系统Navigation,给router页面附加Navigation页面管理能力
├── RouterDefine.ts // router组件类型定义
├── RouterInterceptorManager.ets // 路由组件拦截器管理类
├── RouterManager.ets // 路由组件管理类,解耦方法调用三种路由管理类和拦截器
├── RouterManagerForNavigation.ets // 对接Navigation能力的路由组件管理类
├── RouterManagerForService.ts // 对接服务能力的路由组件管理类
└── RouterManagerForSystemRouter.ts // 对接@ohos.router能力的路由组件管理类

@fw/router目前主要包含了四块核心功能和两块附加功能。

四块核心功能,分别是:RouterManager(路由组件管理器),RouterManagerForSystemRouter(系统router路由管理器),RouterManagerForNavigation(Navigation路由管理器),RouterManagerForService(服务路由管理器)。
两块附加功能,分别是:FWNavigation(封装系统Navigation容器),RouterInterceptorManager(路由拦截器)。

基于低耦合高内聚的思想,系统router路由,Navigation路由,服务路由功能分别封装到各自的管理类中,并且统一实现RouterHandler接口。

因此,理论上系统router路由、Navigation路由、服务路由封装的功能都可以单独使用。有需求可以fork代码自由修改。

@fw/router架构图

image.png

代码解析

我们按照Router页面、Navigation页面、服务路由三条功能线来解析源代码。

Router页面

router页面注册还是使用系统的@Entry装饰器。

@Entry({ routeName: "testPage" })
@Component
export struct TestPage {
// ...
}
openWithRequest

如何打开TestPage页面呢?


RouterManager.getInstance().openWithRequest({
 url: 'libraryHar/testPage',
 params: { 'from': 'Home' }
})

openWithRequest方法的入参是RouterRequest,是interface类型。

export interface RouterRequest {
 /**
 * 路由名称。如果包含url参数,则会覆盖`params`中的同名参数。
 */
 url: string
 /**
 * 参数。
 */
 params?: Record<string, any>
 /**
 * 页面路由打开方式。
 */
 openMode?: PageRouteOpenMode
 /**
 * 打开路由所在的页面对象。有些路由需要知道是哪个页面在调用我。
 */
 pageInstance?: ESObject
 /**
 * 页面策略,若为undefined,默认取RouterManager全局逻辑
 */
 routerStrategy?: RouterStrategy
}

为什么这里要用interface,不用class
主要是因为ArkTS的class类型不允许用字面量初始化,必须是构造器,十分不方便。如下:


RouterManager.getInstance().openWithRequest(new RouterRequest({
 url: 'libraryHar/testPage',
 params: { 'from': 'Home' }
}))

因此openWithRequest方法的入参声明为interface类型。

但是interface类型也有问题,无法再去定义方法,如果要对request参数进行处理,只能写在外部,不符合代码内聚的设计原则。
因此,业务代码中又封装了RouterRequestWrapper类,并在openWithRequest方法中进行了转换。

let wrapper = new RouterRequestWrapper(request)

所以,在openWithRequest方法之后的代码中,请求参数的类型都是RouterRequestWrapper类型。

_realOpen

我们可以看到_realOpen方法的入参和openWithRequest方法完全相同,而openWithRequest方法一共就三行代码,为什么不把这两个方法合并呢?

原因就在于RouterInterceptorManager,我们的拦截器使用插桩的形式进行方法拦截。
如果我们直接插桩拦截openWithRequest方法,那么拦截器方法中拦截到的入参就是interface类型RouterRequest。interface类型有什么问题见上一节。
因此,我们单独拆分了_realOpen方法,这样插桩拦截方法就可以拦截到RouterRequestWrapper

_realOpen方法的主要功能是动态导入代码包。

关于动态导入

该方法支持四种场景:

  1. 关闭了动态导入能力:enableDynamicImport为false;不会执行导入逻辑;
  2. Entry包中的页面(pages/Second),也不需要导入;
  3. 自定义了动态导入方法(delegate.dynamicImport);
  4. 如果以上都没有,则执行默认的动态导入方法(import(packageName));

同时,该方法还支持路由模块名和包名之间的映射:

比如,routeUrl为login/LoginPage,但是实际的login模块命名为@business/login

// 此处代码仅为演示,正常情况下可以在EntryAbility中统一设置
RouterManager.getInstance().setModuleToPackageMapping('login', '@business/login')
// 若未配置模块名包名映射,则直接使用模块名动态导入
let packageName: string = this.moduleNameMapping[request.moduleName] ?? request.moduleName

导包逻辑处理完成后即进入open方法。

open
open(request: RouterRequestWrapper): Promise<RouterResponse> {
 return new Promise<RouterResponse>(async (resolve, reject) => {
 let result: RouterResponse | undefined
 // 默认优先响应服务路由
 let list = this.finalHandlerList(request.rawRequest.routerStrategy)
 for (const handler of list) {
 let response = await handler.open(request)
 if (response.code != RouterResponseError.RequestNotFoundResponsor.code) {
 result = response
 break
 }
 }
 if (!result) {
 result = RouterResponseError.RequestNotFoundResponsor
 }
 this.processResponse(resolve, request, result)
 })
 }

该方法是处理Router,Navigation,服务路由解耦的核心方法。
该方法获取到handlerList后循环遍历,调用其open方法:let response = await handler.open(request)

至于handlerList的取值,依赖于具体的路由策略。

 finalHandlerList(routerStrategy?: RouterStrategy): Array<RouterHandler> {
 if (routerStrategy == undefined) {
 return [RouterManagerForService.getInstance(), ...this.handlerList]
 } else {
 let handlerList = this.getHandlerList(routerStrategy)
 return [RouterManagerForService.getInstance(), ...handlerList]
 }
 }

 getHandlerList(value: RouterStrategy) {
 let list: Array<RouterHandler> = []
 switch (value) {
 case RouterStrategy.navigationFirst:
 list = [
 RouterManagerForNavigation.getInstance(),
 RouterManagerForSystemRouter.getInstance(),
 ]
 break;

 case RouterStrategy.routerFirst:
 list = [
 RouterManagerForSystemRouter.getInstance(),
 RouterManagerForNavigation.getInstance(),
 ]
 break;

 case RouterStrategy.navigationOnly:
 list = [
 RouterManagerForNavigation.getInstance(),
 ]
 break;

 case RouterStrategy.routerOnly:
 list = [
 RouterManagerForSystemRouter.getInstance(),
 ]
 break;

 default:
 break;
 }
 return list
 }

getHandlerList方法通过RouterStrategy策略字段确定不同的handlerList(路由处理器列表)。

我们现在关注router流程,支需要看RouterManagerForSystemRouter.getInstance()即可。

RouterManagerForSystemRouter.open

该方法的主体功能主要是处理Entry页面和replace打开页面的逻辑:

 open(request: RouterRequestWrapper): Promise<RouterResponse> {
 return new Promise((resolve, reject) => {
 // ...
 if (request.isRouterPath) {
 switch (request?.rawRequest.openMode) {
 case PageRouteOpenMode.replace:
 // ...

 default:
 // ...
 }
 } else {
 switch (request?.rawRequest.openMode) {
 case PageRouteOpenMode.replace:
 // ...

 default:
 // ...
 }
 }
 })
 }

但这不是RouterManagerForSystemRouter类的重点,该类的重点在于处理router页面的页面返回值。

核心方法在于:

 observerPageLifecycle(uiAbility: UIAbility) {
 observer.on("routerPageUpdate", uiAbility.context, (routerPageInfo: observer.RouterPageInfo) => {
 hilog.info(0x0000, 'routerPageUpdateTAG',
 "life:" + routerPageInfo.path + ":" + routerPageInfo.name + ":index=" + routerPageInfo.index +
 ":routerStateIndex=" + router.getState().index + ";state:" + routerPageInfo.state);

 let name = routerPageInfo.name
 let fromIndex = routerPageInfo.index - 1
 // 通过监听页面生命周期方法,将系统堆栈和routes保持一致,用来处理返回值回调
 switch (routerPageInfo.state) {
 case observer.RouterPageState.ABOUT_TO_APPEAR:
 // state虽然是ABOUT_TO_APPEAR,但实际上也已经进栈,因此`router.getState().index`获取到的index就是当前页面的index,所以需要-1
 if (!this.hasRequest(name, fromIndex)) {
 let request = this.hasUndefinedRequest(name)
 if (request) {
 request.pageInfo = routerPageInfo
 } else {
 this.inject(new RouterRequestWrapper({ url: "other/" + name }, fromIndex), routerPageInfo)
 }
 }

 break

 case observer.RouterPageState.ON_PAGE_SHOW: {
 if (this.resultStrategy == RouterResultStrategy.onPageShow && name === this.backToRouteName) {
 this.backToIndex = routerPageInfo.index

 // A->B,B返回A时,A页面的ON_PAGE_SHOW比B页面的ABOUT_TO_DISAPPEAR早触发,所以延时执行
 setTimeout(() => {
 const params = router.getParams()
 this.lastResolve?.({
 code: RouterResponseError.Success.code,
 msg: RouterResponseError.Success.msg,
 data: params
 })
 }, 500)
 }
 break
 }

 case observer.RouterPageState.ABOUT_TO_DISAPPEAR: {
 if (this.resultStrategy == RouterResultStrategy.onPagePop) {
 const params = router.getParams()
 const request = this.getRequest(name, fromIndex)
 if (request != undefined) {
 request.request?.resolve?.({
 code: RouterResponseError.Success.code,
 msg: RouterResponseError.Success.msg,
 data: params
 })
 }
 } else {
 // 因为routes中可能存在同名请求,因此需要通过指定的backToIndex,找到对应的请求
 if (fromIndex == this.backToIndex) {
 hilog.info(0x0000, 'routerPageUpdateTAG',
 "找到了callback:backToIndex=" + this.backToIndex + ";index:" + routerPageInfo.index);
 const request = this.getRequest(name, fromIndex)
 if (request != undefined) {
 this.lastResolve = request.request?.resolve
 }
 }
 }

 this.removeRequest(name, fromIndex)
 break
 }

 case observer.RouterPageState.ON_BACK_PRESS: {
 const request = this.getRequest(name, fromIndex)
 if (request != undefined) {
 request.request?.resolve?.({
 code: RouterResponseError.Success.code,
 msg: RouterResponseError.Success.msg
 })
 }

 // 触发ON_BACK_PRESS后还会触发ABOUT_TO_DISAPPEAR状态,但因为request已被删除,所以不会重复触发回调
 this.removeRequest(name, fromIndex)
 break
 }
 }
 })
 }
  1. 监听ABOUT_TO_APPEAR状态,将页面与open方法的request参数(inject方法)绑定;
  2. 监听ABOUT_TO_DISAPPEAR状态,直接返回上一页,在本页面消失时,获取到打开本页面的请求,并触发其resolve回调,回传参数;
  3. 返回指定页面的情况下,监听ON_PAGE_SHOW状态,当指定页面触发该状态,则找到该页面发起的请求,并触发其revolve回调,回传参数;
  4. 监听ON_BACK_PRESS状态,处理返回按钮点击和侧滑返回手势;该逻辑不能和第2点合并;因为第2点处理的逻辑中是带参数的(通过router.getParams()获取),而本逻辑是不带参的;

除此之外的代码主要就是RouterRouteInfo的管理逻辑,此处不做赘述。

总结

在整个router页面的流程中,主要的复杂点在于参数类型、动态导入、方法拦截、代码解耦、页面返回值等几个方面,我们在做路由封装时,花费时间和精力最多的也是在这些地方。
普通的api封装在整个组件开发过程中的时间占比并不高。

写在最后

●如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
●点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
●关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
●更多鸿蒙最新技术知识点,请移步前往小编:https://gitee.com/

在这里插入图片描述

  • 15
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值