Резолверы данных
Резолверы данных позволяют получать данные до перехода на маршрут, чтобы компоненты получили необходимые данные до отрисовки. Это помогает избежать состояний загрузки и улучшить взаимодействие с пользователем за счёт предварительной загрузки важных данных.
Что такое резолверы данных?
Резолвер данных — это сервис, реализующий функцию ResolveFn. Он запускается до активации маршрута и может получать данные из API, баз данных или других источников. Разрешённые данные становятся доступны компоненту через ActivatedRoute.
Резолверам данных доступны сервисы, предоставленные на уровне маршрута, а также сведения о конкретном маршруте через аргумент route.
Зачем использовать резолверы данных?
Резолверы данных помогают решать распространённые задачи маршрутизации:
- Предотвращение пустых состояний: компоненты сразу получают данные при загрузке
- Улучшение взаимодействия с пользователем: никаких индикаторов загрузки для критически важных данных
- Обработка ошибок: обработка ошибок при получении данных до перехода
- Согласованность данных: обеспечение наличия необходимых данных до отрисовки, что важно для SSR
Создание резолвера
Чтобы создать резолвер, напишите функцию с типом ResolveFn.
Она получает в качестве параметров ActivatedRouteSnapshot и RouterStateSnapshot.
Ниже приведён резолвер, который получает сведения о пользователе до отрисовки маршрута с помощью функции inject:
import {inject} from '@angular/core';
import {UserStore, SettingsStore} from './user-store';
import type {ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot} from '@angular/router';
import type {User, Settings} from './types';
export const userResolver: ResolveFn<User> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => {
const userStore = inject(UserStore);
const userId = route.paramMap.get('id')!;
return userStore.getUser(userId);
};
export const settingsResolver: ResolveFn<Settings> = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
) => {
const settingsStore = inject(SettingsStore);
const userId = route.paramMap.get('id')!;
return settingsStore.getUserSettings(userId);
}; Настройка маршрутов с резолверами
Чтобы добавить к маршруту один или несколько резолверов данных, укажите их в ключе resolve конфигурации маршрута. Тип Routes определяет структуру конфигураций маршрутов:
import {Routes} from '@angular/router';
export const routes: Routes = [
{
path: 'user/:id',
component: UserDetail,
resolve: {
user: userResolver,
settings: settingsResolver,
},
},
];
Подробнее о конфигурации resolve можно узнать в документации API.
Доступ к разрешённым данным в компонентах
Использование ActivatedRoute
Получить доступ к разрешённым данным в компоненте можно, обратившись к данным снимка из ActivatedRoute с помощью функции signal:
import {Component, inject, computed} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {toSignal} from '@angular/core/rxjs-interop';
import type {User, Settings} from './types';
@Component({
template: `
<h1>{{ user().name }}</h1>
<p>{{ user().email }}</p>
<div>Theme: {{ settings().theme }}</div>
`,
})
export class UserDetail {
private route = inject(ActivatedRoute);
private data = toSignal(this.route.data);
user = computed(() => this.data().user as User);
settings = computed(() => this.data().settings as Settings);
} Использование withComponentInputBinding
Другой способ получить доступ к разрешённым данным — использовать withComponentInputBinding() при настройке маршрутизатора с помощью provideRouter. Благодаря этому разрешённые данные передаются напрямую в качестве входных параметров компонента:
import {bootstrapApplication} from '@angular/platform-browser';
import {provideRouter, withComponentInputBinding} from '@angular/router';
import {routes} from './app.routes';
bootstrapApplication(App, {
providers: [provideRouter(routes, withComponentInputBinding())],
});
При такой конфигурации можно объявить в компоненте входные параметры, соответствующие ключам резолверов, используя функцию input и input.required для обязательных входных параметров:
import {Component, input} from '@angular/core';
import type {User, Settings} from './types';
@Component({
template: `
<h1>{{ user().name }}</h1>
<p>{{ user().email }}</p>
<div>Theme: {{ settings().theme }}</div>
`,
})
export class UserDetail {
user = input.required<User>();
settings = input.required<Settings>();
}
Этот подход обеспечивает более строгую типизацию и позволяет не внедрять ActivatedRoute только для доступа к разрешённым данным.
Обработка ошибок в резолверах
При сбоях навигации важно корректно обрабатывать ошибки в резолверах данных. В противном случае возникнет NavigationError, и переход на текущий маршрут завершится неудачей, что ухудшит взаимодействие с пользователями.
Есть три основных способа обрабатывать ошибки в резолверах данных:
- Централизация обработки ошибок с помощью
withNavigationErrorHandler - Обработка ошибок через подписку на события маршрутизатора
- Обработка ошибок непосредственно в резолвере
Централизация обработки ошибок с помощью withNavigationErrorHandler
Средство withNavigationErrorHandler позволяет централизованно обрабатывать все ошибки навигации, в том числе возникающие из-за сбоев резолверов данных. Этот подход позволяет хранить логику обработки ошибок в одном месте и не дублировать код обработки ошибок в разных резолверах.
import {bootstrapApplication} from '@angular/platform-browser';
import {provideRouter, withNavigationErrorHandler} from '@angular/router';
import {inject} from '@angular/core';
import {Router} from '@angular/router';
import {routes} from './app.routes';
bootstrapApplication(App, {
providers: [
provideRouter(
routes,
withNavigationErrorHandler((error) => {
const router = inject(Router);
if (error?.message) {
console.error('Navigation error occurred:', error.message);
}
router.navigate(['/error']);
}),
),
],
});
При такой конфигурации резолверы могут сосредоточиться на получении данных, а централизованный обработчик будет управлять сценариями ошибок:
export const userResolver: ResolveFn<User> = (route) => {
const userStore = inject(UserStore);
const userId = route.paramMap.get('id')!;
// No need for explicit error handling - let it bubble up
return userStore.getUser(userId);
}; Обработка ошибок через подписку на события маршрутизатора
Ошибки резолверов также можно обрабатывать, подписавшись на события маршрутизатора и отслеживая события NavigationError. Этот подход обеспечивает более точный контроль над обработкой ошибок и позволяет реализовать пользовательскую логику восстановления после сбоев.
import {Component, inject, signal} from '@angular/core';
import {Router, NavigationError} from '@angular/router';
import {toSignal} from '@angular/core/rxjs-interop';
import {map} from 'rxjs';
@Component({
selector: 'app-root',
template: `
@if (errorMessage()) {
<div class="error-banner">
{{ errorMessage() }}
<button (click)="retryNavigation()">Retry</button>
</div>
}
<router-outlet />
`,
})
export class App {
private router = inject(Router);
private lastFailedUrl = signal('');
private navigationErrors = toSignal(
this.router.events.pipe(
map((event) => {
if (event instanceof NavigationError) {
this.lastFailedUrl.set(event.url);
if (event.error) {
console.error('Navigation error', event.error);
}
return 'Navigation failed. Please try again.';
}
return '';
}),
),
{initialValue: ''},
);
errorMessage = this.navigationErrors;
retryNavigation() {
if (this.lastFailedUrl()) {
this.router.navigateByUrl(this.lastFailedUrl());
}
}
}
Этот подход особенно полезен, если необходимо:
- Реализовать собственную логику повторных попыток при сбое навигации
- Показывать определённые сообщения об ошибках в зависимости от типа сбоя
- Отслеживать сбои навигации для аналитики
Обработка ошибок непосредственно в резолвере
Ниже приведён обновлённый пример userResolver, который записывает ошибку в журнал и возвращает пользователя на общую страницу /users с помощью сервиса Router:
import {inject} from '@angular/core';
import {ResolveFn, RedirectCommand, Router} from '@angular/router';
import {catchError, of, EMPTY} from 'rxjs';
import {UserStore} from './user-store';
import type {User} from './types';
export const userResolver: ResolveFn<User | RedirectCommand> = (route) => {
const userStore = inject(UserStore);
const router = inject(Router);
const userId = route.paramMap.get('id')!;
return userStore.getUser(userId).pipe(
catchError((error) => {
console.error('Failed to load user:', error);
return of(new RedirectCommand(router.parseUrl('/users')));
}),
);
}; Особенности загрузки при навигации
Резолверы данных помогают избежать состояний загрузки внутри компонентов, но создают другую особенность взаимодействия с пользователем: навигация блокируется на время выполнения резолверов. Пользователи могут столкнуться с задержкой между нажатием ссылки и отображением нового маршрута, особенно при медленном сетевом соединении.
Отображение состояния навигации
Чтобы улучшить взаимодействие с пользователем во время выполнения резолверов, можно отслеживать события маршрутизатора и показывать индикаторы загрузки:
import {Component, inject} from '@angular/core';
import {Router} from '@angular/router';
@Component({
selector: 'app-root',
template: `
@if (isNavigating()) {
<div class="loading-bar">Loading...</div>
}
<router-outlet />
`,
})
export class App {
private router = inject(Router);
isNavigating = computed(() => !!this.router.currentNavigation());
}
Такой подход позволяет визуально сообщать пользователям, что навигация выполняется, пока резолверы получают данные.
Рекомендации
- Не перегружайте резолверы: резолверы должны получать только необходимые данные, а не всё, что может понадобиться странице
- Обрабатывайте ошибки: всегда корректно обрабатывайте ошибки, чтобы обеспечить пользователям наилучшее взаимодействие
- Используйте кэширование: рассмотрите возможность кэширования разрешённых данных для повышения производительности
- Учитывайте взаимодействие при навигации: добавьте индикаторы загрузки на время выполнения резолверов, поскольку получение данных блокирует навигацию
- Устанавливайте разумные тайм-ауты: избегайте резолверов, которые могут зависнуть на неопределённое время и заблокировать навигацию
- Строгая типизация: используйте интерфейсы TypeScript для разрешённых данных
Чтение разрешённых данных родительского маршрута во вложенных резолверах
Резолверы выполняются от родительского маршрута к дочернему. Если для родительского маршрута задан резолвер, его разрешённые данные будут доступны дочерним резолверам, которые запускаются после него.
import { inject } from '@angular/core';
import { provideRouter , ActivatedRouteSnapshot } from '@angular/router';
import { userResolver } from './resolvers';
import { UserPosts } from './pages';
import { PostService } from './services',
import type { User } from './types';
provideRouter([
{
path: 'users/:id',
resolve: { user: userResolver }, // user resolver in the parent route
children: [
{
path: 'posts',
component: UserPosts,
// route.data.user is available here while this resolver runs
resolve: {
posts: (route: ActivatedRouteSnapshot) => {
const postService = inject(PostService);
const user = route.parent?.data['user'] as User; // parent data
const userId = user.id;
return postService.getPostByUser(userId);
},
},
},
],
},
]);
Super-powered by Google ©2010–2025.
Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.
https://angular.dev/guide/routing/data-resolvers