跳到主要内容
版本: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-button` 和 `routerLink` 指令。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 {}
提示

如果你使用的是 `routerLink`、`routerDirection` 或 `routerAction`,请确保还为 Ionic 组件导入 `IonRouterLink` 指令,或为 `<a>` 元素导入 `IonRouterLinkWithHref` 指令。在 Ionic Angular 构建选项文档 中可以找到一个示例。

要开始使用独立组件,请 访问 Angular 的官方文档

实时示例

如果你想亲身体验上述概念和代码,请查看我们关于上述主题的 实时示例,它位于 StackBlitz 上。

线性路由与非线性路由

线性路由

如果你已经构建了一个使用路由的 Web 应用程序,你可能以前使用过线性路由。线性路由意味着你可以通过推送和弹出页面来向前或向后移动应用程序历史记录。

以下是移动应用程序中线性路由的一个示例

此示例中的应用程序历史记录具有以下路径

无障碍功能 --> VoiceOver --> 语音

当我们按下后退按钮时,我们会遵循相同的路由路径,只是方向相反。线性路由的优势在于它可以让路由行为变得简单且可预测。这也意味着我们可以使用 Angular Router API,例如 LocationStrategy.historyGo()

线性路由的缺点是它不允许复杂的用户体验,例如标签视图。这就是非线性路由的用武之地。

非线性路由

非线性路由是一个概念,对许多学习使用 Ionic 构建移动应用程序的 Web 开发人员来说可能很陌生。

非线性路由意味着用户应该返回的视图不一定是屏幕上显示的先前视图。

以下是非线性路由的示例

在上面的示例中,我们从 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)中采用的做法。例如,您可以从“主页”、“搜索”和“您的资料库”标签中访问专辑或播客。当访问专辑或播客时,用户将停留在该标签中。该应用程序通过为每个标签创建路由并在代码库中共享一个通用组件来实现这一点。

下面的示例显示了 Spotify 应用程序如何重用相同的专辑组件以在多个标签中显示内容。请注意,每个屏幕截图都显示了相同的专辑,但来自不同的标签。

主页标签搜索标签