17. Route Guards
🛡️ Route Guards в Angular
Заголовок раздела «🛡️ Route Guards в Angular»Route Guards — это охранники маршрутов. Представь, что маршруты в твоём приложении — это двери в здании 🏢. Guards — это охрана: они проверяют, есть ли у пользователя пропуск, прежде чем пустить его. И не только “пустить”, но и “выпустить” (CanDeactivate) 🚪.
🤔 Зачем нужны Guards?
Заголовок раздела «🤔 Зачем нужны Guards?»Пользователь хочет зайти на /admin/dashboard ↓[CanActivate: authGuard] ← авторизован? ↓[CanActivate: roleGuard] ← есть роль admin? ↓[Resolve: dashboardData] ← загружаем данные ↓Компонент отображается ✅Guards позволяют:
- Защитить маршруты от неавторизованных пользователей
- Запросить подтверждение при уходе со страницы (несохранённые данные)
- Загрузить данные до отображения компонента (Resolver)
- Перенаправить пользователя на нужную страницу
🔐 CanActivate: блокируем вход
Заголовок раздела «🔐 CanActivate: блокируем вход»Функциональный Guard (Angular 15+, рекомендуется):
import { inject } from '@angular/core';import { CanActivateFn, Router } from '@angular/router';import { AuthService } from './auth.service';
export const authGuard: CanActivateFn = (route, state) => { const auth = inject(AuthService); const router = inject(Router);
if (auth.isLoggedIn()) { return true; // ✅ разрешаем навигацию }
// Сохраняем URL для редиректа после логина router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); return false; // ❌ блокируем навигацию};
// Или возвращаем UrlTree (более элегантно!)export const authGuard: CanActivateFn = (route, state) => { const auth = inject(AuthService); const router = inject(Router);
return auth.isLoggedIn() ? true : router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });};
// Подключение в маршруте{ path: 'dashboard', component: DashboardComponent, canActivate: [authGuard] }Классовый Guard (устарел, но встречается в старых проектах):
// ❌ Устаревший подход (Angular < 15)@Injectable({ providedIn: 'root' })export class AuthGuard implements CanActivate { constructor(private auth: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (this.auth.isLoggedIn()) return true; this.router.navigate(['/login']); return false; }}Guard с проверкой роли:
export const roleGuard = (requiredRole: string): CanActivateFn => { return () => { const auth = inject(AuthService); const router = inject(Router); const user = auth.currentUser();
if (!user) return router.createUrlTree(['/login']); if (user.role !== requiredRole) return router.createUrlTree(['/forbidden']); return true; };};
// Использование{ path: 'admin', component: AdminComponent, canActivate: [authGuard, roleGuard('admin')] }🚪 CanDeactivate: блокируем выход
Заголовок раздела «🚪 CanDeactivate: блокируем выход»Идеально для форм с несохранёнными данными. Guard спрашивает пользователя перед уходом:
export interface HasUnsavedChanges { hasUnsavedChanges(): boolean;}
export const unsavedChangesGuard: CanDeactivateFn<HasUnsavedChanges> = (component) => { if (!component.hasUnsavedChanges()) return true;
return confirm('У вас есть несохранённые изменения. Уйти?'); // В реальном приложении лучше использовать Modal Dialog};
// Компонент реализует интерфейс@Component({...})export class EditProfileComponent implements HasUnsavedChanges { form = this.fb.group({ name: [''], email: [''] }); private savedValue = this.form.value;
hasUnsavedChanges(): boolean { return JSON.stringify(this.form.value) !== JSON.stringify(this.savedValue); }
save() { this.savedValue = this.form.value; this.http.put('/api/profile', this.form.value).subscribe(); }}
// В маршруте{ path: 'profile/edit', component: EditProfileComponent, canDeactivate: [unsavedChangesGuard]}👨👩👧 CanActivateChild: защита дочерних маршрутов
Заголовок раздела «👨👩👧 CanActivateChild: защита дочерних маршрутов»Защищает сразу все дочерние маршруты родителя — не нужно добавлять guard к каждому:
export const adminGuard: CanActivateChildFn = (childRoute, state) => { return inject(AuthService).hasAdminAccess() ? true : inject(Router).createUrlTree(['/forbidden']);};
// Применяется ко ВСЕМ children{ path: 'admin', component: AdminLayoutComponent, canActivateChild: [adminGuard], // Защищает /admin/*, /admin/*/*, etc. children: [ { path: 'dashboard', component: DashboardComponent }, { path: 'users', component: UsersComponent }, { path: 'settings', component: SettingsComponent }, // ^ Все они защищены adminGuard! ]}🎯 CanMatch: фильтрация маршрутов
Заголовок раздела «🎯 CanMatch: фильтрация маршрутов»CanMatch заменяет устаревший CanLoad. Он запрещает совпадение маршрута — если guard возвращает false, Angular переходит к следующему маршруту в конфигурации:
export const premiumGuard: CanMatchFn = () => { return inject(UserService).isPremium();};
// Конфигурация маршрутовconst routes: Routes = [ // Для premium пользователей — расширенный компонент { path: 'dashboard', component: PremiumDashboardComponent, canMatch: [premiumGuard] }, // Для обычных пользователей — базовый компонент (URL тот же!) { path: 'dashboard', component: BasicDashboardComponent }];🗄️ Resolve: данные до активации
Заголовок раздела «🗄️ Resolve: данные до активации»Resolve загружает данные ДО навигации — компонент уже получает готовые данные:
export const userResolver: ResolveFn<User> = (route) => { const id = route.paramMap.get('id')!; const service = inject(UserService); const router = inject(Router);
return service.getUser(id).pipe( catchError(() => { router.navigate(['/not-found']); return EMPTY; }) );};
{ path: 'users/:id', component: UserComponent, resolve: { user: userResolver } }
// Компонент — данные уже есть!export class UserComponent { user = inject(ActivatedRoute).snapshot.data['user'] as User;}🏗️ Полный пример: AuthService + authGuard
Заголовок раздела «🏗️ Полный пример: AuthService + authGuard»@Injectable({ providedIn: 'root' })export class AuthService { private currentUser$ = new BehaviorSubject<User | null>(null);
isLoggedIn(): boolean { return this.currentUser$.getValue() !== null; }
currentUser(): User | null { return this.currentUser$.getValue(); }
login(username: string, password: string): Observable<User> { return this.http.post<User>('/api/login', { username, password }).pipe( tap(user => { this.currentUser$.next(user); localStorage.setItem('token', user.token); }) ); }
logout() { this.currentUser$.next(null); localStorage.removeItem('token'); this.router.navigate(['/login']); }}
// auth.guard.tsexport const authGuard: CanActivateFn = (route, state) => { const auth = inject(AuthService); const router = inject(Router);
if (auth.isLoggedIn()) return true;
// Запоминаем куда хотел попасть пользователь return router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });};
// login.component.ts — редирект после логина@Component({...})export class LoginComponent { private returnUrl = inject(ActivatedRoute).snapshot.queryParams['returnUrl'] ?? '/';
login() { this.auth.login(this.form.value).subscribe(() => { this.router.navigateByUrl(this.returnUrl); }); }}⚖️ Порядок выполнения Guards
Заголовок раздела «⚖️ Порядок выполнения Guards»Навигация: /dashboard ↓1. CanDeactivate (текущего компонента) ← "Можно уйти?" ↓2. CanMatch (нового маршрута) ← "Маршрут совпадает?" ↓3. CanActivateChild (родителя) ← "Дочерний маршрут доступен?" ↓4. CanActivate (нового компонента) ← "Компонент доступен?" ↓5. Resolve (загрузка данных) ← "Данные готовы?" ↓6. Компонент активируется ✅
⚠️ Если несколько CanActivate:canActivate: [authGuard, roleGuard('admin')]→ Выполняются параллельно (если не зависят друг от друга)→ Если любой вернёт false — навигация отменяетсяПрактика
Заголовок раздела «Практика»Попробуйте концепцию в интерактивном редакторе: