到目前为止,我们的小Angular应用程序中只有一个视图。当然,我们会希望有更多的选择,让用户访问不同的视图,获得不同的信息和体验。就像我们在基本的网页上有链接一样,我们也可以在Angular应用程序中设置链接来导航到各种视图。正是通过Angular中提供的路由功能,我们可以为各种信息集设置各种路由。在本教程中,我们将学习路由是如何工作的,如何配置路由,如何将路由链接到动作,以及如何建立我们需要的各种视图。
创建一个新的组件
现在,有一个简单的游戏列表组件,显示应用程序中所有游戏的列表。现在,我们想建立一个新的组件,一次显示一个游戏的细节。我们希望能够点击一个链接,让Angular路由工作,这样我们就会被送到该游戏的详细视图。换句话说,我们需要一个新的组件,以便我们能够路由到该组件。下面是游戏细节组件的样子。
游戏细节组件由game-detail.css、game-detail.component.html和game-detail.component.ts组成。它们住在 **games**文件夹中。这里是这些文件。
game-detail.css
.col-md-4 {
display: flex;
align-items: center;
justify-content: center;
}
game-detail.component.html
<div class='card'
*ngIf='game'>
<div class='card-header'>
{{pageTitle + ': ' + game.gameName}}
</div>
<div class='card-body'>
<div class='row'>
<div class='col-md-4'>
<img class='center-block img-responsive'
[style.width.px]='200'
[src]='game.imageUrl'
[title]='game.gameName'>
</div>
<div class='col-md-8'>
<h5 class="card-title">{{game.gameName}}</h5>
<p class="card-text">{{game.description}}</p>
<ul class="list-group">
<li class="list-group-item"><b>Part#:</b> {{game.gameCode | lowercase | convertToColon: '-'}}</li>
<li class="list-group-item"><b>Released:</b> {{game.releaseDate}}</li>
<li class="list-group-item"><b>Cost:</b> {{game.price|currency:'USD':'symbol'}}</li>
<li class="list-group-item"><b>Rating:</b> <game-thumb
[rating]='game.thumbRating'></game-thumb>
</li>
</ul>
</div>
</div>
</div>
<div class='card-footer'>
<button class='btn btn-outline-secondary' (click)='onBack()'>
<i class='fa fa-chevron-left'></i> Back
</button>
</div>
</div>
game-detail.component.ts
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {IGame} from './game';
import {GameService} from './game.service';
@Component({
templateUrl: './game-detail.component.html',
styleUrls: ['./game-detail.component.css']
})
export class GameDetailComponent implements OnInit {
pageTitle = 'Game Detail';
errorMessage = '';
game: IGame | undefined;
constructor(private route: ActivatedRoute,
private router: Router,
private gameService: GameService) {
}
ngOnInit() {
const param = this.route.snapshot.paramMap.get('id');
if (param) {
const id = +param;
this.getGame(id);
}
}
getGame(id: number) {
this.gameService.getGame(id).subscribe(
game => this.game = game,
error => this.errorMessage = <any>error);
}
onBack(): void {
this.router.navigate(['/games']);
}
}
Angular路由如何工作
在Angular应用程序中,所有视图都显示在一个页面中。这就是所谓的单页应用程序,或SPA。显示所有视图的页面通常是index.html文件。我们的文件看起来像这样。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Classic Games</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<game-root></game-root>
</body>
</html>
任何时候都可以在索引页上显示任何数量的页面或视图。你可能有三个视图,或者你可能有数百个。它们中的任何一个都可以被设置为显示在index.html文件中。这是如何发生的呢?这是用Angular中的Routing实现的。要在Angular中设置路由,你可以采取以下步骤。
- 为每个组件配置一个路由
- 定义用户的选项或行动
- 将一个路由链接到一个选项或行动
- 根据用户的动作(点击)来触发路由
- 当一个路由被激活时,显示组件的视图
在下面的截图中,是一个向应用程序的用户提供选项的样本菜单。一个路由与菜单选项(链接)相联系,这样Angular就可以路由到某个组件。这是通过内置的路由器指令完成的 routerLink.当用户点击游戏选项时,Angular路由器会导航到/games路由。这就更新了浏览器的URL并将所需的组件加载到视图中。
因此,当用户在地址栏中访问http://localhost:4200/games,他们会看到游戏列表。
配置路由
让我们看看如何在Angular中实际配置一些路由。路由是基于组件的,因此工作流程是首先确定你想把哪个(些)组件作为路由目标。然后,你可以根据需要定义路由。首先,我们需要一个HomepageComponent,因为它现在将被默认显示,而不是游戏列表。这个组件住在一个 home文件夹,由homepage.component.html和homepage.component.ts组成。
homepage.component.html
<div class="card">
<div class="card-header">
{{pageTitle}}
</div>
<div class="card-body">
<div class="container-fluid">
<div class="text-center">
<img src="./assets/images/classic-games.png" class="img-responsive center-block"/>
</div>
</div>
</div>
<div class="card-footer text-muted text-right">
<small>Powered by Angular</small>
</div>
</div>
homepage.component.ts
import { Component } from '@angular/core';
@Component({
templateUrl: './homepage.component.html'
})
export class HomepageComponent {
public pageTitle = 'Homepage';
}
在app.module.ts文件中,我们现在可以指定这个组件为默认路径,可以说是用 RouterModule.forRoot().
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {HttpClientModule} from '@angular/common/http';
import {RouterModule} from '@angular/router';
import {AppComponent} from './app.component';
import {HomepageComponent} from './home/homepage.component';
import {GameModule} from './games/game.module';
@NgModule({
declarations: [
AppComponent,
HomepageComponent
],
imports: [
BrowserModule,
HttpClientModule,
RouterModule.forRoot([
{path: 'home', component: HomepageComponent},
{path: '', redirectTo: 'home', pathMatch: 'full'},
{path: '**', redirectTo: 'home', pathMatch: 'full'}
]),
GameModule
],
bootstrap: [AppComponent]
})
export class AppModule {
}
正是在app.component.ts文件中,我们可以为用户设置routerLink选项。事实上,我们将使用 routerLinkActive指令,这样当用户导航到一个特定的路线时,该按钮或链接就会被高亮显示。还要注意使用特殊的router-outlet元素,这是一个占位符,Angular会根据当前的路由状态动态地填充。换句话说,每当一个路由被激活时,与该路由相关的视图就会被加载到标签中进行显示。
import {Component} from '@angular/core';
@Component({
selector: 'game-root',
template: `
<div class="container">
<nav class='navbar navbar-expand-lg'>
<ul class='nav nav-pills mr-auto'>
<li><a class='nav-link' routerLinkActive='active' [routerLink]="['/home']">Home</a></li>
<li><a class='nav-link' routerLinkActive='active' [routerLink]="['/games']">Games</a></li>
</ul>
<span class="navbar-text">{{pageTitle}}</span>
</nav>
<div class='container'>
<router-outlet></router-outlet>
</div>
</div>`,
styleUrls: ['./app.component.css']
})
export class AppComponent {
pageTitle = 'Angular Games Viewer';
}
向路由传递参数
为了路由到一个集合的特定项目,你可以向路由传递参数。例如,我们应用程序中的所有游戏都有一个与之相关的ID。为了路由到一个特定的游戏,我们需要以某种方式在路由中指定游戏的ID。例如,访问http://localhost:4200/games/5 路由显示一个特定的游戏。
game-list.component.html
在我们的游戏列表组件中,我们列出了所有的游戏,我们可以设置路由参数传递,如下面的高亮代码所示。
<div class='card'>
<div class='card-header'>
{{pageTitle}}
</div>
<div class='card-body'>
<div class="row">
<div class="col-3">
<input type="text" [(ngModel)]='listFilter' class="form-control" id="filterInput"
placeholder="Type to filter">
</div>
<div class="col">
<div *ngIf='listFilter' class="form-text text-muted">Filtered by: {{listFilter}}</div>
</div>
</div>
<div class='table-responsive'>
<table class='table'
*ngIf='games && games.length'>
<thead>
<tr>
<th>
<button class='btn btn-primary'
(click)='toggleImage()'>
{{showImage ? 'Hide ' : 'Show'}} Image
</button>
</th>
<th>Game</th>
<th>Part#</th>
<th>Release Date</th>
<th>Cost</th>
<th>5 Thumb Rating</th>
</tr>
</thead>
<tbody>
<tr *ngFor='let game of filteredGames'>
<td>
<img *ngIf='showImage'
[src]='game.imageUrl'
[title]='game.gameName'
[style.width.px]='imageWidth'
[style.margin.px]='imageMargin'>
</td>
<td>
<a [routerLink]="['/games', game.gameId]">
{{ game.gameName }}
</a>
</td>
<td>{{ game.gameCode | lowercase | convertToColon: '-' }}</td>
<td>{{ game.releaseDate }}</td>
<td>{{ game.price | currency:'USD':'symbol':'1.2-2' }}</td>
<td>
<game-thumb [rating]='game.thumbRating'
(ratingClicked)='onRatingClicked($event)'>
</game-thumb>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="card-footer text-muted text-right">
<small>Powered by Angular</small>
</div>
</div>
<div *ngIf='errorMessage'
class='alert alert-danger'>
Error: {{ errorMessage }}
</div>
用参数定义路由
好了,上面指出的链接已经到位,使用的链接格式为<a [routerLink]="['/games', game.gameId]">。请注意,路由是/games,在逗号之后,我们传递了我们想要查看的游戏的ID。为了使其发挥作用,需要在应用程序的某个地方定义路由。对于这些链接,我们可以在 **game.module.ts文件中使用RouterModule.forChild()**定义路由。
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {GameListComponent} from './game-list.component';
import {GameDetailComponent} from './game-detail.component';
import {ConvertToColonPipe} from '../shared/convert-to-colon.pipe';
import {GameDetailGuard} from './game-detail.guard';
import {SharedModule} from '../shared/shared.module';
@NgModule({
imports: [
RouterModule.forChild([
{path: 'games', component: GameListComponent},
{
path: 'games/:id',
canActivate: [GameDetailGuard],
component: GameDetailComponent
},
]),
SharedModule
],
declarations: [
GameListComponent,
GameDetailComponent,
ConvertToColonPipe
]
})
export class GameModule {
}
game-detail.component.ts文件也必须注意到路由。为了显示正确的游戏,游戏细节组件从URL中读取参数。然后,该参数被用来从我们在早期教程中设置的后端API中获取正确的游戏。为了从URL中获取参数,我们使用了ActivatedRoute服务。这在构造函数中被设置为一个依赖项。
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {IGame} from './game';
import {GameService} from './game.service';
@Component({
templateUrl: './game-detail.component.html',
styleUrls: ['./game-detail.component.css']
})
export class GameDetailComponent implements OnInit {
pageTitle = 'Game Detail';
errorMessage = '';
game: IGame | undefined;
constructor(private route: ActivatedRoute,
private router: Router,
private gameService: GameService) {
}
ngOnInit() {
const param = this.route.snapshot.paramMap.get('id');
if (param) {
const id = +param;
this.getGame(id);
}
}
getGame(id: number) {
this.gameService.getGame(id).subscribe(
game => this.game = game,
error => this.errorMessage = <any>error);
}
onBack(): void {
this.router.navigate(['/games']);
}
}
在浏览器中测试游戏的细节路线,效果相当好
用代码激活路由
你可能已经注意到了返回按钮,它将我们带回游戏列表组件视图。这个路由是用一个专门的函数激活的,由Router服务提供。在这里,我们让game-detail.component.ts文件为这种类型的路由做好准备。
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {IGame} from './game';
import {GameService} from './game.service';
@Component({
templateUrl: './game-detail.component.html',
styleUrls: ['./game-detail.component.css']
})
export class GameDetailComponent implements OnInit {
pageTitle = 'Game Detail';
errorMessage = '';
game: IGame | undefined;
constructor(private route: ActivatedRoute,
private router: Router,
private gameService: GameService) {
}
ngOnInit() {
const param = this.route.snapshot.paramMap.get('id');
if (param) {
const id = +param;
this.getGame(id);
}
}
getGame(id: number) {
this.gameService.getGame(id).subscribe(
game => this.game = game,
error => this.errorMessage = <any>error);
}
onBack(): void {
this.router.navigate(['/games']);
}
}
现在在game-detail.component.html文件中,我们可以简单地调用 **onBack()**当用户点击时,触发路由导航代码。
<div class='card'
*ngIf='game'>
<div class='card-header'>
{{pageTitle + ': ' + game.gameName}}
</div>
<div class='card-body'>
<div class='row'>
<div class='col-md-4'>
<img class='center-block img-responsive'
[style.width.px]='200'
[src]='game.imageUrl'
[title]='game.gameName'>
</div>
<div class='col-md-8'>
<h5 class="card-title">{{game.gameName}}</h5>
<p class="card-text">{{game.description}}</p>
<ul class="list-group">
<li class="list-group-item"><b>Part#:</b> {{game.gameCode | lowercase | convertToColon: '-'}}</li>
<li class="list-group-item"><b>Released:</b> {{game.releaseDate}}</li>
<li class="list-group-item"><b>Cost:</b> {{game.price|currency:'USD':'symbol'}}</li>
<li class="list-group-item"><b>Rating:</b> <game-thumb
[rating]='game.thumbRating'></game-thumb>
</li>
</ul>
</div>
</div>
</div>
<div class='card-footer'>
<button class='btn btn-outline-secondary' (click)='onBack()'>
<i class='fa fa-chevron-left'></i> Back
</button>
</div>
</div>
用守卫来保护一个路由
应用程序的不同用户可能有不同级别的权限。也许只有注册用户才能访问一个特定的路由。在这样的情况下,你可以使用angular中的guards来保护一个给定的路由。angular路由器为你提供了这些护卫。它们包括以下选项。
- CanActivate- 保护导航到一个路由
- CanDeactivate- 保护从一个路由的导航
- Resolve- 在路由激活前预取数据
- CanLoad- 防止异步路由的发生
对于我们的目的,我们可以建立一个防护,只有在URL中提供了有效的路由参数时,才允许用户查看游戏的细节组件。我们可以像这样从game.module.ts开始。
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {GameListComponent} from './game-list.component';
import {GameDetailComponent} from './game-detail.component';
import {ConvertToColonPipe} from '../shared/convert-to-colon.pipe';
import {GameDetailGuard} from './game-detail.guard';
import {SharedModule} from '../shared/shared.module';
@NgModule({
imports: [
RouterModule.forChild([
{path: 'games', component: GameListComponent},
{
path: 'games/:id',
canActivate: [GameDetailGuard],
component: GameDetailComponent
},
]),
SharedModule
],
declarations: [
GameListComponent,
GameDetailComponent,
ConvertToColonPipe
]
})
export class GameModule {
}
在上面的片段中,我们是说,在用户可以浏览游戏细节组件之前,GameDetailGuard必须允许访问。现在,我们需要创建卫兵本身,它应该有必要的规则。我们将强制规定id不能是0或NaN。这里是game-detail.guard.ts。我们感兴趣的逻辑被突出显示,而其他代码是标准的Angular模板。
import {Injectable} from '@angular/core';
import {CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router} from '@angular/router';
import {Observable} from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class GameDetailGuard implements CanActivate {
constructor(private router: Router) {
}
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
const id = +next.url[1].path;
if (isNaN(id) || id < 1) {
alert('Invalid product Id');
this.router.navigate(['/games']);
return false;
}
return true;
}
}
Angular路由实例总结
传递参数
在angular中,任何数量的参数都可以传递给路由,用斜线分隔。例如,这里是一个路由定义。
{path: 'games/:id', component: GameDetailComponent}
参数可以通过填充绑定在routerLink指令上的链接参数数组来传递。
<a [routerLink]="['/games', game.gameId]">
{{game.gameName}}
</a>
在组件文件中,你可以使用ActivatedRoute服务读取参数值。
import { ActivatedRoute } from '@angular/router';
constructor(private route: ActivatedRoute) {
const param = this.route.snapshot.paramMap.get('id');
}
用代码进行路由
要用代码激活一个路由,一定要遵循这些步骤。
- 使用路由器服务
- 确保导入该服务并将其定义为构造函数的依赖项
- 添加一个方法,调用路由器服务实例的navigate方法
- 传入链接参数数组
- 添加一个用户界面元素
- 利用事件绑定来绑定创建的方法
用Guard保护路线
使用angular提供的Guard功能,根据你的需要防止或允许访问。要设置一个防护,请遵循以下步骤。
- 建立一个防护服务
- 实现防护类型(CanActivate、CanDeactivate、Resolve或CanLoad)。
- 创建所需的方法(上面的一个)。
- 注册防护服务提供者(providedIn)。
- 将防护装置添加到所需的路由中
在本教程中,我们看了Angular中可用的各种路由技术。我们现在应该熟悉的基本概念是:如何向Angular中的路由传递参数,如何用代码激活一个路由,以及如何用卫兵保护一个路由。路由在Angular中是一个复杂的话题,还有很多东西需要学习,比如必填、可选和查询参数、路由解析器、二级路由器出口和懒惰加载。