鸿蒙技术分享:Router页面管理-鸿蒙@fw/router框架源码解析(一)


theme: smartblue

本文是系列文章,其他文章见:

鸿蒙@fw/router框架源码解析

介绍

@fw/router是在HarmonyOS鸿蒙系统中开发应用所使用的开源模块化路由框架。

该路由框架基于模块化开发思想设计,支持页面路由和服务路由,支持自定义装饰器自动注册,与系统路由相比使用更便捷,功能更丰富。

具体功能介绍见https://harmonyosdev.csdn.net/67484183522b003a5471c3f3.html@fw/router:鸿蒙模块化路由框架,助力开发者实现高效模块化开发!

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

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

详见gitee传送门

代码结构

@fw/router代码结构

javascript 复制代码
.
├── 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装饰器。

typescript 复制代码
@Entry({ routeName: "testPage" })
@Component
export struct TestPage {
// ...
}
openWithRequest

如何打开TestPage页面呢?

typescript 复制代码
RouterManager.getInstance().openWithRequest({
  url: 'libraryHar/testPage',
  params: { 'from': 'Home' }
})

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

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

为什么这里要用interface,不用class

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

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

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

但是interface类型也有问题,无法再去定义方法,如果要对request参数进行处理,只能写在外部,不符合代码内聚的设计原则。

因此,业务代码中又封装了RouterRequestWrapper类,并在openWithRequest方法中进行了转换。

typescript 复制代码
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

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

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

open
typescript 复制代码
  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的取值,依赖于具体的路由策略。

typescript 复制代码
  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打开页面的逻辑:

typescript 复制代码
  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页面的页面返回值。

核心方法在于:

typescript 复制代码
  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封装在整个组件开发过程中的时间占比并不高。

相关推荐
深海的鲸同学 luvi4 小时前
【HarmonyOS NEXT】华为分享-碰一碰开发分享
华为·harmonyos·碰一碰·华为分享
沅霖10 小时前
鸿蒙harmony json转对象(2)
harmonyos
kirk_wang1 天前
Flutter调用HarmonyOS NEXT原生相机拍摄&相册选择照片视频
flutter·华为·harmonyos
星释1 天前
鸿蒙Flutter实战:17-无痛上架审核指南
flutter·华为·harmonyos
jikuaidi6yuan1 天前
鸿蒙操作系统的安全架构
华为·harmonyos·安全架构
HarderCoder1 天前
鸿蒙开发者认证-题库(二)
harmonyos
轻口味1 天前
HarmonyOS Next 最强AI智能辅助编程工具 CodeGenie介绍
人工智能·华为·harmonyos·deveco-studio·harmonyos-next·codegenie
jikuaidi6yuan1 天前
除了基本的事件绑定,鸿蒙的ArkUI
华为·harmonyos
GY-931 天前
Flutter中PlatformView在鸿蒙中的使用
flutter·harmonyos
小鱼仙官2 天前
鸿蒙系统 将工程HarmonyOS变成OpenHarmony
华为·harmonyos