原生鸿蒙路由总结

381 阅读6分钟

Navigation路由导航

使用Navigation代替所有的路由跳转

理念是:一整个应用都只有一个page,剩余其它的部分都是组件【华为推荐使用Navigation路由导航】

第一种路由方式

  • 根组件最外层必须被Navigation包裹
  • 要被跳转的组件必须被NavDestination包裹
  • 跳转对象是用NavPathStack对象
  • NavPathStack必须绑定给Navigation
  • 下层组件使用各种方式获取上层的NavPathStack进行跳转
  • 跳转维护由.navDestination实现
  • Navigation绑定NavPathStack,最好用Provide提供给子组件使用
  • 路由管理-给Navigation绑定.navDestination(builder)- builder不能写小括号模式
 import { Friend } from './Friend'
 import { Live } from './Live'
 import { Shake } from './Shake'
 ​
 @Entry
 @Component
 struct NavigationCase {
   @Provide
   stackPath: NavPathStack = new NavPathStack()
 ​
 ​
   @Builder
   getRouterBuilder (name: string) {
    // name是要跳转的标识
     if(name === 'friend') {
       Friend()
     }
     else if(name === "live") {
      Live()
     }
     else if(name === "shake") {
      Shake()
     }
   }
 ​
   build() {
     // 1. 使用Navigation包裹整个根组件
     Navigation(this.stackPath) {
        List() {
          ListItem(){
            Text("朋友圈")
              .fontSize(20)
              .width("100%")
          }
          .height(50)
          .width("100%")
          .padding({
            left: 20
          })
          .onClick(() => {
            this.stackPath.pushPath({
              name: 'friend'
            })
          })
 ​
          ListItem(){
            Text("直播")
              .fontSize(20)
              .width("100%")
          }
          .height(50)
          .width("100%")
          .padding({
            left: 20
          })
          .onClick(() => {
            this.stackPath.pushPath({
              name: 'live'
            })
          })
          ListItem(){
            Text("摇一摇")
              .fontSize(20)
              .width("100%")
          }
          .height(50)
          .width("100%")
          .padding({
            left: 20
          })
          .onClick(() => {
            this.stackPath.pushPath({
              name: 'shake'
            })
          })
        }
        .divider({
          strokeWidth: 2,
          color: "#ccc"
        })
     }
     .navDestination(this.getRouterBuilder)
     .mode(NavigationMode.Auto)
 ​
   }
 }
 import { IParams } from "./NavigationCase"
 ​
 @Component
 export struct NavigationCase_friend {
   @Consume navStackPath: NavPathStack
   @State age: number = 0
   @State name: string = ''
 ​
   build() {
     NavDestination() {
       Text("朋友圈")
       Button("点我返回上一页")
         .onClick(() => {
           this.navStackPath.pop()
         })
 ​
       Text(this.name + this.age)
 ​
     }.onWillAppear(() => {
       const params = this.navStackPath.getParamByName('friend') as IParams[]
       this.age = params[params.length - 1]?.age
       this.name = params[params.length - 1]?.name
       AlertDialog.show({ message: JSON.stringify(params, null, 2) })
     })
   }
 }

第二种方式:WrapBuilder的方式

  • wrapBuilder可以包裹一个全局的builder,不会立刻渲染,需要直接运行的builder方法
  • 子组件需要单独到处全局builder

    WrapBuilder

  • wrapBuilder就是一个全局函数

  • 会返回一个类型为WrapBuilder<[]>, 空数组中是参数的类型

  •  map.get(name).builder()
    
 import { FriendPageBuilder } from "./NavigationCase_friend"
 import { LivePageBuilder } from "./NavigationCase_live"
 import { LookPageBuilder } from "./NavigationCase_look"
 ​
 export const RouterMap: Map<string, WrappedBuilder<[]>> = new Map()
 ​
 RouterMap.set("look", wrapBuilder(LookPageBuilder))
 RouterMap.set("live", wrapBuilder(LivePageBuilder))
 RouterMap.set("friend", wrapBuilder(FriendPageBuilder))
 @Component
 export struct NavigationCase_look {
   @Consume navStackPath: NavPathStack
 ​
   build() {
     NavDestination() {
       Text("看一看")
       Button("点我去到live")
         .onClick(() => {
           this.navStackPath.pushPath({ name: 'live' })
         })
 ​
       Button("返回到根首页")
         .onClick(() => this.navStackPath.clear())
     }
   }
 }
 ​
 @Builder
 export function LookPageBuilder() {
   NavigationCase_look()
 }
 ​
 import { RouterMap } from './NavigationCase_WrapBuilder'
 ​
 export interface IParams {
   name: string
   age: number
 }
 ​
 @Entry
 @Component
 struct NavigationCase {
   @Provide navStackPath: NavPathStack = new NavPathStack()
 ​
   @Builder
   getBuilder(name: string) {
     RouterMap.get(name)?.builder()
   }
 ​
   build() {
     Navigation(this.navStackPath) {
       List({ space: 20 }) {
         ListItem() {
           Text("朋友圈")
             .width("100%")
             .height(60)
             .backgroundColor(Color.Pink)
         }.onClick(() => {
           this.navStackPath.pushPath({ name: 'friend', param: { name: 'aaa', age: 18 } as IParams })
         })
 ​
         ListItem() {
           Text("直播")
             .width("100%")
             .height(60)
             .backgroundColor(Color.Brown)
         }
         .onClick(() => {
           this.navStackPath.pushPath({ name: 'live' })
         })
 ​
         ListItem() {
           Text("看一看")
             .width("100%")
             .height(60)
             .backgroundColor(Color.Orange)
         }
         .onClick(() => {
           this.navStackPath.pushPathByName('look', null)
         })
       }
     }
     .mode(NavigationMode.Auto)
     .navDestination(this.getBuilder)
   }
 }

第三种方式:配置型路由

  • 根组件必须被Navigation包裹
  • 必须绑定NavStackPath
  • 子组件必须被NavDestination包裹
  • 必须有一个全局的builder来渲染子组件
  • 不需要再配置.navDestination
  • 需要在项目的module.json5中配置一个routerMap的json文件【"routerMap": "$profile:router_map"】
  • routerMap文件中配置三个内容-name-builder-文件路径
  • 路由map模式只能模拟器才可以测试
 {
   "routerMap": [
     {
       "name": "main",
       "buildFunction": "MainPageBuilder",
       "pageSourceFile": "src/main/ets/pages/AgainNavigation/MainPage.ets"
     },
     {
       "name": "detail",
       "buildFunction": "PageDetailBuilder",
       "pageSourceFile": "src/main/ets/pages/AgainNavigation/PageDetail.ets"
     }
   ]
 }

Navigation取参数 - 返回 - 回到第一页

  • this.stackPath.getParamByName("当前页的name")
  •  .onWillAppear(() => {
          const params =  this.stackPath.getParamByName("detail") as Params[]
           this.age = params[params.length - 1]?.age
          console.log("onWillAppear")
        })
    
  • 如果没传参数,数组依然有长度,但是里面的内容为undefined
  • 返回主页:this.stackPath.popToIndex(-1)
  • 返回上一页:this.stackPath.pop()
  • // 返回到上一个PageOne页面 // this.pageStack.popToName("PageOne")
  • // 返回到根首页(清除栈中所有页面) // this.pageStack.clear()
  • pushPath(追加)/replacePath(替换), 层级无限制
  • router-32层
  • pushPath({ name: '', param: 123 }), 假设要传对象要明确类型,比如 interface/class/Record/Map
  • 接收参数 this.stackPath.getParamByName("xx") - xx是当前组件的name值
  • 得到的参数类型是一个unknown[],强制要求开发者进行 as 具体类型[]
  • 要取最后一个参数-如果不传参数,数组依然存在,只是内容是undfined

Navigation组件的一些常用属性

.title() // 标题,可以设置自定义builder

.titleMode() // 设置页面标题栏显示模式,常用属性:NavigationTitleMode.Mini

.hideTitleBar(true) // 隐藏标题栏

.mode() // 设置导航栏的显示模式,常用Stack,不分栏显示

.navDestination() // 创建NavDestination组件。使用builder函数,基于name和param构造NavDestination组件。builder下只能有一个根节点。builder中允许在NavDestination组件外包含一层自定义组件, 但自定义组件不允许设置属性和事件,否则仅显示空白。

NavDestination组件的建议

不推荐设置位置、大小等布局相关属性,可能会造成页面显示异常。

router路由跳转

路由模式

路由提供了两种不同的跳转模式,不同模式决定了页面是否会创建多个实例

Standard:多实例模式,也是默认情况下的跳转模式。目标页面被添加到页面栈顶,无论是否存在相同url的页面

Single:单实例模式,如果目标页面的url已经存在于页面栈中,则会将离栈顶最近的同url页面移动到栈顶,该页面成为新建页。如果目标页面的url在页面栈中不存在同url页面,则按照默认的多实例模式进行跳转

简而言之:

Standard:无论之前是否添加过,一直添加到页面栈【常用】【默认】

Single:如果之前加过页面,会使用之前添加的页面【看情况使用】

router.pushUrl(options, mode?)  // router.pushUrl(options, router.RouterMode.Standard)
router.replaceUrl(option, mode?)

页面跳转与后退

  • 普通跳转(可返回)

    router.pushUrl({
    	url: '页面地址'  // page/Index
    })
    
  • 替换跳转(无法返回)

    router.replaceUrl({
        url: "页面地址"
    })
    
  • 返回

    router.back()
    

    router.pushUrl()和router.replaceUrl都可以跳转页面,区别:是否会替换当前页

    • router.pushUrl():目标页面不会替换当前页,而是压入页面栈。这样可以保留当前页的状态,并且可以通过返回键或者调用router.back()方法返回到当前页
    • router.replaceUrl():目标页面会替换当前页,并销毁当前页。这样就可以释放当前页面的资源,并且无法返回到当前页

页面栈

页面栈是用来存储程序运行时页面的一种数据结构,遵循先进后出的原则

router方式跳转路由,页面栈的最大容量为32个页面

router.clear()  // 清空页面栈
router.getLength()  // 范湖页面栈当前的长度
router.getState()  // 获取页面状态
	let page = router.getState()
	console.log("current Index = " + page.index)
	console.log("current name = " + page.name)
	console.log("current path = " + page.path)

参数

  • pushUrl

    router.pushUrl({
        url: "地址",
        params: {
            // 以对象的形式传递参数
        }
    })
    
    router.getParams() as object  // 接收路由参数
    
  • replaceUrl

    router.replaceUrl({
        url: "地址",
        params: {
            // 以对象的形式传递参数
        }
    })
    
  • back

    router.back({
    	url: '地址',
    	params: {
    		// 以对象的形式传递参数
    	}  // back返回时是可以传递参数的,但一般不需要为back指定参数
    })
    

router模式的跨包跳转

Navigation的路由跳转 - 跨包:hap -> hsp / hsp -> hsp

Navigation天然支持跨包跳转

通过路径方式

  • 规则:@bundle:包名/模块名/ets/pages/xx页面 // 可以把前缀封装成一个常量:@bundle:包名
router.pushUrl({
    url: '@bundle:com.itheima.sz.new.content/home/ets/pages/Index'
})

通过name的方式(对比路径方式显得麻烦)

  1. 要跳转的页面修改@Entry ==> @Entry({routerName: 'xxx' })
  2. 添加依赖,oh-package.json里面添加要跳转的hsp的模块
  3. 在你要点击跳转的页面中进行import("模块名/路径文件")
  4. router.pushNamedRoute
import { router } from '@kit.ArkUI';

import("home/src/main/ets/pages/Index")

@Entry
@Component
struct IndexPage {
  build() {
    RelativeContainer() {
      Button("跨包跳转1-路径方式")
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          router.pushUrl({
            url: `@bundle:com.itheima.sz.new.content/home/ets/pages/Index`
          })
          // 规则- @bundle:包名/模块名/ets/pages/xxx页面
        })
        .id("btn1")
      Button("跨包跳转2-name方式")
        .alignRules({
          top: { anchor: 'btn1', align: VerticalAlign.Bottom },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .margin({
          top: 20
        })
        .onClick(() => {
          // router.pushUrl({
          //   url: `@bundle:com.itheima.sz.new.content/home/ets/pages/Index`
          // })
          // 规则- @bundle:包名/模块名/ets/pages/xxx页面
          router.pushNamedRoute({
            name: 'homePage'
          })
        })
    }
    .height('100%')
    .width('100%')
  }
}