跳至主要內容
版本:v8

Angular 導航

本指南涵蓋了在 Ionic 和 Angular 中建置的應用程式中路由的工作原理。

Angular 路由器是 Angular 應用程式中最重要的程式庫之一。如果沒有它,應用程式將會是單一檢視/單一上下文的應用程式,或者無法在瀏覽器重新載入時維持其導航狀態。透過 Angular 路由器,我們可以建立可連結且具有豐富動畫的應用程式(當然,搭配 Ionic 時)。讓我們來看看 Angular 路由器的基本知識,以及我們如何為 Ionic 應用程式設定它。

簡單的路由

對於大多數應用程式而言,通常需要某種類型的路由。最基本的設定看起來有點像這樣


import { RouterModule } from '@angular/router';

@NgModule({
imports: [
...
RouterModule.forRoot([
{ path: '', component: LoginComponent },
{ path: 'detail', component: DetailComponent },
])
],
})

我們在此處的最簡單分解是路徑/組件查詢。當我們的應用程式載入時,路由器會透過讀取使用者嘗試載入的 URL 來啟動。在我們的範例中,我們的路由會尋找 '',這基本上是我們的索引路由。因此,對於此,我們會載入 LoginComponent。相當簡單。對於我們在路由器設定中擁有的每個項目,這種將路徑與組件匹配的模式會持續下去。但是,如果我們想在初始載入時載入不同的路徑呢?

處理重新導向

為此,我們可以使用路由器重新導向。重新導向的工作方式與典型的路由物件相同,但僅包含一些不同的索引鍵。

[
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },
{ path: 'detail', component: DetailComponent },
];

在我們的重新導向中,我們會尋找應用程式的索引路徑。然後,如果我們載入該路徑,我們會重新導向至 login 路由。最後一個索引鍵 pathMatch 是必要的,以告知路由器應該如何查詢路徑。

由於我們使用 full,因此我們告訴路由器應該比較完整路徑,即使最終變成類似 /route1/route2/route3 的路徑也是如此。這表示如果我們有

{ path: '/route1/route2/route3', redirectTo: 'login', pathMatch: 'full' },
{ path: 'login', component: LoginComponent },

並載入 /route1/route2/route3,我們將重新導向。但是,如果我們載入 /route1/route2/route4,我們將不會重新導向,因為路徑不完全相符。

或者,如果我們使用

{ path: '/route1/route2', redirectTo: 'login', pathMatch: 'prefix' },
{ path: 'login', component: LoginComponent },

然後載入 /route1/route2/route3/route1/route2/route4,我們將會為這兩個路由重新導向。這是因為 pathMatch: 'prefix' 將只會匹配路徑的一部分。

談論路由很好,但實際上如何導航至所述路由?為此,我們可以使用 routerLink 指令。讓我們回到先前並採用我們較早的簡單路由器設定

RouterModule.forRoot([
{ path: '', component: LoginComponent },
{ path: 'detail', component: DetailComponent },
]);

現在從 LoginComponent,我們可以使用以下 HTML 導航至詳細資料路由。

<ion-header>
<ion-toolbar>
<ion-title>Login</ion-title>
</ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
<ion-button [routerLink]="['/detail']">Go to detail</ion-button>
</ion-content>

這裡的重要部分是 ion-buttonrouterLink 指令。RouterLink 的運作方式與典型的 href 類似,但它不是將 URL 建置為字串,而是可以建置為陣列,這可以提供更複雜的路徑。

我們也可以透過使用路由器 API,以程式設計方式在我們的應用程式中導航。

import { Component } from '@angular/core';
import { Router } from '@angular/router';

@Component({
...
})
export class LoginComponent {

constructor(private router: Router){}

navigate(){
this.router.navigate(['/detail'])
}
}

這兩種選項都提供相同的導航機制,只是適合不同的使用案例。

Angular 路由器有一個 LocationStrategy.historyGo 方法,可讓開發人員在應用程式歷史記錄中向前或向後移動。讓我們來看一下範例。

假設您有下列應用程式歷史記錄

/pageA --> /pageB --> /pageC

如果您要在 /pageC 上呼叫 LocationStrategy.historyGo(-2),您將會被帶回至 /pageA。如果您接著呼叫 LocationStrategy.historyGo(2),您將會被帶回至 /pageC

LocationStrategy.historyGo() 的一個主要特徵是,它預期您的應用程式歷史記錄會是線性的。這表示 LocationStrategy.historyGo() 不應使用在利用非線性路由的應用程式中。如需更多資訊,請參閱 線性路由與非線性路由

延遲載入路由

現在,我們目前的路由設定方式使其包含在與根 app.module 相同的區塊中,這並不是理想的。相反地,路由器有一個設定,允許組件隔離到自己的區塊。


import { RouterModule } from '@angular/router';

@NgModule({
imports: [
...
RouterModule.forRoot([
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: 'login', loadChildren: () => import('./login/login.module').then(m => m.LoginModule) },
{ path: 'detail', loadChildren: () => import('./detail/detail.module').then(m => m.DetailModule) }
])
],
})

雖然類似,但 loadChildren 屬性是一種透過使用原生匯入而不是直接使用組件來參考模組的方式。不過,為了執行此操作,我們需要為每個組件建立一個模組。

...
import { RouterModule } from '@angular/router';
import { LoginComponent } from './login.component';

@NgModule({
imports: [
...
RouterModule.forChild([
{ path: '', component: LoginComponent },
])
],
})
注意

我們排除了一些其他內容,而僅包含必要的零件。

在這裡,我們有典型的 Angular 模組設定,以及 RouterModule 匯入,但我們現在使用 forChild 並在該設定中宣告組件。透過此設定,當我們執行建置時,我們將會為應用程式組件、登入組件和詳細資料組件產生個別區塊。

獨立組件

獨立組件可讓開發人員在路由上延遲載入組件,而無需將組件宣告為 Angular 模組。

開發人員可以使用 Angular 中的獨立組件路由的現有語法

@NgModule({
imports: [
RouterModule.forRoot([
{
path: 'standalone-route',
loadComponent: () => import('./path/to/my-component.component').then((c) => c.MyComponent),
},
]),
],
})
export class AppRoutingModule {}
提示

如果您正在使用 routerLinkrouterDirectionrouterAction,請務必也為 Ionic 組件匯入 IonRouterLink 指令,或為 <a> 元素匯入 IonRouterLinkWithHref 指令。在Ionic Angular 建置選項文件中提供了此範例。

若要開始使用獨立組件,請造訪 Angular 的官方文件

即時範例

如果您想要親身體驗上述概念和程式碼,請查看我們在 StackBlitz 上關於上述主題的即時範例

線性路由與非線性路由

線性路由

如果您建置了使用路由的網頁應用程式,您可能之前使用過線性路由。線性路由表示您可以透過推送和彈出頁面,在應用程式歷史記錄中向前或向後移動。

以下是行動應用程式中線性路由的範例

此範例中的應用程式歷史記錄具有下列路徑

無障礙功能 --> 旁白 --> 語音

當我們按下返回按鈕時,我們會依照相同的路由路徑,只是方向相反。線性路由很有幫助,因為它允許簡單且可預測的路由行為。這也表示我們可以使用路由器 Angular 路由器 API,例如 LocationStrategy.historyGo()

線性路由的缺點是它不允許複雜的使用者體驗,例如索引標籤檢視。這就是非線性路由的用武之地。

非線性路由

非線性路由對於許多學習使用 Ionic 建置行動應用程式的網頁開發人員來說,可能是一個新的概念。

非線性路由表示使用者應該返回的檢視不一定是畫面上顯示的先前檢視。

以下是非線性路由的一個範例:

在上面的範例中,我們從 Originals 標籤開始。點擊卡片會將我們帶到 Originals 標籤內的 Ted Lasso 視圖。

從這裡,我們切換到 Search 標籤。然後,我們再次點擊 Originals 標籤,並回到 Ted Lasso 視圖。此時,我們已經開始使用非線性路由。

為什麼這是非線性路由呢?我們之前所在的視圖是 Search 視圖。然而,在 Ted Lasso 視圖上按下返回按鈕應該會將我們帶回根 Originals 視圖。這是因為行動應用程式中的每個標籤都被視為自己的堆疊。 使用標籤 章節會更詳細地介紹這一點。

如果點擊返回按鈕只是從 Ted Lasso 視圖呼叫 LocationStrategy.historyGo(-1),我們會被帶回 Search 視圖,這是不正確的。

非線性路由允許複雜的使用者流程,而線性路由無法處理。然而,某些線性路由 API,例如 LocationStrategy.historyGo(),無法在此非線性環境中使用。這表示當使用標籤或巢狀出口時,不應使用 LocationStrategy.historyGo()

我應該選擇哪一個?

我們建議盡可能保持應用程式的簡單性,直到您需要添加非線性路由。非線性路由非常強大,但也會為行動應用程式增加相當多的複雜性。

非線性路由最常見的兩個用途是標籤和巢狀 ion-router-outlet。我們建議僅在您的應用程式符合標籤或巢狀路由器出口的使用案例時才使用非線性路由。

有關標籤的更多資訊,請參閱 使用標籤

有關巢狀路由器出口的更多資訊,請參閱 巢狀路由

共用 URL 與巢狀路由

設定路由時,一個常見的混淆點是在共用 URL 或巢狀路由之間做決定。本指南的這部分將解釋這兩者,並幫助您決定使用哪一個。

共用 URL

共用 URL 是一種路由設定,其中路由具有 URL 中共同的部分。以下是一個共用 URL 設定的範例:

const routes: Routes = [
{
path: 'dashboard',
component: DashboardMainPage,
},
{
path: 'dashboard/stats',
component: DashboardStatsPage,
},
];

上述路由被認為是「共用的」,因為它們重複使用 URL 的 dashboard 部分。

巢狀路由

巢狀路由是一種路由設定,其中路由列為其他路由的子路由。以下是一個巢狀路由設定的範例:

const routes: Routes = [
{
path: 'dashboard',
component: DashboardRouterOutlet,
children: [
{
path: '',
component: DashboardMainPage,
},
{
path: 'stats',
component: DashboardStatsPage,
},
],
},
];

上述路由是巢狀的,因為它們位於父路由的 children 陣列中。請注意,父路由會呈現 DashboardRouterOutlet 元件。當您巢狀路由時,您需要呈現另一個 ion-router-outlet 的實例。

我應該選擇哪一個?

當您想要從頁面 A 過渡到頁面 B,同時保留兩個頁面在 URL 中的關係時,共用 URL 非常有用。在我們之前的範例中,/dashboard 頁面上的按鈕可以過渡到 /dashboard/stats 頁面。由於 a) 頁面過渡和 b) URL,兩個頁面之間的關係得以保留。

當您想要在出口 A 中呈現內容,同時在巢狀出口 B 內呈現子內容時,應使用巢狀路由。您會遇到的最常見的使用案例是標籤。當您載入標籤 Ionic 入門應用程式時,您會看到 ion-tab-barion-tabs 元件呈現在第一個 ion-router-outlet 中。ion-tabs 元件會呈現另一個 ion-router-outlet,它負責呈現每個標籤的內容。

在行動應用程式中,巢狀路由有意義的使用案例非常少。如有疑問,請使用共用 URL 路由設定。我們強烈建議不要在標籤以外的環境中使用巢狀路由,因為它會很快使應用程式的導航變得混亂。

使用標籤

使用標籤時,Angular Router 為 Ionic 提供機制來了解應載入哪些元件,但大部分繁重的工作實際上是由標籤元件完成的。讓我們來看一個簡單的範例。

const routes: Routes = [
{
path: 'tabs',
component: TabsPage,
children: [
{
path: 'tab1',
children: [
{
path: '',
loadChildren: () => import('../tab1/tab1.module').then((m) => m.Tab1PageModule),
},
],
},
{
path: '',
redirectTo: '/tabs/tab1',
pathMatch: 'full',
},
],
},
{
path: '',
redirectTo: '/tabs/tab1',
pathMatch: 'full',
},
];

這裡我們有一個要載入的「標籤」路徑。在此範例中,我們將路徑稱為「標籤」,但路徑的名稱可以變更。它們可以根據您的應用程式進行命名。在該路由物件中,我們也可以定義一個子路由。在此範例中,頂層子路由「tab1」充當我們的「出口」,並且可以載入其他子路由。對於此範例,我們有一個單一的子子路由,它只會載入一個新的元件。標籤的標記如下:

<ion-tabs>
<ion-tab-bar slot="bottom">
<ion-tab-button tab="tab1">
<ion-icon name="flash"></ion-icon>
<ion-label>Tab One</ion-label>
</ion-tab-button>
</ion-tab-bar>
</ion-tabs>

如果您以前使用 Ionic 建置過應用程式,這應該會感覺很熟悉。我們建立一個 ion-tabs 元件,並提供一個 ion-tab-barion-tab-bar 提供一個 ion-tab-button,其中有一個 tab 屬性,該屬性與路由器設定中的標籤「出口」相關聯。請注意,最新版本的 @ionic/angular 不再需要 <ion-tab>,而是允許開發人員完全自訂標籤列,而單一的事實來源存在於路由器設定中。

Ionic 中標籤的工作方式

Ionic 中的每個標籤都被視為單獨的導航堆疊。這表示如果您的應用程式中有三個標籤,則每個標籤都有自己的導航堆疊。在每個堆疊中,您可以向前導航(推入視圖)和向後導航(彈出視圖)。

此行為很重要,因為它與其他基於 Web 的 UI 程式庫中找到的大多數標籤實作不同。其他程式庫通常將標籤作為一個單一的歷史堆疊來管理。

由於 Ionic 專注於協助開發人員建置行動應用程式,因此 Ionic 中的標籤旨在盡可能與原生行動標籤匹配。因此,Ionic 標籤中可能存在某些行為與您在其他 UI 程式庫中看到的標籤實作有所不同。請繼續閱讀以了解更多有關這些差異的資訊。

標籤內的子路由

在向標籤新增其他路由時,您應該將它們寫為與父標籤具有路徑前綴的同級路由。下面的範例將 /tabs/tab1/view 路由定義為 /tabs/tab1 路由的同級路由。由於此新路由具有 tab1 前綴,它將呈現在 Tabs 元件內部,而標籤 1 仍將在 ion-tab-bar 中選取。

const routes: Routes = [
{
path: 'tabs',
component: TabsPage,
children: [
{
path: 'tab1',
children: [
{
path: '',
loadChildren: () => import('../tab1/tab1.module').then((m) => m.Tab1PageModule),
},
],
},
{
path: 'tab1/view',
children: [
{
path: '',
loadChildren: () => import('../tab1/tab1view.module').then((m) => m.Tab1ViewPageModule),
},
],
},
{
path: 'tab2',
children: [
{
path: '',
loadChildren: () => import('../tab2/tab2.module').then((m) => m.Tab2PageModule),
},
],
},
{
path: 'tab3',
children: [
{
path: '',
loadChildren: () => import('../tab3/tab3.module').then((m) => m.Tab3PageModule),
},
],
},
],
},
{
path: '',
redirectTo: '/tabs/tab1',
pathMatch: 'full',
},
];

在標籤之間切換

由於每個標籤都是自己的導航堆疊,因此務必注意這些導航堆疊絕不應互動。這表示標籤 1 中永遠不應有一個將使用者路由到標籤 2 的按鈕。換句話說,只能透過使用者點擊標籤列中的標籤按鈕來變更標籤。

在實務中,iOS App Store 和 Google Play Store 行動應用程式就是一個很好的例子。這些應用程式都提供標籤式介面,但它們從不跨標籤路由使用者。例如,iOS App Store 應用程式中的「遊戲」標籤永遠不會將使用者導向「搜尋」標籤,反之亦然。

讓我們來看看使用標籤時會犯的一些常見錯誤。

多個標籤引用的設定標籤

一個常見的做法是將設定視圖建立為自己的標籤。如果開發人員需要呈現數個巢狀設定選單,這非常有用。但是,其他標籤永遠不應嘗試路由到設定標籤。如上所述,啟用設定標籤的唯一方式是使用者點擊適當的標籤按鈕。

如果您發現標籤需要引用設定標籤,我們建議使用 ion-modal 將設定視圖設為強制回應視窗。這是 iOS App Store 應用程式中的做法。使用這種方法,任何標籤都可以呈現強制回應視窗,而不會破壞每個標籤都是自己的堆疊的行動標籤模式。

下面的範例顯示 iOS App Store 應用程式如何處理從多個標籤呈現「帳戶」視圖。透過在強制回應視窗中呈現「帳戶」視圖,應用程式可以在行動標籤最佳實務中運作,以在多個標籤中顯示相同的視圖。

在標籤之間重複使用視圖

另一個常見的做法是在多個標籤中呈現相同的視圖。開發人員經常嘗試透過讓視圖包含在單一標籤中,並讓其他標籤路由到該標籤來執行此操作。如上所述,這會破壞行動標籤模式,應避免使用。

相反地,我們建議在每個標籤中都有引用相同元件的路由。這是 Spotify 等熱門應用程式中採用的做法。例如,您可以從「首頁」、「搜尋」和「您的音樂庫」標籤存取專輯或 Podcast。存取專輯或 Podcast 時,使用者會停留在該標籤內。應用程式會透過為每個標籤建立路由並在程式碼庫中共用通用元件來執行此操作。

下面的範例顯示 Spotify 應用程式如何重複使用相同的專輯元件來在多個標籤中顯示內容。請注意,每個螢幕截圖都顯示相同的專輯,但來自不同的標籤。

首頁標籤搜尋標籤