Angular 從入坑到挖坑 - 路由守衛連連看
阿新 • • 發佈:2020-06-03
### 一、Overview
Angular 入坑記錄的筆記第六篇,介紹 Angular 路由模組中關於路由守衛的相關知識點,瞭解常用到的路由守衛介面,知道如何通過實現路由守衛介面來實現特定的功能需求,以及實現對於特性模組的惰性載入
對應官方文件地址:
- [路由與導航](https://angular.cn/guide/router#routing-and-navigation)
配套程式碼地址:[angular-practice/src/router-combat](https://github.com/Lanesra712/angular-practice/tree/master/src/router-combat "angular route guards tutorial")
### 二、Contents
1. [Angular 從入坑到棄坑 - Angular 使用入門](https://www.cnblogs.com/danvic712/p/getting-started-with-angular.html)
2. [Angular 從入坑到挖坑 - 元件食用指南](https://www.cnblogs.com/danvic712/p/angular-components-guide.html)
3. [Angular 從入坑到挖坑 - 表單控制元件概覽](https://www.cnblogs.com/danvic712/p/angular-forms-overview.html)
4. [Angular 從入坑到挖坑 - HTTP 請求概覽](https://www.cnblogs.com/danvic712/p/angular-http-guide.html)
5. [Angular 從入坑到挖坑 - Router 路由使用入門指北](https://www.cnblogs.com/danvic712/p/getting-started-with-angular-routing.html)
6. [Angular 從入坑到挖坑 - 路由守衛連連看](https://www.cnblogs.com/danvic712/p/getting-started-with-angular-route-guards.html)
### 三、Knowledge Graph
![思維導圖](https://img2020.cnblogs.com/blog/1310859/202006/1310859-20200602210114971-1179323913.png)
### 四、Step by Step
#### 4.1、基礎準備
重複上一篇筆記的內容,搭建一個包含路由配置的 Angualr 專案
新建四個元件,分別對應於三個實際使用到的頁面與一個設定為通配路由的 404 頁面
```powershell
-- 危機中心頁面
ng g component crisis-list
-- 英雄中心頁面
ng g component hero-list
-- 英雄相親頁面
ng g component hero-detail
-- 404 頁面
ng g component page-not-found
```
在 app-routing.module.ts 檔案中完成對於專案路由的定義,這裡包含了對於路由的重定向、通配路由,以及通過動態路由進行引數傳遞的使用
```typescript
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
// 引入元件
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { HeroListComponent } from './hero-list/hero-list.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
const routes: Routes = [
{
path: 'crisis-center',
component: CrisisListComponent,
},
{
path: 'heroes',
component: HeroListComponent,
},
{
path: 'hero/:id',
component: HeroDetailComponent,
},
{
path: '',
redirectTo: '/heroes',
pathMatch: 'full',
},
{
path: '**',
component: PageNotFoundComponent,
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule { }
```
之後,在根元件中,新增 router-outlet 標籤用來宣告路由在頁面上渲染的出口
```html
```
![專案初始化](https://img2020.cnblogs.com/blog/1310859/202006/1310859-20200602210138842-1651919443.gif)
#### 4.2、路由守衛
在 Angular 中,路由守衛主要可以解決以下的問題
- 對於使用者訪問頁面的許可權校驗(是否已經登入?已經登入的角色是否有許可權進入?)
- 在跳轉到元件前獲取某些必須的資料
- 離開頁面時,提示使用者是否儲存未提交的修改
Angular 路由模組提供瞭如下的幾個介面用來幫助我們解決上面的問題
- CanActivate:用來處理系統跳轉到到某個路由地址的操作(判斷是否可以進行訪問)
- CanActivateChild:功能同 CanActivate,只不過針對的是子路由
- CanDeactivate:用來處理從當前路由離開的情況(判斷是否存在未提交的資訊)
- CanLoad:是否允許通過延遲載入的方式載入某個模組
在添加了路由守衛之後,通過路由守衛返回的值,從而達到我們控制路由的目的
- true:導航將會繼續
- false:導航將會中斷,使用者停留在當前的頁面或者是跳轉到指定的頁面
- UrlTree:取消當前的導航,並導航到路由守衛返回的這個 UrlTree 上(一個新的路由資訊)
##### 4.2.1、CanActivate:認證授權
在實現路由守衛之前,可以通過 Angular CLI 來生成路由守衛的介面實現類,通過命令列,在 app/auth 路徑下生成一個授權守衛類,CLI 會提示我們選擇繼承的路由守衛介面,這裡選擇 CanActivate 即可
```shell
ng g guard auth/auth
```
![建立路由守衛實現類](https://img2020.cnblogs.com/blog/1310859/202006/1310859-20200602210154950-1240927886.png)
在 AuthGuard 這個路由守衛類中,我們模擬了是否允許訪問一個路由地址的認證授權。首先判斷是否已經登入,如果登入後再判斷當前登入人是否具有當前路由地址的訪問許可權
```typescript
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
/**
* ctor
* @param router 路由
*/
constructor(private router: Router) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree {
// 判斷是否有 token 資訊
let token = localStorage.getItem('auth-token') || '';
if (token === '') {
this.router.navigate(['/login']);
return false;
}
// 判斷是否可以訪問當前連線
let url: string = state.url;
if (token === 'admin' && url === '/crisis-center') {
return true;
}
this.router.navigate(['/login']);
return false;
}
}
```
之後我們就可以在 app-routing.module.ts 檔案中引入 AuthGuard 類,針對需要保護的路由進行路由守衛的配置
```typescript
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
// 引入元件
import { CrisisListComponent } from './crisis-list/crisis-list.component';
// 引入路由守衛
import { AuthGuard } from './auth/auth.guard';
const routes: Routes = [
{
path: 'crisis-center',
component: CrisisListComponent,
canActivate: [AuthGuard], // 新增針對當前路由的 canActivate 路由守衛
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule { }
```
![使用 CanActivate 進行路由的認證授權](https://img2020.cnblogs.com/blog/1310859/202006/1310859-20200602210212302-322361335.gif)
##### 4.2.2、CanActivateChild:針對子路由的認證授權
與繼承 CanActivate 介面進行路由守衛的方式相似,針對子路由的認證授權可以通過繼承 CanActivateChild 介面來實現,因為授權的邏輯很相似,這裡通過多重繼承的方式,擴充套件 AuthGuard 的功能,從而達到同時針對路由和子路由的路由守衛
改造下原先 canActivate 方法的實現,將認證邏輯修改為使用者的 token 資訊中包含 admin 即可訪問 crisis-center 頁面,在針對子路由進行認證授權的 canActivateChild 方法中,通過判斷 token 資訊是否為 admin-master 模擬完成對於子路由的訪問認證
```typescript
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router, CanActivateChild } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanActivateChild {
/**
* ctor
* @param router 路由
*/
constructor(private router: Router) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree {
// 判斷是否有 token 資訊
let token = localStorage.getItem('auth-token') || '';
if (token === '') {
this.router.navigate(['/login']);
return false;
}
// 判斷是否可以訪問當前連線
let url: string = state.url;
if (token.indexOf('admin') !== -1 && url.indexOf('/crisis-center') !== -1) {
return true;
}
this.router.navigate(['/login']);
return false;
}
canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean | UrlTree | Observable | Promise {
let token = localStorage.getItem('auth-token') || '';
if (token === '') {
this.router.navigate(['/login']);
return false;
}
return token === 'admin-master';
}
}
```
通過 Angular CLI 新增一個 crisis-detail 元件,作為 crisis-list 的子元件
```shell
ng g component crisis-detail
```
接下來在 crisis-list 中新增 router-outlet 標籤,用來定義子路由的渲染出口
```html
```
在針對子路由的認證授權配置時,我們可以選擇針對每個子路由新增 canActivateChild 屬性,也可以定義一個空地址的子路由,將所有歸屬於 crisis-list 的子路由作為這個空路由的子路由,通過針對這個空路徑新增 canActivateChild 屬性,從而實現將守護規則應用到所有的子路由上
這裡其實相當於將原先兩級的路由模式(父:crisis-list,子:crisis-detail)改成了三級(父:crisis-list,子:' '(空路徑),孫:crisis-detail)
```typescript
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
// 引入元件
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';
// 引入路由守衛
import { AuthGuard } from './auth/auth.guard';
const routes: Routes = [
{
path: 'crisis-center',
component: CrisisListComponent,
canActivate: [AuthGuard], // 新增針對當前路由的 canActivate 路由守衛
children: [{
path: '',
canActivateChild: [AuthGuard], // 新增針對子路由的 canActivate 路由守衛
children: [{
path: 'detail',
component: CrisisDetailComponent
}]
}]
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule { }
```
![使用 CanActivateChild 完成對於子路由的認證授權](https://img2020.cnblogs.com/blog/1310859/202006/1310859-20200602210251234-1519079637.gif)
#### 4.2.3、CanDeactivate:處理使用者未提交的修改
當進行表單填報之類的操作時,因為會涉及到一個提交的動作,當用戶沒有點選儲存按鈕就離開時,最好能暫停,對使用者進行一個友好性的提示,由使用者選擇後續的操作
建立一個路由守衛,繼承於 CanDeactivate 介面
```shell
ng g guard hero-list/guards/hero-can-deactivate
```
與上面的 CanActivate、CanActivateChild 路由守衛的使用方式不同,對於 CanDeactivate 守衛來說,我們需要將引數中的 unknown 替換成我們實際需要進行路由守衛的元件
```typescript
import { Injectable } from '@angular/core';
import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class HeroCanDeactivateGuard implements CanDeactivate {
canDeactivate(
component: unknown,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot): Observable | Promise | boolean | UrlTree {
return true;
}
}
```
例如,這裡針對的是 HeroListComponent 這個元件,因此我們需要將泛型的引數 unknown 改為 HeroListComponent,通過 component 引數,就可以獲得需要進行路由守衛的元件的相關資訊
```typescript
import { Injectable } from '@angular/core';
import {
CanDeactivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';
// 引入需要進行路由守衛的元件
import { HeroListComponent } from '../hero-list.component';
@Injectable({
providedIn: 'root',
})
export class HeroCanDeactivateGuard
implements CanDeactivate {
canDeactivate(
component: HeroListComponent,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot
):
| Observable
| Promise
| boolean
| UrlTree {
// 判斷是否修改了原始資料
//
const data = component.hero;
if (data === undefined) {
return true;
}
const origin = component.heroList.find(hero => hero.id === data.id);
if (data.name === origin.name) {
return true;
}
return window.confirm('內容未提交,確認離開?');
}
}
```
這裡模擬判斷使用者有沒有修改原始的資料,當用戶修改了資料並移動到別的頁面時,觸發路由守衛,提示使用者是否儲存後再離開當前頁面
![使用 CanDeactivate 處理使用者未提交的修改](https://img2020.cnblogs.com/blog/1310859/202006/1310859-20200602210317647-1833092934.gif)
#### 4.3、非同步路由
##### 4.3.1、惰性載入
當應用逐漸擴大,使用現有的載入方式會造成應用在第一次訪問時就載入了全部的元件,從而導致系統首次渲染過慢。因此這裡可以使用惰性載入的方式在請求具體的模組時才載入對應的元件
惰性載入只針對於特性模組(NgModule),因此為了使用惰性載入這個功能點,我們需要將系統按照功能劃分,拆分出一個個獨立的模組
首先通過 Angular CLI 建立一個危機中心模組(crisis 模組)
```powershell
-- 檢視建立模組的相關引數
ng g module --help
-- 建立危機中心模組(自動在 app.moudule.ts 中引入新建立的 CrisisModule、添加當前模組的路由配置)
ng g module crisis --module app --routing
```
將 crisis-list、crisis-detail 元件全部移動到 crisis 模組下面,並在 CrisisModule 中新增對於 crisis-list、crisis-detail 元件的宣告,同時將原來在 app.module.ts 中宣告的元件程式碼移除
```typescript
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CrisisRoutingModule } from './crisis-routing.module';
import { FormsModule } from '@angular/forms';
// 引入模組中使用到的元件
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';
@NgModule({
declarations: [
CrisisListComponent,
CrisisDetailComponent
],
imports: [
CommonModule,
FormsModule,
CrisisRoutingModule
]
})
export class CrisisModule { }
```
同樣的,將當前模組的路由配置移動到專門的路由配置檔案 crisis-routing.module.ts 中,並將 app-routing.module.ts 中相關的路由配置刪除
```typescript
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
// 引入元件
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';
// 引入路由守衛
import { AuthGuard } from '../auth/auth.guard';
const routes: Routes = [{
path: '',
component: CrisisListComponent,
canActivate: [AuthGuard], // 新增針對當前路由的 canActivate 路由守衛
children: [{
path: '',
canActivateChild: [AuthGuard], // 新增針對子路由的 canActivate 路由守衛
children: [{
path: 'detail',
component: CrisisDetailComponent
}]
}]
}];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class CrisisRoutingModule { }
```
重新執行專案,如果你在建立模組的命令中設定了自動引入當前模組到 app.module.ts 檔案中,大概率會遇到下面的問題
![建立特性模組](https://img2020.cnblogs.com/blog/1310859/202006/1310859-20200602210339246-415131525.png)
這裡的問題與配置通配路由需要放到最後的原因相似,因為腳手架在幫我們將建立的模組匯入到 app.module.ts 中時,是新增到整個陣列的最後,同時因為我們已經將 crisis 模組的路由配置移動到專門的 crisis-routing.module.ts 中了,框架在進行路由匹配時會預先匹配上 app-routing.module.ts 中設定的通配路由,從而導致無法找到實際應該對應的元件,因此這裡我們需要將 AppRoutingModule 放到宣告的最後
![app.module.ts](https://img2020.cnblogs.com/blog/1310859/202006/1310859-20200602210352066-1076220923.png)
當問題解決後,就可以針對 crisis 模組設定惰性載入
在配置惰性路由時,我們需要以一種類似於子路由的方式進行配置,通過路由的 loadChildren 屬性來載入對應的模組,而不是具體的元件,修改後的 AppRoutingModule 程式碼如下
```typescript
import { HeroCanDeactivateGuard } from './hero-list/guards/hero-can-deactivate.guard';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: 'crisis-center',
loadChildren: () => import('./crisis/crisis.module').then(m => m.CrisisModule)
}
];
@NgModule({
imports: [RouterModule.forRoot(routes, { enableTracing: true })],
exports: [RouterModule],
})
export class AppRoutingModule { }
```
當導航到這個 /crisis-center 路由時,框架會通過 loadChildren 字串來動態載入 CrisisModule,然後把 CrisisModule 新增到當前的路由配置中,而惰性載入和重新配置工作只會發生一次,也就是在該路由首次被請求時執行,在後續請求時,該模組和路由都是立即可用的
##### 4.3.2、CanLoad:杜絕未通過認證授權的元件載入
在上面的程式碼中,對於 CrisisModule 模組我們已經使用 CanActivate、CanActivateChild 路由守衛來進行路由的認證授權,但是當我們並沒有許可權訪問該路由的許可權,卻依然點選了連結時,此時框架路由仍會載入該模組。為了杜絕這種授權未通過仍載入模組的問題發生,這裡需要使用到 CanLoad 守衛
因為這裡的判斷邏輯與認證授權的邏輯相同,因此在 AuthGuard 中,繼承 CanLoad 介面即可,修改後的 AuthGuard 程式碼如下
```typescript
import { Injectable } from '@angular/core';
import {
CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router, CanActivateChild, CanLoad, Route, UrlSegment
} from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanActivateChild, CanLoad {
/**
* ctor
* @param router 路由
*/
constructor(private router: Router) { }
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree {
// 判斷是否有 token 資訊
let token = localStorage.getItem('auth-token') || '';
if (token === '') {
this.router.navigate(['/login']);
return false;
}
// 判斷是否可以訪問當前連線
let url: string = state.url;
if (token.indexOf('admin') !== -1 && url.indexOf('/crisis-center') !== -1) {
return true;
}
this.router.navigate(['/login']);
return false;
}
canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot): boolean | UrlTree | Observable | Promise {
let token = localStorage.getItem('auth-token') || '';
if (token === '') {
this.router.navigate(['/login']);
return false;
}
return token === 'admin-master';
}
canLoad(route: Route, segments: UrlSegment[]): boolean | Observable | Promise {
let token = localStorage.getItem('auth-token') || '';
if (token === '') {
this.router.navigate(['/login']);
return false;
}
let url = `/${route.path}`;
if (token.indexOf('admin') !== -1 && url.indexOf('/crisis-center') !== -1) {
return true;
}
}
}
```
同樣的,針對路由守衛的實現完成後,將需要使用到的路由守衛新增到 crisis-center 路由的 canLoad 陣列中即可實現授權認證不通過時不載入模組
```typescript
import { HeroCanDeactivateGuard } from './hero-list/guards/hero-can-deactivate.guard';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: 'crisis-center',
loadChildren: () => import('./crisis/crisis.module').then(m => m.CrisisModule)
}
];
@NgModule({
imports: [RouterModule.forRoot(routes, { enableTracing: true })],
exports: [RouterModule],
})
export class AppRoutingModule
Angular Router
危機中心
- {{ crisis.id }}{{ crisis.name }}