简介
到本系列教程目前为止,任何用户都能在任何时候导航到任何地方。但有时候出于种种原因需要控制对该应用的不同部分的访问。可能包括如下场景:
-
该用户可能无权导航到目标组件或者需要先登录(认证)才能访问; -
在显示目标组件前,你可能得先获取某些数据; -
在离开组件前,你可能要先保存修改或者根据用户意愿判定是否保持。
-
如果它返回 true,导航过程会继续; -
如果它返回 false,导航过程就会终止,且用户留在原地; -
如果它返回 UrlTree,则取消当前的导航,并且开始导航到返回的这个UrlTree。
-
用 CanActivate来处理导航到某路由的情况; -
用 CanActivateChild来处理导航到某子路由的情况; -
用 CanDeactivate来处理从当前路由离开的情况; -
用 Resolve在路由激活之前获取路由数据; -
用 CanLoad来处理异步导航到某特性模块的情况。
CanActivate
CanActivate 守卫是一个管理需要身份验证导航类业务规则的工具。admin 模块来演示。如果用户没有登录,是不能进入 admin 管理界面,自动跳转到登录页面去登录,登录后重定向回 admin 管理界面。最终结果如下:
准备工作
新建 admin 模块:
ng g m components/router-study/admin --routing
新建 admin 组件:
ng g c components/router-study/admin -c OnPush -s
新建 admin-dashboard 组件:
ng g c components/router-study/admin/admin-dashboard -c OnPush -s
新建 manage-user 组件:
ng g c components/router-study/admin/manage-user -c OnPush -s
在 router-study 模块中引入 admin 模块:
// router-study.module.ts imports: [ ... AdminModule ]
配置 admin 路由信息:
// admin-routing.module.ts
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
children: [
{
path: '',
children: [
{ path: 'user', component: ManageUserComponent },
{ path: '', component: AdminDashboardComponent }
]
}
]
}
];
admin 下面有一个无组件路由,它包含了我们创建的两个子路由。并且默认展示的是 admin-dashboard 组件。admin-dashboard 组件模板内容(两个导航链接及一个路由出口):<!-- admin.component.html -->
<h2>ADMIN</h2>
<nav>
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link" routerLink="./" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="./user" routerLinkActive="active">Manage user</a>
</li>
</ul>
</nav>
<router-outlet></router-outlet>
routerLinkActiveOptions 属性,这是限制 routerLinkActive 的匹配规则, true 表示完全匹配。因为上面 admin-dashboard 组件的路径是空,如不加以限制,任意路径都会添加 ‘active’。给 admin 模块添加入口:
// router-study.component.ts
template: `
<div class="container">
<h1>router study page</h1>
<ul class="nav nav-pills">
<li class="nav-item">
<a class="nav-link" routerLink="users" routerLinkActive="active">Users</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="/comments" routerLinkActive="active">Comments</a>
</li>
<!-- 入口 -->
<li class="nav-item">
<a class="nav-link" routerLink="/admin" routerLinkActive="active">Admin</a>
</li>
</ul>
<router-outlet></router-outlet>
</div>
`

添加守卫
auth 模块来专门管理用户认证信息:ng g m components/router-study/auth --routing
ng g g components/router-study/auth/auth
g 是 guard 的简写。执行上面命令后会让我们选择创建哪个守卫,我们选择 CanActivate :
auth.guard.ts 文件默认代码:// auth.guard.ts
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return true;
}
}
- 守卫也是一个服务,并且默认从 ‘
root’ 中提供; - 返回值可以是三种类型的布尔值或
UrlTree。
使用守卫,只需要在路由配置文件中添加 CanActivate: [AuthGuard]:
// admin-routing.module.ts
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
canActivate: [AuthGuard],
...
}
];
此时,因为守卫直接返回了 true ,所以,同样可以导航到 admin 组件,并没有什么影响。
显然,我们是需要通过逻辑来判定最后返回的结果,以便控制守卫进行拦截或放行。
auth 服务进行统筹管理登录状态:ng g s components/router-study/auth/auth
auth 服务的逻辑:// auth.service.ts
...
export class AuthService {
// 登陆状态
isLoggedIn = false;
// 保存登录后重定向的路径
redirectUrl: string;
// 模拟登录
login(): Observable<boolean> {
return of(true).pipe(
delay(1000),
tap(val => this.isLoggedIn = true)
);
}
logout(): void {
this.isLoggedIn = false;
}
}
auth 服务:// auth.guard.ts
export class AuthGuard implements CanActivate {
// 引入服务
constructor(private authService: AuthService, private router: Router) {}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): true | UrlTree {
const url: string = state.url; // 将要跳转的路径
return this.checkLogin(url);
}
private checkLogin(url: string): true | UrlTree {
// 已经登录,直接返回true
if (this.authService.isLoggedIn) { return true; }
// 修改登陆后重定向的地址
this.authService.redirectUrl = url;
// 重定向到登录页面
return this.router.parseUrl('/login');
}
}
login 组件:ng g c components/router-study/auth/login -s -c OnPush
login 模板:<!-- login.component.html-->
<h3>LOGIN</h3>
<p [class.text-danger]="!authService.isLoggedIn">{{message}}</p>
<p>
<button class="btn btn-primary" (click)="login()" *ngIf="!authService.isLoggedIn">登录</button>
<button class="btn btn-danger" (click)="logout()" *ngIf="authService.isLoggedIn">退出登录</button>
</p>
// login.component.ts
...
export class LoginComponent {
message: string;
constructor(public authService: AuthService, public router: Router) {
this.setMessage();
}
setMessage() {
this.message = this.authService.isLoggedIn ? '已经登录~' : '没有登录!';
}
login() {
this.message = '登录中 ...';
this.authService.login().subscribe(() => {
this.setMessage();
if (this.authService.isLoggedIn) {
const redirectUrl = this.authService.redirectUrl || '/admin'; // 防止用户直接在地址栏输入造成的redirectUrl为空的错误
// 跳转回重定向路径
this.router.navigate([redirectUrl]);
}
});
}
logout() {
this.authService.logout();
this.setMessage();
this.router.navigate(['/']);
}
}
login 路由:// auth-routing.module.ts
const routes: Routes = [
{path: '/login', component: LoginComponent}
];
router-study 模块中引入 auth 模块:// router-study.module.ts imports: [ ... AdminModule, AuthModule ]
CanActivate 守卫的逻辑全部完成。CanActivateChild
CanActivateChild 守卫跟 CanActivate 守卫类似。只不过是用来保护子路由。它们的区别在于,CanActivateChild 会在任何子路由被激活之前运行。
在 auth.guard 中实现 CanActivateChild 守卫:
// auth.guard.ts
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(private authService: AuthService, private router: Router) {}
canActivateChild(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): true | UrlTree {
// 直接复用前面的逻辑
return this.canActivate(next, state);
}
// ...
}
只能在子路由中使用守卫:
// admin-routing.module.ts
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
children: [
{
path: '',
canActivateChild: [AuthGuard],
children: [
{ path: 'user', component: ManageUserComponent },
{ path: '', component: AdminDashboardComponent }
]
}
]
}
];
其实这样我们是拦截了整个子路由,跟前面的效果是一样的。如果还想拦截进一步的 manage-user ,那需要创建一个无组件路由来包裹 manage-user :
// admin-routing.module.ts
const routes: Routes = [
{
path: 'admin',
component: AdminComponent,
children: [
{
path: '',
children: [
{
path: 'user',
canActivateChild: [AuthGuard],
children: [
{path: '', component: ManageUserComponent}
]
},
{ path: '', component: AdminDashboardComponent }
]
}
]
}
];

CanDeactivate
CanDeactivate 守卫则是拦截跳出路由。我们将通过修改 user 详情来做示例。
- 没有修改的情况下:点‘保存’,则正常保存退出,点‘取消’,就不保存退出;
- 有变动的情况下:点‘保存’,则正常保存退出,点‘取消’,根据弹出询问框用户选择结果退出。
最终效果如下:

准备工作
修改原来的 user 组件:
// user.component.ts
...
template: `
<div *ngIf="user">
<h4>{{user.name}}</h4>
<div class="form-group row">
<label for="staticEmail" class="col-sm-1 col-form-label">Email:</label>
<div class="col-sm-10">
<input type="text" class="" id="staticEmail" [(ngModel)]="editEmail" placeholder="email...">
</div>
</div>
<div class="btn btn-group">
<button class="btn btn-secondary" (click)="save()">保存</button>
<button class="btn btn-danger" (click)="cancel()">取消</button>
</div>
</div>
`
...
export class UserComponent implements OnInit {
user: User; // 这里不使用Observable
editEmail: string; // 保存初始email值
constructor(
private userServe: UserService,
private route: ActivatedRoute,
private cdr: ChangeDetectorRef,
private router: Router
) { }
ngOnInit(): void {
this.route.params.pipe(
switchMap(params => this.userServe.getUser(params.id))
).subscribe(res => {
this.user = res;
this.editEmail = res.email;
// 标记需要被变更检测
this.cdr.markForCheck();
});
}
save(): void {
this.user.email = this.editEmail;
this.goBack();
}
cancel(): void {
this.goBack();
}
private goBack(): void {
this.router.navigate(['../'], {relativeTo: this.route});
}
}

实现守卫
Dialog 服务,来处理用户的确认操作:ng g s components/router-study/user/dialog
confirm 方法:// dialog.service.ts
...
export class DialogService {
confirm(message?: string): Observable<boolean> {
const confirmation = confirm(message || 'Is it OK?');
return of(confirmation);
}
}
user 下创建一个 can-deactivate 守卫:ng g g components/router-study/user/can-deactivate
// can-deactivate.guard.ts
...
// 声明一个包含canDeactivate方法的接口
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
// 将组件作为参数传入
canDeactivate(component: CanComponentDeactivate): Observable<boolean> | Promise<boolean> | boolean {
// 判读传入的组件是否存在canDeactivate方法,如果存在,则返回执行结果
return component?.canDeactivate();
}
}
deactivate 方法,它可以检测 UserComponent 组件有没有 CanDeactivate() 方法并调用它。使用守卫:// router-study-routing.module.ts
const routes: Routes = [
...
{
path: 'users',
children: [
{
path: '',
component: UsersComponent,
children: [
{
path: ':id',
component: UserComponent,
canDeactivate: [CanDeactivateGuard]
}
]
}
]
},
];
此时,因为 UserComponent 组件中没有 CanDeactivate 方法,所以并不会拦截路由跳出。
给 UserComponent 组件添加 CanDeactivate 方法:
// user.component.ts
...
canDeactivate(): Observable<boolean> | boolean {
if (!this.user || this.user.email === this.editEmail) {
return true;
}
return this.dialogService.confirm('放弃保存?');
}
Resolve
Resolve 守卫用来预先获取组件数据。
通常情况下,我们从服务器获取数据都会有一定延迟,这样就会导致页面第一时间拿不到数据,导致无法显示。 Resolve 守卫就可以解决这一痛点。
在 user 组件下新建一个 user-resolve 服务,用来获取 users 数据:
ng g s components/router-study/user/user-resolve
实现 Resolve 守卫:
// user-resolve.service.ts
...
// 引入Resolve接口
export class UserResolveService implements Resolve<User>{
constructor(private userServe: UserService, private router: Router) { }
// 实现对应方法
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<User> | Promise<User> | User | Observable<never> {
return this.userServe.getUser(route.paramMap.get('id')).pipe(
first(), // 让流变成可结束流
mergeMap(user => {
if (user) {
return of(user);
} else { // 没有找到数据
this.router.navigateByUrl('/');
return EMPTY; // 返回EMPTY,就不能进入到下一级路由
}
})
);
}
}
使用守卫,路由中配置 Resolve :
// router-study-routing.module.ts
...
{
path: 'users',
children: [
{
path: '',
component: UsersComponent,
children: [
{
path: ':id',
component: UserComponent,
canDeactivate: [CanDeactivateGuard],
resolve: {
user: UserResolveService
}
}
]
}
]
}
user 组件中修改获取数据方式:
// user.component.ts
...
ngOnInit(): void {
this.route.data.subscribe((data: { user: User }) => {
this.user = data.user;
this.editEmail = data.user.email;
this.cdr.markForCheck();
});
}
...
tips: this.route.data 可以获取到路由配置中所有自定义的属性。
这样我们就实现了 Resolve 守卫全部的功能,因为我们获取本地数据的速度足够快,所以页面显示效果并没有差异。
下图演示下如果找不到数据,页面会拦截进入详情路由,并跳转首页:

总结
CanActivate、CanActivateChild守卫拦截进入路由,可以进行鉴权相关判定;CanDeactivate守卫拦截退出路由,可以处理未保存的更改;Resolve守卫用来预先获取组件数据。
本文转载自:岩弈,版权归原作者所有,本博客仅以学习目的的传播渠道,不作版权和内容观点阐述,转载时根据场景需要有所改动。



最新评论