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

相关文章
相关标签/搜索