Angular 9. Перезапуск guard-ов текущей страницы. Trigger current route guards

в 16:14, , рубрики: angular, guard, onSameUrlNavigation, route, runGuardsAndResolvers

Столкнулся с необходимостью перезапустить guard-ы для текущей страницы, вне зависимости от того какая страница открыта.

Стандартного решения не нашел, а предлагаемые в интернете ограничиваются одной страницей. Поэтому написал своё и решил им поделиться.

Описание кейса

Страницы приложения разделяются на 3 группы:

  • Только для авторизованных пользователей
  • Только для неавторизованных пользователей
  • Для любых пользователей

Авторизоваться или выйти можно на любой странице.

Если вход/выход производится на странице с ограниченным доступом, то нужно перейти на разрешенную страницу.

Если на странице без ограничений, то нужно остаться на текущей странице.

Для лучшего понимания желательно знать о:

Решение

Разграничение прав доступа к страницам осуществляется через guard — CanActivate. Проверка осуществляется при перехеде на страницу, но не реагирует на изменение прав, когда переход уже осуществлен. Во избежании дублирования логики по правам доступа, принудительно перезапускаем guard-ы.

Перезапуска guard-ов для текущей страницы осуществляется за счет навигации по текущему url. И изменения стратегий Router.onSameUrlNavigation и Route.runGuardsAndResolvers.

Здесь готовое решение. Более детально в следующем разделе.

  import { Injectable } from '@angular/core';
  import { ActivatedRoute, PRIMARY_OUTLET, Router, RunGuardsAndResolvers } from '@angular/router';

  @Injectable()
  export class GuardControlService {
    constructor(
      private route: ActivatedRoute,
      private router: Router,
    ) {}

    /**
    * Принудительный запуск guard-ов текущего url
    */
    forceRunCurrentGuards(): void {
      // Изменяем стратегию Router.onSameUrlNavigation на чувствительную к навигации на текущий url
      const restoreSameUrl = this.changeSameUrlStrategy(this.router, 'reload');

      // Получаем текущий ActivatedRoute для primary outlet
      const primaryRoute: ActivatedRoute = this.getLastRouteForOutlet(this.route.root, PRIMARY_OUTLET);

      // Изменяем стратегию runGuardsAndResolvers для ActivatedRoute и его предков на чувствительную к навигации на текущий url
      const restoreRunGuards = this.changeRunGuardStrategies(primaryRoute, 'always');

      // Запуск события навигации
      this.router.navigateByUrl(
        this.router.url
      ).then(() => {
        // Восстановление onSameUrlNavigation
        restoreSameUrl();
        // Восстановление runGuardsAndResolvers
        restoreRunGuards();
      });
    }

    /**
    * Изменение onSameUrlNavigation с сохранением текущего значения
    * @param router - Router, для которого осуществляется замена
    * @param strategy - новая стратегия
    * @return callback для восстановления значения
    */
    private changeSameUrlStrategy(router: Router, strategy: 'reload' | 'ignore'): () => void {
      const onSameUrlNavigation = router.onSameUrlNavigation;
      router.onSameUrlNavigation = strategy;

      return () => {
        router.onSameUrlNavigation = onSameUrlNavigation;
      }
    }

    /**
    * Получение последнего route для outlet-а
    * @param route - Route относительно которого осуществляется поиск
    * @param outlet - имя outlet-а, по которому осуществляется поиск
    * @return Текущий ActivatedRoute для заданного outlet
    */
    private getLastRouteForOutlet(route: ActivatedRoute, outlet: string): ActivatedRoute {
      if (route.children?.length) {
        return this.getLastRouteForOutlet(
          route.children.find(item => item.outlet === outlet),
          outlet
        );
      } else {
        return route;
      }
    }

    /**
    * Изменение runGuardsAndResolvers для ActivatedRoute и его предков, с сохранением текущих значений
    * @param route - ActivatedRoute для которого осуществляется замена
    * @param strategy - новая стратегия
    * @return callback для восстановления значения
    */
    private changeRunGuardStrategies(route: ActivatedRoute, strategy: RunGuardsAndResolvers): () => void {
      const routeConfigs = route.pathFromRoot
        .map(item => {
          if (item.routeConfig) {
            const runGuardsAndResolvers = item.routeConfig.runGuardsAndResolvers;
            item.routeConfig.runGuardsAndResolvers = strategy;
              return runGuardsAndResolvers;
              } else {
            return null;
          }
        });

      return () => {
        route.pathFromRoot
          .forEach((item, index) => {
            if (item.routeConfig) {
              item.routeConfig.runGuardsAndResolvers = routeConfigs[index];
            }
          });
      }
    }
  }
  

Дополнительное описание решения

Первое, что хочется попробовать для перезапуска guard-ов — использовать навигацию по текущему url.

this.router.navigateByUrl(this.router.url);

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

Настройка маршрутизации

1. Изменить стратегию Router.onSameUrlNavigation

onSameUrlNavigation может принимать следующие значения:

onSameUrlNavigation: 'reload' | 'ignore';

Для чувствительности к переходу по текущему url нужно установить 'reload'.

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

this.router.events.subscribe();

2. Изменить стратегию Route.runGuardsAndResolvers

runGuardsAndResolvers может принимать следующие значения:

type RunGuardsAndResolvers = 'pathParamsChange' | 'pathParamsOrQueryParamsChange' | 'paramsChange' | 'paramsOrQueryParamsChange' | 'always' | ((from: ActivatedRouteSnapshot, to: ActivatedRouteSnapshot) => boolean);

Для чувствительности к переходу по текущему url нужно установить 'always'.

Настройка маршрутизации во время конфигурации приложения

onSameUrlNavigation:

const routes: : Route[] = [];
@NgModule({
  imports: [
    RouterModule.forRoot(
      routes,
      { onSameUrlNavigation: 'reload' }
    )
  ]
})

runGuardsAndResolvers:

const routes: Route[] = [
  {
    path: '',
    component: AppComponent,
    runGuardsAndResolvers: 'always',
  }
];

Настройка маршрутизации во время исполнения

constructor(
  private router: Router,
  private route: ActivatedRoute
) {
  this.router.onSameUrlNavigation = 'reload';
  this.route.routeConfig.runGuardsAndResolvers = 'always';
}

Перезапуск guard-ов

Для перезапуска guard-ов одной определенной страницы достаточно настроить маршрутизацию во время конфигурации.

Но для перезапука guard-ов любой страницы, изменения runGuardsAndResolvers в каждом Route приведут к лишним проверкам. А необходимость всегда помнить об этом параметре — к ошибкам.

Так как наш кейс предполагает перезапуск для любой страницы без ограничений в настройке приложения, нужно:

1. Заменить onSameUrlNavigation и сохранить текущее значение

// Изменяем стратегию Router.onSameUrlNavigation на чувствительную к навигации на текущий url
const restoreSameUrl = this.changeSameUrlStrategy(this.router, 'reload');

...
/**
* Изменение onSameUrlNavigation с сохранением текущего значения
* @param router - Router, для которого осуществляется замена
* @param strategy - новая стратегия
* @return callback для восстановления значения
*/
private changeSameUrlStrategy(router: Router, strategy: 'reload' | 'ignore'): () => void {
  const onSameUrlNavigation = router.onSameUrlNavigation;
  router.onSameUrlNavigation = strategy;

  return () => {
    router.onSameUrlNavigation = onSameUrlNavigation;
  }
}

2. Получить ActivatedRoute для текущего url

Так как inject ActivatedRoute осуществляется в сервисе, полученниый ActivatedRoute не связан с текущим url.

ActivatedRoute для текущего url лежит в последнем primary outlet и его нужно найти:

// Получаем текущий ActivatedRoute для primary outlet
const primaryRoute: ActivatedRoute = this.getLastRouteForOutlet(this.route.root, PRIMARY_OUTLET);

...
/**
* Получение последнего route для outlet-а
* @param route - Route относительно которого осуществляется поиск
* @param outlet - имя outlet-а, по которому осуществляется поиск
* @return Текущий ActivatedRoute для заданного outlet
*/
private getLastRouteForOutlet(route: ActivatedRoute, outlet: string): ActivatedRoute {
  if (route.children?.length) {
    return this.getLastRouteForOutlet(
      route.children.find(item => item.outlet === outlet),
      outlet
   );
  } else {
    return route;
  }
}

3. Заменить runGuardsAndResolvers для всех ActivatedRoute и его предков, с сохранение текущих значений

Guard, ограничивающий доступ, может располагаться в любом из предков текущего ActivatedRoute. Все предки располагаются в pathFromRoot.

// Изменяем стратегию runGuardsAndResolvers для ActivatedRoute и его предков на чувствительную к навигации на текущий url
const restoreRunGuards = this.changeRunGuardStrategies(primaryRoute, 'always');

...
/**
* Изменение runGuardsAndResolvers для ActivatedRoute и его предков, с сохранением текущих значений
* @param route - ActivatedRoute для которого осуществляется замена
* @param strategy - новая стратегия
* @return callback для восстановления значения
*/
private changeRunGuardStrategies(route: ActivatedRoute, strategy: RunGuardsAndResolvers): () => void {
  const routeConfigs = route.pathFromRoot
    .map(item => {
      if (item.routeConfig) {
        const runGuardsAndResolvers = item.routeConfig.runGuardsAndResolvers;
        item.routeConfig.runGuardsAndResolvers = strategy;
        return runGuardsAndResolvers;
      } else {
        return null;
      }
    });

  return () => {
    route.pathFromRoot
      .forEach((item, index) => {
        if (item.routeConfig) {
          item.routeConfig.runGuardsAndResolvers = routeConfigs[index];
        }
      });
  }
}

4. Перейти по текущему url

this.router.navigateByUrl(this.router.url);

5. Вернуть runGuardsAndResolvers и onSameUrlNavigation в исходное состояние

restoreRunGuards();
restoreSameUrl();

6. Объединить этапы в одной функции

constructor(
  private route: ActivatedRoute,
  private router: Router,
) {}

/**
* Принудительный запуск guard-ов текущего url
*/
forceRunCurrentGuards(): void {
  // Изменяем стратегию Router.onSameUrlNavigation на чувствительную к навигации на текущий url
  const restoreSameUrl = this.changeSameUrlStrategy(this.router, 'reload');

  // Получаем текущий ActivatedRoute для primary outlet
  const primaryRoute: ActivatedRoute = this.getLastRouteForOutlet(this.route.root, PRIMARY_OUTLET);

  // Изменяем стратегию runGuardsAndResolvers для ActivatedRoute и его предков на чувствительную к навигации на текущий url
  const restoreRunGuards = this.changeRunGuardStrategies(primaryRoute, 'always');

  // Запуск события навигации
  this.router.navigateByUrl(
    this.router.url
  ).then(() => {
    // Восстановление onSameUrlNavigation
    restoreSameUrl();
    // Восстановление runGuardsAndResolvers
    restoreRunGuards();
  });
}


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

Автор: oborodin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js