学习Angular中可用的各种路由技术

99 阅读9分钟

到目前为止,我们的小Angular应用程序中只有一个视图。当然,我们会希望有更多的选择,让用户访问不同的视图,获得不同的信息和体验。就像我们在基本的网页上有链接一样,我们也可以在Angular应用程序中设置链接来导航到各种视图。正是通过Angular中提供的路由功能,我们可以为各种信息集设置各种路由。在本教程中,我们将学习路由是如何工作的,如何配置路由,如何将路由链接到动作,以及如何建立我们需要的各种视图。


创建一个新的组件

现在,有一个简单的游戏列表组件,显示应用程序中所有游戏的列表。现在,我们想建立一个新的组件,一次显示一个游戏的细节。我们希望能够点击一个链接,让Angular路由工作,这样我们就会被送到该游戏的详细视图。换句话说,我们需要一个新的组件,以便我们能够路由到该组件。下面是游戏细节组件的样子。
angular game detail component

游戏细节组件由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并将所需的组件加载到视图中。
routerlink in angular

因此,当用户在地址栏中访问http://localhost:4200/games,他们会看到游戏列表。
angular menu navigation


配置路由

让我们看看如何在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 路由显示一个特定的游戏。
angular route parameter example


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中是一个复杂的话题,还有很多东西需要学习,比如必填、可选和查询参数、路由解析器、二级路由器出口和懒惰加载。