Получение данных маршрута с помощью функции преобразователя в 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 и других технологий.