## 一、路由守卫:当用户满足一定条件才被允许进入或者离开一个路由。 >[warning] 路由守卫场景: * 该用户可能无权导航到目标组件。 * 可能用户得先登录(认证)。 * 在显示目标组件前,你可能得先获取某些数据。 * 在离开组件前,你可能要先保存修改。 * 你可能要询问用户:你是否要放弃本次更改,而不用保存它们? >[info] 路由守卫返回一个布尔值以控制路由: * 如果它返回`true`,导航过程会继续 * 如果它返回`false`,导航过程就会终止,且用户留在原地。 * 如果它返回`UrlTree`,则取消当前的导航,并且开始导航到返回的这个`UrlTree` >[danger] 守卫可以用同步的方式返回一个布尔值,但大多情况都是异步的,例如网络请求,因此路由守卫可以返回一个Observable对象 或 Promise对象,并且路由器会等待这个可观察对象被解析为`true`或`false`。 ## 二、路由守卫接口 >[info] 路由器可以支持多种守卫接口: * 用`CanActivate`来处理导航到某路由的情况。 * 用`CanActivateChild`来处理导航到某子路由的情况。 * 用`CanDeactivate`来处理从当前路由离开的情况. * 用`Resolve`在路由激活之前获取路由数据。 * 用`CanLoad`来处理异步导航到某特性模块的情况。 > 路由器会先按照从最深的子路由由下往上检查的顺序来检查 CanDeactivate() 和 CanActivateChild() 守卫。 然后它会按照从上到下的顺序检查 CanActivate() 守卫。如果特性模块是异步加载的,在加载它之前还会检查 CanLoad() 守卫。 如果任何一个守卫返回 false,其它尚未完成的守卫会被取消,这样整个导航就被取消了。 ### 1.CanActivate:要求认证(登录认证、权限认证) 创建一个admin特征模块:`ng generate module admin --routing` 创建admin特征模块的子组件: * `ng generate component admin/admin-dashboard` * `content_copyng generate component admin/admin` * `content_copyng generate component admin/manage-crises` * `content_copyng generate component admin/manage-heroes` admin特征模块的文件内容如下: ``` src/app/admin ├─admin │ ├─admin.component.css │ ├─admin.component.html │ └─admin.component.ts ├─admin-dashboard │ ├─admin-dashboard.component.css │ ├─admin-dashboard.component.html │ └─admin-dashboard.component.ts ├─manage-crises │ ├─manage-crises.component.css │ ├─manage-crises.component.html │ └─manage-crises.component.ts ├─manage-heroes │ ├─manage-heroes.component.css │ ├─manage-heroes.component.html │ └─manage-heroes.component.ts ├─admin.module.ts └─admin-routing.module.ts ``` >[info] src/app/admin/admin/admin.component.html文件内容 ``` <h3>ADMIN</h3> <nav> <a routerLink="./" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Dashboard</a> <a routerLink="./crises" routerLinkActive="active">Manage Crises</a> <a routerLink="./heroes" routerLinkActive="active">Manage Heroes</a> </nav> <router-outlet></router-outlet> ``` > 路由链接中有一个斜杠/的路由,表示它能够匹配admin特征模块中的任何路由,但是只希望在访问 Dashboard 路由时才激活该链接,往Dashboard 这个 routerLink 上添加另一个绑定 \[routerLinkActiveOptions\]="{ exact: true }",这样导航到admin特征模块时会激活Dashboard 路由而不会激活其他路由 (访问admin特征模块时激活Dashboard 路由) #### 无组件路由: 分组路由,而不需要组件 >[info] admin特征模块下的路由配置文件: ``` const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, children: [ { path: '', children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {} ``` > `AdminComponent`的子路由,有**path**属性和**children**属性, 但它没有使用**component**。代表这是一个无组件路由 > 这里是对`admin`路径下的`危机中心`管理类路由进行分组,并不需要另一个仅用来分组路由的组件。 一个无组件的路由能让守卫子路由变得更容易。 在app.module.ts中导入AdminModule并把它加入`imports`数组中来注册这些管理类路由。 在app.component.html模板中添加admin特征模块的导航链接 ``` <h1 class="title">Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a> </nav> <div [@routeAnimation]="getAnimationData(routerOutlet)"> <router-outlet #routerOutlet="outlet"></router-outlet> </div> <router-outlet name="popup"></router-outlet> ``` #### 使用CanActivate接口守卫admin特征模块 >[danger] 目前admin特征模块的路由对所有人都是开放的,这显然不够安全,使用CanActivate() 守卫,将正在尝试访问admin特征模块的匿名用户重定向到登录页,只有登录的用户才有权访问 这是一个通用的守卫(假设另外一些特征模块也要求已认证过的用户才能访问),所以可以在`auth`目录下生成一个`AuthGuard`。 一、创建路由守卫 >[info] 在CLI输入命令:ng generate guard auth/auth(路由守卫类名) 输入命令后创建的路由守卫`src/app/auth/auth.guard.ts` ``` import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate { canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { console.log('AuthGuard#canActivate called'); return true; } } ``` 二、添加路由守卫 在`admin`特征模块路由文件中引入`AuthGuard`类并添加`CanActivate()` 守卫 >[danger] canActivate可以指定多个守卫,值是一个数组。 ``` //引入路由守卫类 import { AuthGuard } from '../auth/auth.guard'; const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, canActivate: [AuthGuard], //添加canActivate路由守卫 children: [ { path: '', children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ], } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {} ``` 三、编写路由守卫逻辑进行认证 创建一个服务进行授权认证,该服务能让用户登录(模拟登陆),并且保存当前用户的信息 >[info] 在CLI输入命令:ng generate service auth/auth 输入命令后创建的服务`src/app/auth/auth.service.ts` ``` import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { tap, delay } from 'rxjs/operators'; @Injectable({ providedIn: 'root', }) export class AuthService { isLoggedIn = false; // 存储url,用于登录后进行跳转 redirectUrl: string; login(): Observable<boolean> { return of(true).pipe( delay(1000), tap(val => this.isLoggedIn = true) ); } logout(): void { this.isLoggedIn = false; } } ``` 在路由守卫中使用服务进行登录验证 ``` import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate( next: ActivatedRouteSnapshot, // ActivatedRouteSnapshot 包含了即将被激活的路由 state: RouterStateSnapshot): boolean { //RouterStateSnapshot 包含了该应用即将到达的状态 let url: string = state.url; return this.checkLogin(url); } checkLogin(url: string): boolean { if (this.authService.isLoggedIn) { return true; } //存储要跳转到的url this.authService.redirectUrl = url; // 跳转到登录 this.router.navigate(['/login']); return false; } } ``` >[info] 创建一个登录组件用于用户登录,登录成功后将导航到原来的url。 ### 2.CanActivateChild:保护子路由 >[info] 使用CanActivateChild 路由守卫来保护子路由。 CanActivateChild 守卫和 CanActivate 守卫很像。 区别在于,CanActivateChild 会在任何子路由被激活之前运行。 一、扩展`AuthGuard`,添加`CanActivateChild `接口。 ``` import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild //添加CanActivateChild接口 } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate, CanActivateChild { //继承CanActivateChild 接口 constructor(private authService: AuthService, private router: Router) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { let url: string = state.url; return this.checkLogin(url); } //实现canActivateChild路由逻辑 canActivateChild( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { return this.canActivate(route, state); } /* . . . */ } ``` 二、在特征路由中添加`canActivateChild`路由守卫 同样把这个`AuthGuard`添加到“无组件的”管理路由,来同时保护它的所有子路由,而不是为每个路由单独添加这个`AuthGuard`。 ``` const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, canActivate: [AuthGuard], children: [ { path: '', canActivateChild: [AuthGuard], //添加canActivateChild路由守卫 children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {} ``` ### 3.CanDeactivate:处理未保存的更改 一、创建一个守卫 >[info] 在CLI输入命令:ng generate guard can-deactivate 输入命令后创建的文件`src/app/can-deactivate.guard.ts` ``` import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class CanDeactivatesGuard implements CanActivate { canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { return true; } } ``` 二、实现CanDeactivate路由守卫逻辑 >[info]实现CanDeactivate路由守卫的逻辑,判断表单的数据是否有改变,当数据有所改变用户却要离开页面时提示用户数据未保存 1.单独为组件实现CanDeactivate守卫 ``` import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { CrisisDetailComponent } from './crisis-center/crisis-detail/crisis-detail.component'; @Injectable({ providedIn: 'root', }) export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> { canDeactivate( component: CrisisDetailComponent, //组件 route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean> | boolean { // 获取危机中心的id console.log(route.paramMap.get('id')); //获取当前url console.log(state.url); // 如果表单的值没有改变,则继续导航 if (!component.crisis || component.crisis.name === component.editName) { return true; } //弹出确认框 return component.dialogService.confirm('Discard changes?'); } } ``` 2.实现复用的CanDeactivate守卫 (1)在组件中实现CanDeactivate方法 ``` export class CrisisDetailComponent implements OnInit { crisis: Crisis; editName: string; constructor( private route: ActivatedRoute, private router: Router, public dialogService: DialogService ) {} //在组件中实现canDeactivate路由守卫的实现逻辑 canDeactivate(): Observable<boolean> | boolean { // 如果表单的值没有改变,则继续导航 if (!component.crisis || component.crisis.name === component.editName) { return true; } //弹出确认框 return component.dialogService.confirm('Discard changes?'); } } ``` (2)在CanDeactivateGuard中调用组件的CanDeactivate方法 ``` import { Injectable } from '@angular/core'; import { CanDeactivate } from '@angular/router'; import { Observable } from 'rxjs'; export interface CanComponentDeactivate { canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean; } @Injectable({ providedIn: 'root', }) export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> { canDeactivate(component: CanComponentDeactivate) { //组件中是否有canDeactivate方法,有就执行没有返回true return component.canDeactivate ? component.canDeactivate() : true; } } ``` 三、在路由配置文件中添加CanDeactivate守卫 ``` import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate.guard'; import { CrisisDetailResolverService } from './crisis-detail-resolver.service'; const crisisCenterRoutes: Routes = [ { path: '', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard],//添加canDeactivate路由守卫 resolve: { crisis: CrisisDetailResolverService } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { } ``` ### 4.Resolve: 预先获取组件数据 >[warning] resolve:在进入路由之前去服务器读数据,把需要的数据都读好以后,带着这些数据进到路由里,立刻就把数据显示出来。 一、创建服务获取数据 >[info] 在CLI输入命令:ng generate service crisis-center/crisis-detail-resolver 输入命令后创建的服务`src/app/crisis-center/crisis-detail-resolver.service.ts` ``` import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class CrisisDetailResolverService { constructor() { } } ``` >[info] 使用服务获取数据: ``` import { Injectable } from '@angular/core'; import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { Observable, of, EMPTY } from 'rxjs'; import { mergeMap, take } from 'rxjs/operators'; import { CrisisService } from './crisis.service'; import { Crisis } from './crisis'; @Injectable({ providedIn: 'root', }) export class CrisisDetailResolverService implements Resolve<Crisis> { constructor(private cs: CrisisService, private router: Router) {} resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Crisis> | Observable<never> { let id = route.paramMap.get('id'); //获取路由传递的参数 //获取数据,通常会发起网络请求获取数据,此处只是模拟 return this.cs.getCrisis(id).pipe( take(1), mergeMap(crisis => { if (crisis) { return of(crisis); } else { //id not found this.router.navigate(['/crisis-center']); return EMPTY; } }) ); } } ``` 二、添加Resolve路由守卫 ``` import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate.guard'; import { CrisisDetailResolverService } from './crisis-detail-resolver.service'; const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolverService //添加resolve路由守卫,CrisisDetailResolverService是获取数据的服务 } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { } ``` 三、组件接收数据 ``` ngOnInit() { this.route.data .subscribe((data: { crisis: Crisis }) => { this.editName = data.crisis.name; this.crisis = data.crisis; }); } ``` ### 5.CanLoad接口:保护对特性模块的未授权加载 >[info] 目前已使用CanActivate路由守卫保护admin特征模块,它会阻止未授权的用户访问admin模块,。如果用户未登录,它会跳转到登录页。但是admin模块仍被加载了,因此需要使用CanLoad路由守卫接口实现已登录的情况才加载admin模块 一、实现canLoad路由守卫 打开` auth.guard.ts`,从 `@angular/router` 中导入` CanLoad `接口。 把它添加到 AuthGuard 类的 implements 列表中。 然后实现 canLoad,代码如下: > 路由器会把 canLoad() 方法的 route 参数设置为准备访问的目标 URL。 如果用户已经登录了,checkLogin() 方法就会重定向到那个 URL。 ``` canLoad(route: Route): boolean { let url = `${route.path}`; //url return this.checkLogin(url); } ``` 二、添加canLoad路由守卫 ``` { path: 'admin', loadChildren: './admin/admin.module#AdminModule', canLoad: [AuthGuard] }, ```