Получение данных маршрута с помощью функции преобразователя в Angular
В этом сообщении блога я демонстрирую технику использования функции разрешения данных для получения данных во время изменения маршрута. Когда маршрут завершает активацию, компонент получает доступ к данным маршрута и может отображать их в шаблоне или манипулировать ими для получения новых состояний.
Я работал над фейковой демо-версией магазина, где на первой странице отображаются все продукты в списке. Когда пользователь щелкает название продукта, он перенаправляется на страницу сведений о продукте. Этот вариант использования очень прост, но на самом деле я потратил некоторое время на реализацию окончательного решения.
Первоначальное решение заключалось в том, чтобы сделать HTTP-запрос для получения Observable продукта, а затем использовать NgIf и AsyncPipe для разрешения Observable в шаблоне. Angular представил Signal, и я решил сохранить продукт в Signal и визуализировать шаблон со значением Signal. Решение не было элегантным, и мне пришлось его поцарапать. Наконец, я реализовал функцию разрешения данных для получения продукта по идентификатору. Когда страница сведений о продукте полностью маршрутизирована, я извлек продукт из данных маршрута и использовал его в компоненте.
Вариант использования демо-версии
В демонстрации поддельного магазина я вызываю API продукта, чтобы получить все продукты и отобразить их в ProductListComponent. Когда пользователь нажимает на название продукта, я вызываю другой API, чтобы получить сведения по идентификатору и отобразить данные в ProductDetailsComponent.
// routes.ts
export const routes: Routes = [
{
path: 'products',
loadComponent: () => import('./products/product-list/product-list.component').then((m) => m.ProductListComponent),
title: 'Product list',
},
{
path: 'products/:id',
loadComponent: () => import('./products/product-details/product-details.component').then((m) => m.ProductDetailsComponent),
title: 'Product',
},
];
Этот вариант использования типичен для приложения CRUD, но для разработки решения, которое получает продукт, фактически потребовалось 3 итерации.
Попытка 1. Получение Observable продукта по идентификатору и разрешение Observable в шаблоне HTML с помощью NgIf и AsyncPipe.
Попытка 2. Применение toSignal для преобразования Observable в Signal и отображения значения Signal в шаблоне. Это решение на самом деле было слишком сложным и усугубляло ситуацию.
Попытка 3. Использование функции преобразователя данных для получения наблюдаемого объекта или продукта. С помощью withComponentInputBinding продукт доступен в ProductDetailsComponent в качестве входных данных. Затем я применил входные данные во встроенном шаблоне для отображения значений продукта.
В следующих нескольких разделах я собираюсь показать, как я итеративно реализовал функцию разрешения, чтобы вернуть продукт и использовать его в компоненте.
Решение 1.
Получение Observable из API и его разрешение во встроенном шаблоне.
С помощью withComponentInputBinding параметр пути (id) является входными данными ProductDetailsComponent. Я использовал идентификатор для получения продукта и разрешил Observable во встроенном шаблоне с помощью NgIf и AsyncPipe.
// main.ts
bootstrapApplication(App, {
providers: [provideHttpClient(), provideRouter(routes, withComponentInputBinding())]
});
// product.service.ts
const PRODUCTS_URL = 'https://fakestoreapi.com/products';
@Injectable({
providedIn: 'root'
})
export class ProductService {
private readonly httpClient = inject(HttpClient);
getProduct(id: number): Observable<Product | undefined> {
return this.httpClient.get<Product>(`${PRODUCTS_URL}/${id}`)
.pipe(catchError((err) => of(undefined)));
}
}
// product-details.component.ts
@Component({
selector: 'app-product-details',
standalone: true,
imports: [AsyncPipe, NgIf],
template: `
<div>
<div class="product" *ngIf="product$ | async as product">
<div class="row">
<img [src]="product?.image" [attr.alt]="product?.title" width="200" height="200" />
</div>
<div class="row">
<span>id:</span><span>{{ id }}</span>
</div>
<div class="row">
<span>Category: </span><span>{{ (product?.category '' }}</span>
</div>
<div class="row">
<span>Description: </span><span>{{ product?.description || '' }}</span>
</div>
<div class="row">
<span>Price: </span><span>{{ product?.price || '' }}</span>
</div>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductDetailsComponent implements OnInit {
@Input({ required: true, transform: numberAttribute })
id!: number;
productService = inject(ProductService);
product$!: Observable<Product | undefined>;
ngOnInit() {
this.product$ = this.productService.getProduct(this.id);
}
}
Однако я предпочел не иметь дело с Observable, NgIf и AsyncPipe в компоненте и HTML-шаблоне. Если я не буду использовать Observable, я буду использовать Signal, который отслеживает реактивность приложения.
Позвольте мне провести рефакторинг решения для использования Signal и отображения значения Signal во встроенном шаблоне.
Решение 2.
Преобразование наблюдаемого в сигнал и отображение значения сигнала во встроенном шаблоне.
toSignal() — это функция, которая преобразует Observable в Signal, и я подумал, что это единственное, что мне нужно в ngOnInit. Однако компилятор выдал ошибку, поскольку toSignal() не вызывался в контексте внедрения. Чтобы исправить эту ошибку, я выполнил логику функции обратного вызова runInInjectionContext.
// product-details.component.ts
injector = inject(Injector);
product: Signal<Product | undefined> = signal(undefined);
ngOnInit() {
runInInjectionContext(this.injector, () => {
this.product = toSignal(this.productService.getProduct(this.id),
{ initialValue: undefined });
});
}
Благодаря Signal мне не нужно импортировать NgIf и AsyncPipe, и каждый раз, когда продукт изменяется в product().
<div>
<div class="product">
<div class="row">
<img [src]="product()?.image" [attr.alt]="product()?.title" width="200" height="200" />
</div>
<div class="row">
<span>id:</span><span>{{ id }}</span>
</div>
<div class="row">
<span>Category: </span><span>{{ product()?.category || '' }}</span>
</div>
<div class="row">
<span>Description: </span><span>{{ product()?.description || '' }}</span>
</div>
<div class="row">
<span>Price: </span><span>{{ product()?.price || '' }}</span>
</div>
</div>
</div>
При сравнении решений Observable и Signal последнее добавило ненужные сложности, такие как inject(Injector) и runInInjectionContext(...). Я бы предпочел вернуться к исходному решению, чем использовать toSignal ради использования Signal.
Затем я сформулировал другое решение, которое использует функцию преобразователя данных для получения продукта при изменении маршрута. Аналогично, withComponentInputBinding должен предоставить мне входные данные о продукте в ProductDetailsComponent.
Решение 3.
Использование функции преобразователя данных для возврата продукта в ProductDetailsComponent.
Функция разрешения данных принимает маршрут и возвращает либо Observable, либо Promise. Поэтому я извлек идентификатор из URL-адреса и передал его в службу продуктов для получения наблюдаемого продукта.
// product.resolver.ts
export const productResolver = (route: ActivatedRouteSnapshot) => {
const productId = route.paramMap.get('id');
if (!productId) {
return of(undefined);
}
return inject(ProductService).getProduct(+productId);
}
В route.ts я изменил конфигурацию маршрутов, чтобы назначить productResolver свойству разрешения пути Products/:id.
// route.ts
export const routes: Routes = [
{
path: 'products/:id',
loadComponent: () => import('./products/product-details/product-details.component').then((m) => m.ProductDetailsComponent),
title: 'Product',
resolve: {
product: productResolver,
}
},
];
Далее я мог бы очистить ProductDetailsComponents, поскольку преобразователь продукта устранил предыдущую логику в ngOnInit.
// product-details.component
@Component({
selector: 'app-product-details',
standalone: true,
template: `
<div>
<div class="product">
<div class="row">
<img [src]="product?.image" [attr.alt]="product?.title || 'product image'"
width="200" height="200"
/>
</div>
<div class="row">
<span>id:</span>
<span>{{ product?.id || '' }}</span>
</div>
<div class="row">
<span>Category: </span>
<span>{{ product?.category || '' }}</span>
</div>
<div class="row">
<span>Description: </span>
<span>{{ product?.description || '' }}</span>
</div>
<div class="row">
<span>Price: </span>
<span>{{ product?.price || '' }}</span>
</div>
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductDetailsComponent {
@Input()
product: Product | undefined = undefined;
}
Наконец, я заменил все вхождения Product() на Product во встроенном шаблоне.
Заключение
Это конец статьи в блоге, и я надеюсь, что вам понравится ее содержание, и вы продолжите следить за моим опытом изучения Angular и других технологий.