Angular 從入坑到挖坑 - 路由守衛連連看

Overview

Angular 入坑記錄的筆記第六篇,介紹 Angular 路由模塊中關於路由守衛的相關知識點,瞭解經常使用到的路由守衛接口,知道如何經過實現路由守衛接口來實現特定的功能需求,以及實現對於特性模塊的惰性加載html

對應官方文檔地址:git

配套代碼地址:angular-practice/src/router-combat程序員

Contents

  1. Angular 從入坑到棄坑 - Angular 使用入門
  2. Angular 從入坑到挖坑 - 組件食用指南
  3. Angular 從入坑到挖坑 - 表單控件概覽
  4. Angular 從入坑到挖坑 - HTTP 請求概覽
  5. Angular 從入坑到挖坑 - Router 路由使用入門指北
  6. Angular 從入坑到挖坑 - 路由守衛連連看

Knowledge Graph

思惟導圖

Step by Step

基礎準備

重複上一篇筆記的內容,搭建一個包含路由配置的 Angualr 項目github

新建四個組件,分別對應於三個實際使用到的頁面與一個設置爲通配路由的 404 頁面typescript

-- 危機中心頁面
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 文件中完成對於項目路由的定義,這裏包含了對於路由的重定向、通配路由,以及經過動態路由進行參數傳遞的使用shell

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 標籤用來聲明路由在頁面上渲染的出口編程

<h1>Angular Router</h1>
<nav>
  <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> &nbsp;&nbsp;
  <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
</nav>
<router-outlet></router-outlet>
複製代碼

項目初始化

路由守衛

在 Angular 中,路由守衛主要能夠解決如下的問題數組

  • 對於用戶訪問頁面的權限校驗(是否已經登陸?已經登陸的角色是否有權限進入?)
  • 在跳轉到組件前獲取某些必須的數據
  • 離開頁面時,提示用戶是否保存未提交的修改

Angular 路由模塊提供了以下的幾個接口用來幫助咱們解決上面的問題app

  • CanActivate:用來處理系統跳轉到到某個路由地址的操做(判斷是否能夠進行訪問)
  • CanActivateChild:功能同 CanActivate,只不過針對的是子路由
  • CanDeactivate:用來處理從當前路由離開的狀況(判斷是否存在未提交的信息)
  • CanLoad:是否容許經過延遲加載的方式加載某個模塊

在添加了路由守衛以後,經過路由守衛返回的值,從而達到咱們控制路由的目的框架

  • true:導航將會繼續
  • false:導航將會中斷,用戶停留在當前的頁面或者是跳轉到指定的頁面
  • UrlTree:取消當前的導航,並導航到路由守衛返回的這個 UrlTree 上(一個新的路由信息)

CanActivate:認證受權

在實現路由守衛以前,能夠經過 Angular CLI 來生成路由守衛的接口實現類,經過命令行,在 app/auth 路徑下生成一個受權守衛類,CLI 會提示咱們選擇繼承的路由守衛接口,這裏選擇 CanActivate 便可

ng g guard auth/auth
複製代碼

建立路由守衛實現類

在 AuthGuard 這個路由守衛類中,咱們模擬了是否容許訪問一個路由地址的認證受權。首先判斷是否已經登陸,若是登陸後再判斷當前登陸人是否具備當前路由地址的訪問權限

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<boolean | UrlTree> | Promise<boolean | UrlTree> | 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 類,針對須要保護的路由進行路由守衛的配置

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 進行路由的認證受權

CanActivateChild:針對子路由的認證受權

與繼承 CanActivate 接口進行路由守衛的方式類似,針對子路由的認證受權能夠經過繼承 CanActivateChild 接口來實現,由於受權的邏輯很類似,這裏經過多重繼承的方式,擴展 AuthGuard 的功能,從而達到同時針對路由和子路由的路由守衛

改造下原先 canActivate 方法的實現,將認證邏輯修改成用戶的 token 信息中包含 admin 便可訪問 crisis-center 頁面,在針對子路由進行認證受權的 canActivateChild 方法中,經過判斷 token 信息是否爲 admin-master 模擬完成對於子路由的訪問認證

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<boolean | UrlTree> | Promise<boolean | UrlTree> | 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<boolean | UrlTree> | Promise<boolean | UrlTree> {
    let token = localStorage.getItem('auth-token') || '';
    if (token === '') {
      this.router.navigate(['/login']);
      return false;
    }

    return token === 'admin-master';
  }
}
複製代碼

經過 Angular CLI 新增一個 crisis-detail 組件,做爲 crisis-list 的子組件

ng g component crisis-detail
複製代碼

接下來在 crisis-list 中添加 router-outlet 標籤,用來定義子路由的渲染出口

<h2>危機中心</h2>

<ul class="crises">
  <li *ngFor="let crisis of crisisList">
    <a [routerLink]="[crisis.id]">
      <span class="badge">{{ crisis.id }}</span>{{ crisis.name }}
    </a>
  </li>
</ul>

<!-- 定義子路由的渲染出口 -->
<router-outlet></router-outlet>
複製代碼

在針對子路由的認證受權配置時,咱們能夠選擇針對每一個子路由添加 canActivateChild 屬性,也能夠定義一個空地址的子路由,將全部歸屬於 crisis-list 的子路由做爲這個空路由的子路由,經過針對這個空路徑添加 canActivateChild 屬性,從而實現將守護規則應用到全部的子路由上

這裏其實至關於將原先兩級的路由模式(父:crisis-list,子:crisis-detail)改爲了三級(父:crisis-list,子:' '(空路徑),孫:crisis-detail)

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 完成對於子路由的認證受權

CanDeactivate:處理用戶未提交的修改

當進行表單填報之類的操做時,由於會涉及到一個提交的動做,當用戶沒有點擊保存按鈕就離開時,最好能暫停,對用戶進行一個友好性的提示,由用戶選擇後續的操做

建立一個路由守衛,繼承於 CanDeactivate 接口

ng g guard hero-list/guards/hero-can-deactivate
複製代碼

與上面的 CanActivate、CanActivateChild 路由守衛的使用方式不一樣,對於 CanDeactivate 守衛來講,咱們須要將參數中的 unknown 替換成咱們實際須要進行路由守衛的組件

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<unknown> {
  canDeactivate(
    component: unknown,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState?: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
    return true;
  }
  
}
複製代碼

例如,這裏針對的是 HeroListComponent 這個組件,所以咱們須要將泛型的參數 unknown 改成 HeroListComponent,經過 component 參數,就能夠得到須要進行路由守衛的組件的相關信息

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<HeroListComponent> {
  canDeactivate(
    component: HeroListComponent,
    currentRoute: ActivatedRouteSnapshot,
    currentState: RouterStateSnapshot,
    nextState?: RouterStateSnapshot
  ):
    | Observable<boolean | UrlTree>
    | Promise<boolean | UrlTree>
    | 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 處理用戶未提交的修改

異步路由

惰性加載

當應用逐漸擴大,使用現有的加載方式會形成應用在第一次訪問時就加載了所有的組件,從而致使系統首次渲染過慢。所以這裏可使用惰性加載的方式在請求具體的模塊時才加載對應的組件

惰性加載只針對於特性模塊(NgModule),所以爲了使用惰性加載這個功能點,咱們須要將系統按照功能劃分,拆分出一個個獨立的模塊

首先經過 Angular CLI 建立一個危機中心模塊(crisis 模塊)

-- 查看建立模塊的相關參數
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 中聲明的組件代碼移除

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 中相關的路由配置刪除

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 文件中,大機率會遇到下面的問題

建立特性模塊

這裏的問題與配置通配路由須要放到最後的緣由類似,由於腳手架在幫咱們將建立的模塊導入到 app.module.ts 中時,是添加到整個數組的最後,同時由於咱們已經將 crisis 模塊的路由配置移動到專門的 crisis-routing.module.ts 中了,框架在進行路由匹配時會預先匹配上 app-routing.module.ts 中設置的通配路由,從而致使沒法找到實際應該對應的組件,所以這裏咱們須要將 AppRoutingModule 放到聲明的最後

app.module.ts

當問題解決後,就能夠針對 crisis 模塊設置惰性加載

在配置惰性路由時,咱們須要以一種相似於子路由的方式進行配置,經過路由的 loadChildren 屬性來加載對應的模塊,而不是具體的組件,修改後的 AppRoutingModule 代碼以下

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 添加到當前的路由配置中,而惰性加載和從新配置工做只會發生一次,也就是在該路由首次被請求時執行,在後續請求時,該模塊和路由都是當即可用的

CanLoad:杜絕未經過認證受權的組件加載

在上面的代碼中,對於 CrisisModule 模塊咱們已經使用 CanActivate、CanActivateChild 路由守衛來進行路由的認證受權,可是當咱們並無權限訪問該路由的權限,卻依然點擊了連接時,此時框架路由仍會加載該模塊。爲了杜絕這種受權未經過仍加載模塊的問題發生,這裏須要使用到 CanLoad 守衛

由於這裏的判斷邏輯與認證受權的邏輯相同,所以在 AuthGuard 中,繼承 CanLoad 接口便可,修改後的 AuthGuard 代碼以下

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<boolean | UrlTree> | Promise<boolean | UrlTree> | 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<boolean | UrlTree> | Promise<boolean | UrlTree> {
    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<boolean> | Promise<boolean> {
    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 數組中便可實現受權認證不經過時不加載模塊

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 { }
複製代碼

佔坑

        做者:墨墨墨墨小宇
        我的簡介:96年生人,出生於安徽某四線城市,畢業於Top 10000000 院校。.NET程序員,槍手死忠,喵星人。於2016年12月開始.NET程序員生涯,微軟.NET技術的堅決堅持者,立志成爲雲養貓的少年中面向谷歌編程最厲害的.NET程序員。
        我的博客:yuiter.com
        博客園博客:www.cnblogs.com/danvic712

相關文章
相關標籤/搜索