Перейти к содержанию

Маршрутизация. Route guards

Route Guards позволяют ограничить доступ к маршрутам на основе определенного условия, например, только авторизованные пользователи с определенным набором прав могут просматривать страницу.

Также можно перед переходом на другой URL предупредить пользователя, что все его несохраненные изменения на текущей странице будут потеряны.

Различают следующие виды guard-ов:

  • CanActivate - разрешает/запрещает доступ к маршруту;
  • CanActivateChild -разрешает/запрещает доступ к дочернему маршруту;
  • CanDeactivate - разрешает/запрещает уход с текущего маршрута;
  • Resolve - выполняет какое-либо действие перед переходом на маршрут, обычно ожидает данные от сервера;
  • CanLoad - разрешает/запрещает загрузку модуля, загружаемого асинхронно.

Все guard-ы должны возвращать либо true, либо false. И происходить это может как в синхронном режиме (тип Boolean), так и в асинхронном режиме (Observable<boolean> или Promise<boolean>).

Если будет возвращено false, будет инициировано событие маршрутизации NavigationCancel.

У одного URL может быть одновременно несколько guard-ов, причем одного и того же типа. Все guard-ы - обычные классы, реализующие определенный интерфейс. Указываются они в виде массива значением одноименных свойств при определении маршрутов.

Сначала выполняются CanDeactivate и CanActivateChild, затем - CanActivate. CanLoad будет вызван только в случае асинхронной загрузки модуля.

Guard-ы импортируются в модуль, к которому относится модуль маршрутизации, и перечисляются в блоке providers.

auth.guard.ts

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(@Inject(AuthService) private auth: AuthService) {}

  canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.auth.isLoggedIn
  }

  canActivateChild(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.canActivate(next, state)
  }
}

app-routing.module.ts

const routes: Routes = [
  { path: 'login', component: LoginComponent },
  {
    path: 'pages',
    component: PagesComponent,
    canActivate: [AuthGuard],
    canActivateChild: [AuthGuard],
    children: [{ path: 'about', component: AboutComponent }, { path: 'contacts', component: ContactsComponent }]
  }
]

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

app.module.ts

providers: [AuthGuard]

В AuthGuard реализуется интерфейс CanActivate и CanActivateChild (создаваемый класс обязательно должен содержать определение метода CanActivate и CanActivateChild соответственно).

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

В приведенном примере предполагается, что при авторизации пользователя значение свойства isLoggedIn сервиса AuthService устанавливается в true.

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

can-deactivate.guard.ts

@Injectable()
export class DataChangesGuard implements CanDeactivate<BuyTicketFormComponent> {
  constructor() {}

  canDeactivate(component: BuyTicketFormComponent, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) {
    if (component.buyTicketForm.dirty) return window.confirm('Unsaved data detected. Want to exit?')
    else return true
  }
}

app-routing.module.ts

{
  path: 'ticket', component: BuyTicketFormComponent, canDeactivate: [DataChangesGuard]
}

В отличие от CanActivate CanDeactivate имеет дополнительно еще один параметр component, который содержит экземпляр указанного компонента, в нашем случае это BuyTicketFormComponent.

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

Если в приложении нескольким маршрутам необходим один и тот же CanDeactivate guard, то во избежание создания guard-а для каждого из компонентов, можно создать интерфейс и указывать его вместо компонента при создании guard-а.

export interface CanComponentDeactivate {
  canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean
}

@Injectable({ providedIn: 'root' })
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate) {
    return component.canDeactivate ? component.canDeactivate() : true
  }
}

При обращении приложения к удаленному API, отображение данных происходит с некоторой задержкой, во время которой интерфейс может оказаться "сломанным", что сразу негативно скажется на общем впечатлении.

Для решения этой проблемы можно было бы использовать CanActivate, но концептуально он предназначен для других целей. А здесь понадобится resolver.

Resolver - это сервис, реализующий интерфейс Resolve, а именно метод resolve(), который обязательно должен возвращать данные типа Observable. Указанный для любого маршрута, Resolver разрешает переход на него после выполнения Observable в resolve().

@Injectable()
export class ContactsResovler implements Resolve<any> {
  constructor(private http: HttpClient, private router: Router) {}

  resolve(route: ActivatedRouteSnapshot): Observable<any> {
    return this.http.get('/api/contacts').pipe(
      tap(
        res => of(res),
        err => {
          this.router.navigate(['/'])
          return EMPTY
        }
      )
    )
  }
}

Выполнение метода resolve() (как синхронного, так и асинхронного) инициирует событие NavigationEnd, что можно использовать для скрытия прелоадера при переходах между страницами.