前言
如果你還在使用router做為頁面跳轉,建議切換Navigation組件作為應用路由框架,不為別的,因為官方目前針對router已不在推薦。
需要說明的是,Navigation它是一個組件,并不是像router那樣可調用的方法,一般作為首頁的根容器使用。
簡單使用
簡單實現一個小案例,從頁面A跳轉到頁面B。按照官方案例,大致三步即可。
第一步,使用Navigation替換主入口頁面,并設置NavPathStack,因為要使用NavPathStack執行跳轉的邏輯。
@Entry
@Component
struct Index {
pageStack: NavPathStack = new NavPathStack()
build() {
Navigation(this.pageStack) {
RelativeContainer() {
Button("點擊")
.onClick(() => {
this.pageStack.pushPath({ name: "TestPage" })
})
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
}
.height('100%')
.width('100%')
}
}
}
第二步,跳轉目標頁面使用NavDestination做為根布局,并且聲明一個跳轉頁面入口函數。
// 跳轉頁面入口函數
@Builder
export function TestPageBuilder() {
TestPage()
}
@Component
struct TestPage {
@State message: string = 'Hello TestPage';
build() {
NavDestination() {
RelativeContainer() {
Text(this.message)
.id('TestPageHelloWorld')
.fontSize(50)
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
}
.height('100%')
.width('100%')
}
}
}
第三步,添加路由表配置,在跳轉目標模塊的配置文件module.json5進行添加:
{
"module" : {
"routerMap": "$profile:route_map"
}
}
route_map.json文件配置如下:
{
"routerMap": [
{
"name": "TestPage",
"pageSourceFile": "src/main/ets/pages/TestPage.ets",
"buildFunction": "TestPageBuilder"
}
]
}
以上三步執行完,我們就簡單的實現了頁面的跳轉,當然了,Navigation作為路由容器,有著自身的生命周期還有很多可用的屬性或方法,我們挑其中常見的舉例說一下。
例一個簡單的大綱,方便我們直觀的了解:
1、了解Navigation的生命周期。
2、Navigation常見屬性方法。
3、NavPathStack常見屬性方法。
4、如何使用代碼動態配置路由表。
5、常見使用方法
一、了解Navigation的生命周期
大家可以驗證一個問題,當使用了Navigation組件之后,會有一個常見的問題,那就是,子頁面的onPageShow,onPageHide聲明周期是不會走的。
aboutToAppear(): void {
console.log("===聲明周期:aboutToAppear")
}
onPageShow(): void {
console.log("===聲明周期:onPageShow")
}
onPageHide(): void {
console.log("===聲明周期:onPageHide")
}
當跳轉到目標頁面時,控制臺只會打印aboutToAppear:
其主要原因就是,Navigation作為一個路由容器,生命周期會承載在NavDestination組件上,以組件事件的形式進行開放。
如何得知頁面的顯示或隱藏,或者切換了前臺和后臺,需要通過NavDestination的生命周期方法進行獲取:
onWillShow:NavDestination組件布局顯示之前執行,此時頁面不可見(應用切換到前臺不會觸發)。
onShown:NavDestination組件布局顯示之后執行,此時頁面已完成布局。
onWillHide:NavDestination組件觸發隱藏之前執行(應用切換到后臺不會觸發)。
onHidden:NavDestination組件觸發隱藏后執行(非棧頂頁面push進棧,棧頂頁面pop出棧或應用切換到后臺)
二、Navigation常見屬性方法
需要說明的是,Navigation雖然提供了很多方法,而在實際的開發中,用到的確屈指可數,因為一般情況下,什么導航欄,標題欄,我們都是不需要的,畢竟系統的是不符合我們UI設計的,只需要針對性隱藏即可。
比如提供的標題欄如下所示,能和實際中的UI匹配度,可以說是很低的,當然了,如果你們的設計是類似的,那么完全可以使用系統的。
1、title,設置頁面標題
也就是上面圖中的主標題,直接設置如下:
.title("我是一個標題")
由于subTitle副標題已經過時了,官方替代方案是使用title來代替:
.title(this.NavigationTitle)
通過@Builder自定義布局。
@Builder NavigationTitle() {
Column() {
Text('Title')
.fontColor('#182431')
.fontSize(30)
.lineHeight(41)
.fontWeight(700)
Text('subtitle')
.fontColor('#182431')
.fontSize(14)
.lineHeight(19)
.opacity(0.4)
.margin({ top: 2, bottom: 20 })
}.alignItems(HorizontalAlign.Start)
}
效果:
2、menus,設置頁面右上角菜單
.menus(this.NavigationMenus)
通過@Builder自定義布局。
@Builder
NavigationMenus() {
Row() {
Image($r("app.media.app_icon"))
.width(24)
.height(24)
Image($r("app.media.app_icon"))
.width(24)
.height(24)
.margin({ left: 24 })
}
}
效果:
3、titleMode,設置頁面標題欄顯示模式
目前官方提供了有三種模式,Full:固定為大標題模式,Mini:固定為小標題模式,Free:當內容為滿一屏的可滾動組件時,標題隨著內容向上滾動而縮小(子標題的大小不變、淡出)。向下滾動內容到頂時則恢復原樣。
4、backButtonIcon,設置標題欄中返回鍵圖標
.backButtonIcon(new SymbolGlyphModifier($r('app.media.app_icon')))
5、mode,導航欄的顯示模式
設置導航欄的顯示模式。支持Stack、Split與Auto模式。
當然了,還有很多的屬性,如果使用系統提供的標題欄的話,盡量去官方多熟悉熟悉,在實際的開發中,其實這些都是不需要的,直接隱藏即可。
當我們什么也不設置的時候,會發現,內容區域是無法覆蓋完整的,這是由于標題欄導致的,我們只需要隱藏即可。
未隱藏前:
隱藏后:
.hideTitleBar(true)
當然了,NavDestination中也有hideTitleBar屬性,如果采用自己的UI標題欄,也需要設置為true。
三、NavPathStack常見屬性方法
NavPathStack是Navigation路由棧,用于管理路由,比如跳轉,移除等等,非常的重要,針對常見的幾個屬性,我們簡單的說一下。
1、pushPath
頁面跳轉,將info指定的NavDestination頁面信息入棧。
this.pageStack.pushPath({ name: "TestPage" })
兩個參數,一個是NavPathInfo,一個參數是NavigationOptions,這是api12+的版本,如果是以下的版本,第二個參數是boolean類型,意思是是否支持轉場動畫。
NavPathInfo對象,name指的是NavDestination頁面名稱,param是傳遞的參數,onPop是NavDestination頁面觸發pop時返回的回調。
比如傳遞參數
this.pageStack.pushPath({ name: "TestPage",param:"我是一個參數"})
第二個參數NavigationOptions,可以設置頁面棧的操作模式和轉場動畫,比如設置從棧底向棧頂查找,支持轉場動畫:
this.pageStack.pushPath({ name: "TestPage",param:"我是一個參數"},{
launchMode:LaunchMode.POP_TO_SINGLETON,
animated:true
})
2、pushPathByName
將name指定的NavDestination頁面信息入棧。
this.pageStack.pushPathByName("TestPage","我是一個參數")
三個參數,第一個指的是NavDestination頁面名稱,第二個是傳遞的參數,最后一個是boolean類型,是否支持轉場動畫。
3、pushDestination
和pushPath一致,將info指定的NavDestination頁面信息入棧,使用Promise異步回調返回接口調用結果。
4、pushDestinationByName
和pushPathByName一致,將name指定的NavDestination頁面信息入棧,使用Promise異步回調返回接口調用結果。
5、replacePath
和pushPath的參數使用一致,主要是用于替換頁面棧操作。
6、replacePathByName
和pushPathByName的參數一致,將當前頁面棧棧頂退出,將name指定的頁面入棧。
this.pageStack.replacePathByName("TestPage","我是一個參數",true)
7、pop
彈出路由棧棧頂元素,也就是銷毀當前頁面,并觸發onPop回調傳入頁面處理結果。
this.pageStack.pop("返回結果")
8、popToName
回退路由棧到由棧底開始第一個名為name的NavDestination頁面,和pop,使用方式一致,不過第一個參數為NavDestination頁面名稱。
9、popToIndex
回退路由棧到index指定的NavDestination頁面,和pop,使用方式一致,不過第一個參數為頁面棧的索引。
10、moveToTop
將由棧底開始第一個名為name的NavDestination頁面移到棧頂。
11、moveIndexToTop
將index指定的NavDestination頁面移到棧頂。
12、clear
清除棧中所有頁面。
四、如何使用代碼動態配置路由表
開篇我們是用的靜態配置的路由實現的跳轉,每個頁面都需要在路由json文件里進行配置,十分的不便,針對此問題,官方也給我們提供了自定義路由表的方式來實現跨包動態路由。
按照官方解讀,具體的實現方案如下:
1、定義頁面跳轉配置項。
使用資源文件進行定義,通過資源管理@ohos.resourceManager在運行時對資源文件解析。
在ets文件中配置路由加載配置項,一般包括路由頁面名稱(即pushPath等接口中頁面的別名),文件所在模塊名稱(hsp/har的模塊名),加載頁面在模塊內的路徑(相對src目錄的路徑)。
2、加載目標跳轉頁面,通過動態import將跳轉目標頁面所在的模塊在運行時加載, 在模塊加載完成后,調用模塊中的方法,通過import在模塊的方法中加載模塊中顯示的目標頁面,并返回頁面加載完成后定義的Builder函數。
3、觸發頁面跳轉,在Navigation的navDestination屬性執行步驟2中加載的Builder函數,即可跳轉到目標頁面。
我們簡單實現一個小案例:
第一步,創建靜態路由模塊,此模塊,用于存放路由相關的工具類,配置路由加載配置項,并且需要被使用到的模塊進行依賴,也就是,牽扯到路由跳轉的所有模塊都需要依賴此模塊。
路由工具類:
export class RouterModule {
static builderMap: Map<string, WrappedBuilder<[object]>> = new Map<string, WrappedBuilder<[object]>>();
static routerMap: Map<string, NavPathStack> = new Map<string, NavPathStack>();
// Registering a builder by name.
public static registerBuilder(builderName: string, builder: WrappedBuilder<[object]>): void {
RouterModule.builderMap.set(builderName, builder);
}
// Get builder by name.
public static getBuilder(builderName: string): WrappedBuilder<[object]> {
const builder = RouterModule.builderMap.get(builderName);
return builder as WrappedBuilder<[object]>;
}
// Registering a router by name.
public static createRouter(routerName: string, router: NavPathStack): void {
RouterModule.routerMap.set(routerName, router);
}
// Get router by name.
public static getRouter(routerName: string): NavPathStack {
return RouterModule.routerMap.get(routerName) as NavPathStack;
}
// Jumping to a Specified Page by Obtaining the Page Stack.
public static async push(router: RouterModel): Promise<void> {
const harName = router.builderName.split('_')[0];
await import(harName).then((ns: ESObject): Promise<void> => ns.harInit(router.builderName))
RouterModule.getRouter(router.routerName).pushPath({ name: router.builderName, param: router.param });
}
// Obtain the page stack and pop it.
public static pop(routerName: string): void {
// Find the corresponding route stack for pop.
RouterModule.getRouter(routerName).pop();
}
// Get the page stack and clear it.
public static clear(routerName: string): void {
// Find the corresponding route stack for pop.
RouterModule.getRouter(routerName).clear();
}
// Directly jump to the specified route.
public static popToName(routerName: string, builderName: string): void {
RouterModule.getRouter(routerName).popToName(builderName);
}
}
路由靜態變量
export class BuilderNameConstants {
static readonly Test: string = 'Test';
}
// Indicates the key of the routerMap table in the RouterModule.
export class RouterNameConstants {
static readonly ENTRY_HAP: string = 'EntryHap_Router';
}
路由模型對象
export class RouterModel {
// Route page alias, in the form:${bundleName}_${pageName}.
builderName: string = "";
// Routing Stack Name.
routerName: string = "";
// Parameters that need to be transferred to the page.
param?: object = new Object();
}
第二步,主頁面配置
@Entry
@Component
struct Index {
private pageStack: NavPathStack = new NavPathStack()
aboutToAppear() {
RouterModule.createRouter(RouterNameConstants.ENTRY_HAP, this.pageStack);
}
@Builder
routerMap(builderName: string, param: object) {
RouterModule.getBuilder(builderName).builder(param);
}
build() {
Navigation(this.pageStack) {
RelativeContainer() {
Button("點擊")
.onClick(() => {
RouterModule.getRouter(RouterNameConstants.ENTRY_HAP)
.pushPath({ name: BuilderNameConstants.Test })
})
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
}
.width('100%')
.height('100%')
.backgroundColor(Color.Pink)
}.width('100%')
.height('100%')
.hideTitleBar(true)
.navDestination(this.routerMap);
}
}
第三步,子頁面配置
@Component
struct TestPage {
build() {
Column() {
Text("子頁面")
}
.width('100%')
.height('100%')
}
}
@Builder
export function TestBuilder(value: object) {
NavDestination() {
TestPage()
}
.hideTitleBar(true)
}
const builderName = BuilderNameConstants.Test;
if (!RouterModule.getBuilder(builderName)) {
const builder: WrappedBuilder<[object]> = wrapBuilder(TestBuilder);
RouterModule.registerBuilder(builderName, builder);
}
第四步,初始化,動態導包,可以在Ability里或者AbilityStage里進行初始化。
export function importPages() {
import('../pages/TestPage')
}
以上四步,我們就實現了一個動態路由配置,是不是也是有點復雜,畢竟,動態導包是需要手動配置的,下篇文章,我們就把以上的程序進行簡單化。
五、常見使用問題匯總
1、子頁面如何拿到NavPathStack
所有的動作都是通過NavPathStack對象進行執行的,主頁面我們聲明了Navigation,傳遞了NavPathStack,那么子頁面是沒有NavPathStack對象的,如何進行跳轉操作呢?很簡單,只需要傳遞到子頁面即可。
這里用到了@Provide裝飾器和@Consume裝飾器,也就是與后代組件雙向同步。
在主頁面聲明的時候
@Provide('pageStack') pageStack: NavPathStack = new NavPathStack()
子頁面接收
@Consume('pageStack') pageStack: NavPathStack;
2、頁面如何拿到傳遞的參數
根據頁面Name來獲取傳遞的數據。
this.pageStack.getParamByName("TestPage")
除此之外,還可以根據索引
this.pageStack.getParamByIndex(1)
3、頁面如何接收返回的參數
比如返回,我們隨便傳遞一個數據:
this.pageStack.pop("返回一個數據")
接收返回的數據
this.pageStack.pushPath({
name: "TestPage", onPop: (info) => {
//接收返回的數據
console.log("==========" + info.result)
}
})
4、如何對路由進行攔截
this.pageStack.setInterception({
willShow: (from: NavDestinationContext | "navBar", to: NavDestinationContext | "navBar",
operation: NavigationOperation, animated: boolean) => {
//做一些邏輯處理,比如重定向等等
}
})