Angular IDPStrong Junior
🔥 0

Модуль 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']);
};

≈ 45 хв · +25 XP за урок