Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Angular router: маршрутизація, охоронці та ледача завантаження». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Angular Router** зв'язує URL-шляхи з компонентами, контролює доступ через guards і завантажує модулі на вимогу за допомогою lazy loading. ```typescript { path: 'admin', canMatch: [authGuard], loadChildren: () => import('./admin') } ``` **Ключове:** для lazy-маршрутів використовуй `canMatch`, а не `canActivate` - він блокує завантаження чанку до його старту, а не лише рендер компонента.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Angular Router** зв'язує URL-шляхи з компонентами, контролює доступ через guards і завантажує модулі на вимогу за допомогою lazy loading. ## Теорія ### TL;DR - Router - як готельна стійка реєстрації: зіставляє URL (номер кімнати) з компонентом (кімнатою), перевіряє токен (guard) перед входом, завантажує поверхи (модулі) лише коли гість прибуває - Головна різниця: eager loading пакує все в один файл при старті; lazy loading ділить код на окремі чанки, які завантажуються при переході на відповідний маршрут - `loadComponent` - для одного standalone-компонента, `loadChildren` - для цілого файлу маршрутів - Wildcard `{ path: '**' }` завжди повинен бути останнім - він ловить усе - `canMatch` (Angular 14+) блокує завантаження чанку повністю; `canActivate` лише блокує рендер ### Швидкий приклад ```typescript // app.routes.ts export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'admin', canMatch: [authGuard], // блокує завантаження для неавторизованих loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES) }, { path: '**', redirectTo: '' } // wildcard завжди останній ]; ``` ```html <a routerLink="/" routerLinkActive="active">Головна</a> <router-outlet></router-outlet> ``` Переходиш на `/admin` авторизованим: адмін-чанк завантажується, guard пропускає, компонент рендериться. Переходиш без авторизації: guard спрацьовує, редирект на `/login`, чанк так і не завантажується. ### Lazy loading проти eager loading Eager loading пакує кожен компонент в один JS-файл при старті. 2MB бандл їде при кожному першому завантаженні, навіть якщо користувач ніколи не відкриє адмін-панель. Lazy loading ділить це на `main.js` (наприклад 100KB) і `admin.js` (300KB), який завантажується лише при переході на `/admin`. Для будь-якого застосунку з більш ніж 10-15 маршрутами різниця у часі першого відображення стає помітною. `loadComponent` вказує на один standalone-компонент. `loadChildren` вказує на файл маршрутів з кількома sub-маршрутами, їхніми компонентами та guards. Використовуй `loadChildren` коли фіча має власне дерево маршрутів. ### Параметри маршруту ```typescript export class UserDetailComponent { private route = inject(ActivatedRoute); ngOnInit() { // Observable - правильно обробляє повторну навігацію на той самий компонент this.route.paramMap.subscribe(params => { const id = params.get('id')!; this.loadUser(id); }); } } ``` `snapshot.paramMap.get('id')` читає значення один раз при ініціалізації. Якщо користувач переходить з `/users/1` на `/users/2` і Angular перевикористовує той самий екземпляр компонента, `ngOnInit` більше не викликається і snapshot не оновлюється. Observable обробляє це коректно. ### Guards Guards виконують перевірки перед активацією маршруту. З Angular 14+ замість класових guards використовуються функціональні, що звертаються до сервісів через `inject()`. ```typescript // auth.guard.ts export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router); return authService.isLoggedIn() ? true : router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } }); }; ``` Три типи guards, які зустрічаються найчастіше: - `canActivate`: запускається перед рендером компонента. Стандартна перевірка авторизації. - `canDeactivate`: запускається при виході з маршруту. Використовується для попередження про незбережені зміни у формі. - `canMatch`: запускається перед завантаженням lazy-чанку. Краще ніж `canActivate` для захищених lazy-маршрутів - неавторизовані користувачі взагалі не завантажують код модуля. ```typescript // canDeactivate приклад export const unsavedChangesGuard: CanDeactivateFn<EditFormComponent> = (component) => { if (component.hasUnsavedChanges()) { return confirm('Покинути без збереження?'); } return true; }; ``` ### Resolvers Resolver отримує дані перед рендером маршруту. Компонент отримує дані вже готовими через `ActivatedRoute.snapshot.data`. ```typescript // user.resolver.ts export const userResolver: ResolveFn<User> = (route) => { return inject(UserService).getUserById(route.paramMap.get('id')!); }; // маршрути { path: 'users/:id', component: UserDetailComponent, resolve: { user: userResolver } } // компонент - дані вже готові export class UserDetailComponent { user = inject(ActivatedRoute).snapshot.data['user'] as User; } ``` Компроміс реальний: resolver затримує навігацію до отримання відповіді від API. На повільній мережі користувач довше дивиться на попередню сторінку. В більшості продакшн-проектів, де мені доводилось працювати, показ скелетону всередині компонента давав кращий UX - навігація відчувалась миттєвою, а завантаження відбувалось у фоні. ### Вкладені маршрути ```typescript { path: 'settings', component: SettingsLayoutComponent, children: [ { path: '', redirectTo: 'profile', pathMatch: 'full' }, { path: 'profile', component: ProfileSettingsComponent }, { path: 'security', component: SecuritySettingsComponent }, ] } ``` `SettingsLayoutComponent` повинен мати власний `<router-outlet>` для рендеру дочірніх компонентів. Outlet на рівні застосунку рендерить `SettingsLayoutComponent`, а вкладений outlet рендерить активну вкладку налаштувань. ### Програмна навігація ```typescript export class AppComponent { private router = inject(Router); goToUser(id: string) { this.router.navigate(['/users', id]); } search(term: string) { this.router.navigate(['/search'], { queryParams: { q: term, page: 1 } }); } } ``` ### Типові помилки **Читання параметрів через snapshot у компоненті з повторною навігацією.** ```typescript // неправильно - ламається при переході з /users/1 на /users/2 ngOnInit() { const id = this.route.snapshot.paramMap.get('id'); } // правильно ngOnInit() { this.route.paramMap.subscribe(params => { this.id = params.get('id')!; }); } ``` Angular може перевикористати екземпляр компонента при навігації по тому ж маршруту. Snapshot не оновлюється. Observable - так. **Відсутність `pathMatch: 'full'` на порожніх маршрутах-редиректах.** ```typescript // неправильно - /home теж збігається з '' за prefix-стратегією { path: '', redirectTo: 'home' } // правильно { path: '', redirectTo: 'home', pathMatch: 'full' } ``` Стратегія збігу за замовчуванням - префіксна, тому порожній шлях збігається з будь-яким URL. Це найпоширеніше питання по Angular-роутингу на Stack Overflow. **Використання `canActivate` замість `canMatch` на lazy-маршрутах.** ```typescript // неправильно - чанк завантажується до запуску guard { path: 'admin', loadChildren: () => import('./admin'), canActivate: [authGuard] } // правильно - guard спрацьовує перед завантаженням { path: 'admin', canMatch: [authGuard], loadChildren: () => import('./admin') } ``` **Wildcard у неправильному місці.** ```typescript // неправильно - wildcard перехоплює все, інші маршрути не досягаються { path: '**', component: NotFoundComponent }, { path: 'home', component: HomeComponent }, // мертвий код // правильно { path: 'home', component: HomeComponent }, { path: '**', component: NotFoundComponent }, ``` ### Де застосовується - E-commerce checkout: `canActivate` перевіряє, чи є товари у кошику перед переходом на `/checkout` - Адмін-панелі: `canMatch` блокує завантаження адмін-бандлу для звичайних користувачів - Сторінки редагування профілю: `canDeactivate` запобігає втраті даних форми при випадковому переході - Багатокрокові майстри (wizards): resolvers завантажують необхідні дані перед першим кроком - Великі SPA (патерн NGX-Admin): вкладені lazy-маршрути для шляхів на кшталт `/crm/clients/:id/orders` ### Питання на співбесіді **Q:** У чому різниця між `canActivate` і `canMatch`? **A:** `canActivate` запускається після завантаження lazy-чанку. `canMatch` - до. Для захищених lazy-маршрутів `canMatch` є кращим вибором: неавторизовані користувачі взагалі не завантажують код фічі. **Q:** Коли варто використовувати resolver замість завантаження даних всередині компонента? **A:** Resolver підходить, коли дані потрібні до рендеру - наприклад, заголовок сторінки або структура layout залежать від отриманих даних. Завантаження всередині компонента краще, коли API повільний і потрібно показати скелетон одразу. **Q:** Як Angular обробляє оновлення сторінки (F5) на lazy-маршруті? **A:** Router ініціалізується з поточного URL через `APP_BASE_HREF`, повторно завантажує відповідний lazy-чанк і виконує повний процес активації включно з guards. Без SSR нічого додаткового робити не потрібно. **Q:** Навіщо потрібен патерн `forRoot`/`forChild` в NgModule-застосунках? **A:** `RouterModule.forRoot()` реєструє провайдери роутера глобально і викликається рівно один раз. `RouterModule.forChild()` додає маршрути без повторної реєстрації провайдерів. Виклик `forRoot` всередині lazy-модуля створює другий екземпляр роутера і ламає навігацію. **Q:** Як налагодити проблеми зі збігом маршрутів? **A:** Додай `enableTracing: true` до `RouterModule.forRoot()` або `withDebugTracing()` в standalone-конфігурації. Router логуватиме кожен крок зіставлення в консоль - які маршрути перевірялись, що збіглося і чому решта була відхилена. ## Приклади ### Базове налаштування маршрутизації ```typescript export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'about', component: AboutComponent }, { path: 'users/:id', component: UserDetailComponent }, { path: '**', component: NotFoundComponent }, // обов'язково останній ]; ``` ```html <nav> <a routerLink="/" routerLinkActive="active">Головна</a> <a routerLink="/about" routerLinkActive="active">Про нас</a> <a [routerLink]="['/users', user.id]">Профіль</a> </nav> <router-outlet></router-outlet> ``` `routerLinkActive` додає CSS-клас, коли шлях посилання збігається з поточним URL. Синтаксис `[routerLink]` з масивом зручний для динамічних сегментів - не потрібно склеювати рядки. ### Auth guard із ледаче завантаженим адмін-модулем ```typescript // auth.guard.ts export const authGuard: CanActivateFn = (route, state) => { const authService = inject(AuthService); const router = inject(Router); return authService.isLoggedIn() ? true : router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } }); }; // app.routes.ts export const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'login', component: LoginComponent }, { path: 'admin', canMatch: [authGuard], loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES), }, ]; // admin/admin.routes.ts export const ADMIN_ROUTES: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, { path: 'dashboard', loadComponent: () => import('./dashboard.component').then(m => m.DashboardComponent), }, ]; ``` Неавторизовані користувачі при переході на `/admin` отримують редирект на `/login?returnUrl=/admin`. Адмін-бандл так і не завантажується. Після входу читаємо `returnUrl` з query params і відправляємо користувача туди, куди він йшов. ### Resolver із вкладеними маршрутами та canDeactivate ```typescript // user.resolver.ts export const userResolver: ResolveFn<User> = (route) => { return inject(UserService).getUser(route.paramMap.get('id')!); }; // unsaved-changes.guard.ts export const unsavedChangesGuard: CanDeactivateFn<UserEditComponent> = (component) => { return component.hasUnsavedChanges() ? confirm('Покинути без збереження?') : true; }; // user.routes.ts export const USER_ROUTES: Routes = [ { path: ':id', component: UserShellComponent, resolve: { user: userResolver }, children: [ { path: '', redirectTo: 'overview', pathMatch: 'full' }, { path: 'overview', component: UserOverviewComponent }, { path: 'edit', component: UserEditComponent, canDeactivate: [unsavedChangesGuard] }, ], }, ]; // user-shell.component.ts export class UserShellComponent { user = inject(ActivatedRoute).snapshot.data['user'] as User; // дані готові одразу, стан завантаження не потрібен } ``` `UserShellComponent` відображає ім'я користувача в спільному заголовку, який бачать обидва дочірні маршрути. Resolver отримує дані до завершення навігації. Guard `canDeactivate` на маршруті редагування запитує підтвердження при виході, якщо форма містить незбережені зміни.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.