鴻蒙開發:一文探究Navigation路由組件

前言

如果你還在使用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) => {
       //做一些邏輯處理,比如重定向等等
      }
    })
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容