• 作者:老汪软件技巧
  • 发表时间:2024-09-11 15:01
  • 浏览量:

前言

作为一个鸿蒙新手,在 Harmony NEXT 发布后不久就去学习了一下,也水了一篇文章「鸿蒙即将抛弃Android,你还不来学习一下?」,在部分,我们知道鸿蒙上使用 router 模块进行页面跳转。

转眼几个月过去了,再去看官方文档,router 已经被标记为「不推荐」了(还没焐热就被废弃了),同时官方推荐使用 Navigation(这又是个什么玩意),既然官方推荐了,今天就和大家一起学习一下。

这里多说一句,第一眼看到 Navigation 的时候,怎么感觉有点熟悉,没错,Android Jetpack Compose 中的路由组件也叫 Navigation,而且二者的功能也非常接近,看来华为是借鉴了 Google 的命名。

结论

先说结论:Navigation 提供了简单易用的分栏模式,如果你有平板/折叠屏适配需求,那么强烈建议你使用 Navigation,如果你的应用只在手机上运行,那么使用 Navigation 和 router 几乎没有差别。

以 wan-harmony 为例,对比使用 router 和 Navigation 在平板上的效果:

介绍

Navigation 是路由容器组件,一般作为首页的根容器,包括单栏(Stack)、分栏(Split)和自适应(Auto)三种显示模式。

既然 Navigation 是用来取代 router 的,那在功能上,自然和 router 差不多,而 Navigation 的强大之处在于,它提供了自适应分栏能力,即在手机上是正常的全屏路由,而在平板/折叠屏上自动分栏展示,可以说 Navigation 是为了大屏而生。

除此之外,与 router 只能全屏页面路由不同,Navigation 还支持嵌套,即 Navigation 的子页面也可以是另一个 Navigation,两者相互独立,由于 Navigation 也是一个 UI 组件,因此可以自由设置大小。

基础使用替换根布局

首先需要把入口页面的根布局替换为 Navigation 组件。

@Entry
@Component
struct Index {
  pageStack : NavPathStack = new NavPathStack();
  build() {
    Navigation(this.pageStack) {
      // 原始首页布局
      this.tabContainerBuilder()
    }
    .mode(NavigationMode.Auto)
    .hideTitleBar(true)
  }
}

注册系统路由表

将目标跳转页面添加到系统 Navigation 路由表。

a. 在跳转目标模块的配置文件 module.json5 添加路由表配置:

{
  "module" : {
    "routerMap": "$profile:route_map"
  }
}

b. 添加完路由配置文件地址后,需要在工程 resources/base/profile 中创建 route_map.json 文件。添加如下配置信息:

{
  "routerMap": [
    {
      "name": "PageOne",
      "pageSourceFile": "src/main/ets/pages/PageOne.ets",
      "buildFunction": "PageOneBuilder",
      "data": {
        "description" : "this is PageOne"
      }
    }
  ]
}

c. 在跳转目标页面中,需要配置入口 Builder 函数,函数名称需要和 router_map.json 配置文件中的buildFunction 保持一致,否则在编译时会报错。

  // 跳转页面入口函数
  @Builder
  export function PageOneBuilder() {
    PageOne()
  }
  @Component
  struct PageOne {
    pathStack: NavPathStack = new NavPathStack()
    build() {
      NavDestination() {
      }
      .title('PageOne')
      .onReady((context: NavDestinationContext) => {
         this.pathStack = context.pathStack
      })
    }
  }

d. 通过 pushPathByName 等路由接口进行页面跳转。

  @Entry
  @Component
  struct Index {
    pageStack : NavPathStack = new NavPathStack();
    build() {
      Navigation(this.pageStack){
      }.onAppear(() => {
        this.pageStack.pushPathByName("PageOne", null, false);
      })
      .hideTitleBar(true)
    }
  }

进阶使用

可以看出,基础使用和 router 基本一致,但总感觉差了点什么,没错,注册路由不够灵活,路由 API 使用上也不够简洁,那么能否实现跟 Android 上的路由框架一致的使用体验呢,比如 CRouter:

CRouter.with(this)
    .url("scheme://host/target.html")
    .extra("key", "value")
    .startForResult {
        if (it.isSuccess("key")) {
            val value = it.data?.getStringExtra("key")
            alert("跳转取值", value)
        }
    }

推荐标签是什么_火锅店废弃油使用_

下面我们就对 Navigation 进行简单的封装,让它变得更好用。

动态路由

第一步,我们需要支持路由动态注册。

鸿蒙官方提供了动态路由能力,通过 navDestination 即可动态加载布局。

@Entry
@Component
struct Index {
  pageStack : NavPathStack = new NavPathStack();
  build() {
    Navigation(this.pageStack) {
      this.tabContainerBuilder()
    }
    .mode(NavigationMode.Auto)
    // 动态布局
    .navDestination(this.navDestination)
    .hideTitleBar(true)
  }
  @Builder
  navDestination(url: string, params: object) {
    // 根据 url 加载对应页面布局
    if (url === '/web') {
      WebPage()
    } else {
      // ...
    }
  }
}

我们只需要把所有页面布局注册到路由表,即可实现动态路由。

封装

接下来,我们对路由接口进行封装。

路由信息

创建一个对象,用于保存路由相关的信息,如页面 url、参数、动画等等。

export class RouteInfo {
  targetUrl: string = ''
  currentParams: RouteParams = new Map()
  targetParams: RouteParams = new Map()
  mode: RouteMode = RouteMode.Standard
  animated: boolean = true
  onPop?: (params: RouteParams) => void
}
export type RouteParams = Map<string, Object>
export enum RouteMode {
  Standard = 'Standard',
  MoveToTop = 'MoveToTop',
  ClearTop = 'ClearTop',
}

路由请求

类似 CRouter,我们将每次跳转行为看做一次路由请求,请求中包含路由信息。

export class RouteRequest {
  private readonly routeInfo: RouteInfo = new RouteInfo()
  constructor(navDesInfo?: NavDestinationInfo) {
    if (navDesInfo) {
      this.routeInfo.currentParams = (navDesInfo.param ?? new Map()) as RouteParams
    }
  }
  getCurrentParams(): RouteParams {
    return this.routeInfo.currentParams
  }
  url(url: string): RouteRequest {
    this.routeInfo.targetUrl = url
    return this
  }
  params(key: string, value: Object): RouteRequest {
    this.routeInfo.targetParams.set(key, value)
    return this
  }
  mode(value: RouteMode): RouteRequest {
    this.routeInfo.mode = value
    return this
  }
  animated(value: boolean): RouteRequest {
    this.routeInfo.animated = value
    return this
  }
  start(onPop?: (params: RouteParams) => void) {
    this.routeInfo.onPop = onPop
    // 执行跳转
  }
  back() {
    // 执行返回
  }
}

路由实现

我们需要一个真正执行路由操作的执行器。

class _RouteExecutor {
  start(info: RouteInfo) {
    let builder = HRouter.getNavDesBuilder(info.targetUrl)
    if (!builder) {
      return
    }
    switch (info.mode) {
      case RouteMode.ClearTop:
        this.startWithClearTop(info)
        break;
      case RouteMode.MoveToTop:
        this.startWithMoveToTop(info)
        break;
      case RouteMode.Standard:
      default:
        this.startWithStandardMode(info)
        break;
    }
  }
  back(info: RouteInfo) {
    HRouter.pathStack.pop(info.targetParams, info.animated)
  }
  private startWithStandardMode(info: RouteInfo) {
    if (info.onPop) {
      let onPopListener = info.onPop
      HRouter.pathStack.pushPathByName(info.targetUrl, info.targetParams, (popInfo) => {
        onPopListener(popInfo.result as RouteParams)
      }, info.animated)
    } else {
      HRouter.pathStack.pushPathByName(info.targetUrl, info.targetParams, info.animated)
    }
  }
  private startWithClearTop(info: RouteInfo) {
    let indices = HRouter.pathStack.getIndexByName(info.targetUrl)
    if (indices.length) {
      let lastIndex = indices[indices.length - 1]
      while (lastIndex <= HRouter.pathStack.size() - 1) {
        HRouter.pathStack.pop(false)
      }
      this.startWithStandardMode(info)
    } else {
      this.startWithStandardMode(info)
    }
  }
  private startWithMoveToTop(info: RouteInfo) {
    let indices = HRouter.pathStack.getIndexByName(info.targetUrl)
    if (indices.length) {
      HRouter.pathStack.moveIndexToTop(indices[indices.length - 1], info.animated)
    } else {
      this.startWithStandardMode(info)
    }
  }
}
export const RouteExecutor: _RouteExecutor = new _RouteExecutor();

使用和 Navigation 容器绑定的 NavPathStack 进行路由操作。

路由门面

最后,我们需要提供一个路由能力的入口。

class _HRouter {
  readonly pathStack: NavPathStack = new NavPathStack()
  private readonly navDesBuilderMap: Map<string, WrappedBuilder<[RouteParams]>> = new Map()
  register(url: string, builder: WrappedBuilder<[RouteParams]>) {
    this.navDesBuilderMap.set(url, builder)
  }
  getNavDesBuilder(url: string): WrappedBuilder<[RouteParams]> | undefined {
    return this.navDesBuilderMap.get(url)
  }
  with(info?: NavDestinationInfo): RouteRequest {
    return new RouteRequest(info)
  }
  clearStack() {
    this.pathStack.clear(false)
  }
}
export const HRouter: _HRouter = new _HRouter()

效果

看一下最终执行路由操作的代码:

HRouter.with()
  .url(Routes.Login)
  .params('key', 'value')
  .start((params) => {
    if (params.get('success')) {
      onLogin()
    }
  })

对比下封装前:

this.pageStack.pushPathByName("PageOne", params,
  (popInfo) => {
    onPopListener(popInfo.result as RouteParams)
  }, false);

对比起来,封装后的看着更加顺畅,而且这仅仅是最简单的用法,NavPathStack 提供了诸多跳转方法,如 replacePathByName、moveToTop 等等,我们可以全部封装到 HRouter 中,通过参数来控制,对上层更加友好。

需要源码的可以点击传送门。

总结

本文主要介绍鸿蒙上最新路由方案 Navigation 及其使用方法,对比 router 可以更轻松的适配平板/折叠屏,通过对 NavPathStack 接口的封装,使得接口易用性大大提高。

目前路由还需要手动注册,如果要做到极致的体验,可以通过 plugin 在编译器自动生成路由表,不过这部分不是本文的核心,有兴趣的朋友可以尝试一下。


上一条查看详情 +说下锁的理解当然只是简单的理解
下一条 查看详情 +没有了