- 作者:老汪软件技巧
- 发表时间: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 在编译器自动生成路由表,不过这部分不是本文的核心,有兴趣的朋友可以尝试一下。