介绍
@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架构图
代码解析
我们按照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
方法的主要功能是动态导入代码包。
关于动态导入
该方法支持四种场景:
- 关闭了动态导入能力:enableDynamicImport为false;不会执行导入逻辑;
- Entry包中的页面(pages/Second),也不需要导入;
- 自定义了动态导入方法(delegate.dynamicImport);
- 如果以上都没有,则执行默认的动态导入方法(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
}
}
})
}
- 监听
ABOUT_TO_APPEAR
状态,将页面与open方法的request参数(inject方法)绑定; - 监听
ABOUT_TO_DISAPPEAR
状态,直接返回上一页,在本页面消失时,获取到打开本页面的请求,并触发其resolve回调,回传参数; - 返回指定页面的情况下,监听
ON_PAGE_SHOW
状态,当指定页面触发该状态,则找到该页面发起的请求,并触发其revolve回调,回传参数; - 监听
ON_BACK_PRESS
状态,处理返回按钮点击和侧滑返回手势;该逻辑不能和第2点合并;因为第2点处理的逻辑中是带参数的(通过router.getParams()获取),而本逻辑是不带参的;
除此之外的代码主要就是RouterRouteInfo的管理逻辑,此处不做赘述。
总结
在整个router页面的流程中,主要的复杂点在于参数类型、动态导入、方法拦截、代码解耦、页面返回值等几个方面,我们在做路由封装时,花费时间和精力最多的也是在这些地方。
普通的api封装在整个组件开发过程中的时间占比并不高。
写在最后
●如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
●点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
●关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
●更多鸿蒙最新技术知识点,请移步前往小编:https://gitee.com/