angular 路由守卫

路由守卫

所谓守卫,不就是把守出入口以控制进出的作用吗?路由守卫也就是掌控用户导航到应用不同场景的手段。

简介

到本系列教程目前为止,任何用户都能在任何时候导航到任何地方。但有时候出于种种原因需要控制对该应用的不同部分的访问。可能包括如下场景:

  • 该用户可能无权导航到目标组件或者需要先登录(认证)才能访问;
  • 在显示目标组件前,你可能得先获取某些数据;
  • 在离开组件前,你可能要先保存修改或者根据用户意愿判定是否保持。

你可以往路由配置中添加守卫,来处理这些场景。 守卫返回一个值,以控制路由器的行为:

  • 如果它返回 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>

tips: 上面使用了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

tips:gguard的简写。

执行上面命令后会让我们选择创建哪个守卫,我们选择 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守卫全部的功能,因为我们获取本地数据的速度足够快,所以页面显示效果并没有差异。

下图演示下如果找不到数据,页面会拦截进入详情路由,并跳转首页:

在这里插入图片描述

总结

  1. CanActivateCanActivateChild守卫拦截进入路由,可以进行鉴权相关判定;

  2. CanDeactivate守卫拦截退出路由,可以处理未保存的更改;

  3. Resolve守卫用来预先获取组件数据。


感谢你的阅读,如果你觉得有用,欢迎评论、点赞、转发~
当然也希望你能关注我的公众号:前端大乱炖
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yanyi24

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值