【Angular】Angular中按顺序执行路由守卫的一种新方式

发布时间 2023-12-15 11:37:43作者: unuliha

翻译自:A New Way Of Ordering Guards In Angular

概览

在Angular发布了“函数化守卫和解析器(functional guards and resolvers)”的新特性后,我们能够很容易地实现按顺序执行路由守卫,而不是像之前那样一次执行所有的路由守卫。

在路由配置中有多个守卫时,它们通常会一次性执行。例如,假设你的路由配置如下:

{
    path: 'crisis-center',
    canActivate: [FirstGuard, SecondGuard],
    ...
}

即使这些守卫是放在数组中,不管FirstGuard是否返回trueSecondGuard总是会执行。我们想要的效果其实是当FirstGuard返回false后,SecondGuard就压根儿不要执行了。

举个更实际的例子,第一个守卫就是确保用户已经登入,等二个守卫是基于用户已登入去调用一些API执行的操作。

Functional solution

在之前,实现顺序路由守卫的最好方式就是写一个额外的“parent"守卫来顺序调用现有的守卫,这意味着,你需要为每个要顺序执行的路由配置,新建一个"guard class",或者以一种更加通用的方式写你的"guard",即配置上routerdata对象。

现在有一种更加简单的方式。Angular 15 announcement blog post中对Angular的新特性进行了简要介绍,里面提到了"Functional router guards",就是说在你任何想要放置路由守卫的地方现在都可以放置"functions"了,而不是像之前只能放"classes"。

一种最简单的形式,你可以像下面这样写你的路由配置:

canActivate: [() => isUserLoggedIn()]

如果我们将这个新特性和最近更新的inject函数结合,我们可以像下面这样替换任何"guard class":

canActivate: [FirstGuard]

用下面的方式替换:

canActivate: [(route, state) =>
  inject(FirstGuard).canActivate(route, state)]

虽然更冗长,但它给我们提供了很多能力,例如,守卫合并。

顺序执行同步守卫

在下面这个例子中,假设你的守卫都是同步的,并且会立即返回一个布尔值。如果你想要它们顺序执行,你可以像下面这样写:

const orderedSyncGuards =
  (guards) =>
    (route, state) =>
      guards.every(guard =>
        inject(guard).canActivate(route, state));

const ROUTE = {
  ...
  canActivate: [orderedSyncGuards([FirstGuard, SecondGuard])]

这个例子有点过于简单化了,下面让我们看一个更加复杂的例子,它是异步守卫,同时能够返回一个UrlTrees

顺序执行异步守卫

在介绍之前,我需要做一个简短的免责说明:按顺序执行多个异步守卫将会拖慢你的页面加载速度,因为后面的守卫必须要等前面的守卫执行完毕才开始执行。如果你重视性能,你最好重写你的守卫,处理这些不利情况。

完整例子:follow the full example on StackBlitz.

这个例子中我们假设所有的守卫都返回"observable",这些“observable”只会发出布尔值或者UrlTree,并且马上"complete"。

因为我们想要按顺序执行守卫,就可以使用concatMap作为我们的主要的操作符。我们同时想要在任何一个守卫返回的值不是true时中断函数,同时函数需要返回最后一次发出的值。

在对代码的TypeScript的类型进行了一些简化后,下面的代码就是最终结果:

interface AsyncGuard extends CanActivate {
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean | UrlTree>;
}

function orderedAsyncGuards(
  guards: Array<new () => AsyncGuard>
): CanActivateFn {
  return (route, state) => {
    // Instantiate all guards.
    const guardInstances = guards.map(inject) as AsyncGuard[];
    // Convert an array into an observable.
    return from(guardInstances).pipe(
      // For each guard, fire canActivate and wait for it
      // to complete.
      concatMap((guard) => guard.canActivate(route, state)),
      // Don't execute the next guard if the current guard's
      // result is not true.
      takeWhile((value) => value === true, /* inclusive */ true),
      // Return the last guard's result.
      last()
    );
  };
}

const ROUTE = {
  ...
  canActivate: [orderedAsyncGuards([FirstGuard, SecondGuard])]

这就是你可以用来实现的一个通用的同步或异步守卫按顺序执行的思路,它仅使用单个函数调用!

我将把编写一个通用的顺序守卫(sync/async,boolean/UrlTree)留给读者作为练习:),这里有一个提示:从async解决方案开始,并检查guard.canActivate调用的结果。