Модуль 2 · урок 7 з 15
Guards
Guard — це функція, яка вирішує, пускати користувача на маршрут чи ні. У таблиці ліда
вони названі прямо: CanActivate і CanDeactivate. Перший захищає вхід,
другий — вихід, і саме другий рятує від втрати заповненої форми.
CanActivate: пускати чи ні
// core/guards/role.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from '../auth/auth.service';
export const roleGuard: CanActivateFn = (route) => {
const auth = inject(AuthService);
const router = inject(Router);
const required = route.data['role'] as string | undefined;
if (!required || auth.hasRole(required)) {
return true; // пускаємо
}
return router.createUrlTree(['/forbidden']); // не пускаємо і кажемо куди
};
// у маршрутах
{
path: 'admin',
canActivate: [roleGuard],
data: { role: 'producer' },
loadComponent: () => import('./features/admin/admin').then(m => m.Admin),
}
Guard — звичайна функція, тому в ній працює inject(). Повернути можна три речі:
| Що повернути | Що станеться |
|---|---|
true | Перехід відбувається |
false | Перехід скасовано, користувач лишається де був — і не розуміє чому |
UrlTree | Перехід скасовано й одразу зроблено редірект — правильний варіант |
Observable або Promise будь-чого з цього | Роутер зачекає на результат |
createUrlTree, а не router.navigate
Якщо всередині guard викликати navigate() і повернути false,
вийде дві навігації одночасно: одна скасовується, друга стартує. Інколи це працює, інколи
дає гонку й дивні стрибки адреси. Повернути UrlTree — це сказати роутеру
«замість цього переходу зроби ось цей», і він розрулить сам.
CanDeactivate: не дати втратити введене
// core/guards/unsaved-changes.guard.ts
import { CanDeactivateFn } from '@angular/router';
export interface HasUnsavedChanges {
hasUnsavedChanges(): boolean;
}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => {
if (!component.hasUnsavedChanges()) {
return true;
}
return confirm('У формі є незбережені зміни. Вийти без збереження?');
};
// компонент просто виконує контракт інтерфейсу
export class TaskForm implements HasUnsavedChanges {
private form = /* … */;
hasUnsavedChanges(): boolean {
return this.form.dirty && !this.saved();
}
}
{
path: 'tasks/:id/edit',
canDeactivate: [unsavedChangesGuard],
loadComponent: () => import('./features/tasks/task-form').then(m => m.TaskForm),
}
Зверни увагу на дженерик CanDeactivateFn<HasUnsavedChanges>: guard не знає
нічого про конкретний компонент, він знає лише, що той уміє відповісти на одне питання.
Це і є та сама типізація через інтерфейс, якої чекає лід — і водночас приклад на generics
із наступних уроків.
confirm()
Системне вікно тут — навмисне спрощення для першої версії. У модулі 6 ти заміниш його на
нормальний діалог, і guard поверне Observable<boolean> — роутер зачекає на
відповідь користувача. Логіка самого guard при цьому не зміниться.
Інші види
| Guard | Коли викликається | Типове застосування |
|---|---|---|
CanActivate | Перед входом на маршрут | Автентифікація, ролі |
CanActivateChild | Перед входом на будь-який дочірній | Захистити цілий розділ одним правилом |
CanDeactivate | Перед виходом | Незбережені зміни |
CanMatch | Перед тим, як маршрут узагалі розглянуть | Різні компоненти на одну адресу за фіче-прапорцем; не вантажить чужий бандл |
CanMatch — найцікавіший із рідковживаних: він відсіює маршрут до лінивого
завантаження, тож код захищеного екрана навіть не потрапить у браузер. Для адмінки це не просто
зручність, а питання того, що взагалі можна побачити в бандлі.
Guard — це чиста функція
Найкорисніша звичка: винести саме рішення в окрему функцію без Angular, а guard лишити тонкою обгорткою. Тоді логіку можна протестувати без TestBed — і в модулі 8 це буде найпростіший із твоїх тестів.
// чиста логіка — жодного Angular
export function decideAccess(userRoles: string[], required?: string): boolean {
if (!required) return true;
return userRoles.includes(required);
}
// guard лише дістає залежності й делегує
export const roleGuard: CanActivateFn = (route) => {
const auth = inject(AuthService);
const router = inject(Router);
return decideAccess(auth.roles(), route.data['role'])
? true
: router.createUrlTree(['/forbidden']);
};