Angular имеет отличный инструмент для ограничения навигации между страницам из коробки. Но, как и любом большом проекте, у него есть свои подводные камни. Сегодня я расскажу вам об одном из них.

Перед прочтением статьи освежите знания по Angular Guards (далее гарды). Статья будет о CanActivate, но это относиться так же и к остальным гардам. Теперь перейдем к истории обнаружения приоритетов.

Первая проба

Недавно я разрабатывал новую фичу для админов. Требовалось ограничить доступ на страницу — пускать пользователя, если у него включен определенный фича флаг и он администратор, в противном случае редирект на главную.

Кажется, что задача тривиальная. В моем мозгу сразу появилось решение с помощью гарда CanActivate. Допустим, уже существуют два сервиса на эти два условия. Далее вызываем нужные методы, объединяем их в один Observable и дальше если хоть один вернул false, то редиректим на главную, если оба true, то пускаем пользователя к странице.

@Injectable()
export class AdminGuard implements CanActivate {
  constructor(
    private readonly featureToggleService: FeatureToggleService,
    private readonly userService: UserService,
    private readonly router: Router
  ) {}

  canActivate(): Observable<boolean | UrlTree> {
    const isAdminPageEnabled$ =
      this.featureToggleService.isAdminPageEnabled$.pipe(startWith(null));

    const isAdmin$ = this.userService.isAdmin$.pipe(startWith(null));

    return combineLatest([isAdminPageEnabled$, isAdmin$]).pipe(
      first(
        conditions =>
          conditions.some(condition => condition === false) ||
          conditions.every(Boolean)
      ),
      map(
        ([isAdminPageEnabled, isAdmin]) =>
          (isAdminPageEnabled && isAdmin) || this.router.createUrlTree(['/'])
      )
    );
  }
}

Код работает, но если присмотреться, то можно написать лучше. В реализации слишком много ответственности на один маленький гард, и код не читаемый. Поправим это.

Рефакторинг

Раз слишком много ответственности, можно разделить гарды на два, учитывая, что свойство CanActivate у роута принимает массив гардов.

// admin-page-feature.guard.ts
@Injectable()
export class AdminPageFeatureGuard implements CanActivate {
  constructor(
    private readonly featureToggleService: FeatureToggleService,
    private readonly router: Router
  ) {}

  canActivate(): Observable<boolean | UrlTree> {
    return this.featureToggleService.isAdminPageEnabled$.pipe(
      map(
        isAdminPageEnabled =>
          isAdminPageEnabled || this.router.createUrlTree(['/'])
      )
    );
  }
}

// admin.guard.ts
@Injectable()
export class AdminGuard implements CanActivate {
  constructor(
    private readonly userService: UserService,
    private readonly router: Router
  ) {}

  canActivate(): Observable<boolean | UrlTree> {
    return this.userService.isAdmin$.pipe(
      map(isAdmin => isAdmin || this.router.createUrlTree(['/']))
    );
  }
}

Теперь стало два гарда, которые можно переиспользовать, и код стал читаемый. Казалось бы все отлично, но на этом моменте я озадачился одним вопросом. Гарды в массиве CanActivate выполняются последовательно или параллельно?

const routes: Routes = [
  {
    path: 'admin',
    component: AdminPageComponent,
    canActivate: [AdminGuard, AdminPageFeatureGuard],
  },
];

Дела мои плохи, если гарды выполняются последовательно, так как нет смысла заставлять пользователя ждать каждый гард отдельно.

Я провел исследование и выяснил, что они выполняются параллельно, но с некоторыми нюансами.

Приоритеты гардов

В далекой седьмой версии Angular завезли приоритет для гардов. Переданные в массив для CanActivate гарды будут выполняться параллельно, но роутер будет ждать, пока более высокий приоритет завершится, чтобы двигаться дальше. Приоритет определяется порядком в массиве СanActivate.

Разберемся на примере. Есть два гарда HighPriorityGuard и LowPriorityGuard. Оба возвращают Observable<boolean | UrlTree>. Мы производим навигацию по роуту, где применяются эти гарды.

canActivate: [HighPriorityGuard, LowPriorityGuard]

Факты о выполнение гардов:

  • Запускаются параллельно.

  • Если LowPriorityGuard возвращает UrlTree|false  первым, то роутер все равно будет дожидаться выполнения HighPriorityGuard перед тем, как начать навигацию.

  • Если HighPriorityGuard возвращает первым UrlTree , то сразу будет произведена навигация на UrlTree из HighPriorityGuard.

  • Если LowPriorityGuard возвращает UrlTree первым, а далее HighPriorityGuard возвращает UrlTree, то навигация произойдет на UrlTree из HighPriorityGuard, так как побеждает более высокий приоритет.

  • Если HighPriorityGuard возвращает true первым, роутер будет ждать LowPriorityGuard, так как не может производить навигацию, пока не убедится, что все гарды возвращают true.

Если остались вопросы по приоритетам гардов, пожалуйста, перейдите по этой ссылке, я подготовил небольшое демо, там будет проще разобраться.

Зачем нужны приоритеты

Команда Angular добавила приоритет для гардов, чтобы решить проблему множественных редиректов. Допустим, в приложение есть два гарда — аутентификации и админа. Гард аутентификации, будет отправлять на страницу логина, если пользователь не залогинился. А гард админа, будет отправлять на страницу “Вы не админ”, если у пользователя нет нужной роли. До седьмой версии Angular, навигация выполнялась из гарда, который первый сделает редирект. И выходило так что пользователь, который не залогинен, может попасть на страницу “Вы не админ”, потому что гард админа выполнился быстрее.

Но теперь мы имеем UrlTree и приоритет гардов. Если гард аутентификации важнее, то ставим его первым в массив, тем самым даем ему более высокий приоритет, тогда роутер будет ждать его выполнения и проводить редирект на UrlTree из него.

Краткие итоги

Наделяйте гарды только одной ответственностью, тогда их легче читать и переиспользовать. Но помните про ловушку приоритетов. Если у вас первый гард выполняется за секунду, а второй за час, и нет разницы по редиректу, то стоит подумать о порядке в массиве CanActivate/CanActivateChild/CanLoad/CanDeactivate, чтобы иногда пользователь не ждал один час.

Я написал свое решение для обхода приоритета гардов, потому что меня категорично не устраивало заставлять пользователя ждать, но об этом в следующей статьей.

Комментарии (0)