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

от автора

Столкнулся с необходимостью перезапустить 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();   }); } 


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

ссылка на оригинал статьи https://habr.com/ru/post/517958/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *