سرویس با سیگنال در Angular

معرفی
در این پست وبلاگ، من می خواهم “سرویس با موضوع” را به “سرویس با سیگنال” تبدیل کنم و فقط سیگنال ها را در معرض نمایش قرار دهم. از طریق تماس امکان پذیر است toSignal برای تبدیل Observable به سیگنال. سپس، می توانم مقادیر سیگنال را برای نمایش داده ها به اجزای Angular ارسال کنم. پس از استفاده مستقیم از مقادیر سیگنال در برنامه، الگوهای درون خطی نیازی به استفاده از لوله ناهمگام برای حل Observable ندارند. علاوه بر این، آرایه واردات قطعات نیازی به NgIf و AsyncPipe.
کدهای منبع “سرویس با موضوع”
// pokemon.service.ts
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class PokemonService {
private readonly pokemonIdSub = new Subject<number>();
readonly pokemonId$ = this.pokemonIdSub.asObservable();
updatePokemonId(pokemonId: number) {
this.pokemonIdSub.next(pokemonId);
}
}
// pokemon.http.ts
export const retrievePokemonFn = () => {
const httpClient = inject(HttpClient);
return (id: number) => httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`)
.pipe(
map((pokemon) => ({
id: pokemon.id,
name: pokemon.name,
height: pokemon.height,
weight: pokemon.weight,
back_shiny: pokemon.sprites.back_shiny,
front_shiny: pokemon.sprites.front_shiny,
abilities: pokemon.abilities.map((ability) => ({
name: ability.ability.name,
is_hidden: ability.is_hidden
})),
stats: pokemon.stats.map((stat) => ({
name: stat.stat.name,
effort: stat.effort,
base_stat: stat.base_stat,
})),
}))
);
}
export const getPokemonId = () => inject(PokemonService).pokemonId$;
// pokemon.component.ts
@Component({
selector: 'app-pokemon',
standalone: true,
imports: [AsyncPipe, NgIf, PokemonControlsComponent, PokemonAbilitiesComponent, PokemonStatsComponent, PokemonPersonalComponent],
template: `
<h1>
Display the first 100 pokemon images
</h1>
<div>
<ng-container *ngIf="pokemon$ | async as pokemon">
<div class="container">
<img [src]="pokemon.front_shiny" />
<img [src]="pokemon.back_shiny" />
</div>
<app-pokemon-personal [pokemon]="pokemon"></app-pokemon-personal>
<app-pokemon-stats [stats]="pokemon.stats"></app-pokemon-stats>
<app-pokemon-abilities [abilities]="pokemon.abilities"></app-pokemon-abilities>
</ng-container>
</div>
<app-pokemon-controls></app-pokemon-controls>
`,
styles: [`...omitted due to brevity...`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent {
retrievePokemon = retrievePokemonFn();
pokemon$ = getPokemonId().pipe(switchMap((id) => this.retrievePokemon(id)));
}
PokemonService کپسوله می کند pokemonIdSub موضوع و افشا می کند pokemonId$ قابل مشاهده که در PokemonComponent، استناد می کنم retrievePokemon عملکردی برای بازیابی یک پوکمون جدید در هر زمان pokemonId$ یک شناسه جدید منتشر می کند. pokemon$ یک Pokemon Observable است که من آن را در قالب درون خطی حل می کنم تا شی پوکمون را به اجزای فرزند اختصاص دهم.
بعد، من قصد دارم تبدیل کنم PokemonService از «سرویس با موضوع» تا «سرویس با سیگنال» برای برجسته کردن مزایای استفاده از سیگنالها.
تبدیل به “سرویس با سیگنال”
ابتدا ترکیب می کنم pokemon.http.ts و pokemon.service.ts حرکت کردن retrievePokemonFn به خدمت دوم اینکه اعلام میکنم pokemon که نتیجه را ذخیره می کند toSignal که یک سیگنال است. سوم، من می توانم استفاده کنم pokemon سیگنال برای محاسبه personalData سیگنال و اختصاص دهید personalData به PokemonPersonalComponent.
// pokemon.service.ts
// Point 1: move helper functions to this service
const retrievePokemonFn = () => {
const httpClient = inject(HttpClient);
return (id: number) => httpClient.get<Pokemon>(`https://pokeapi.co/api/v2/pokemon/${id}`);
}
const pokemonTransformer = (pokemon: Pokemon): DisplayPokemon => {
const stats = pokemon.stats.map((stat) => ({
name: stat.stat.name,
effort: stat.effort,
baseStat: stat.base_stat,
}));
const abilities = pokemon.abilities.map((ability) => ({
name: ability.ability.name,
isHidden: ability.is_hidden
}));
const { id, name, height, weight, sprites } = pokemon;
return {
id,
name,
height,
weight,
backShiny: sprites.back_shiny,
frontShiny: sprites.front_shiny,
abilities,
stats,
}
}
const initialValue: DisplayPokemon = {
id: -1,
name: '',
height: 0,
weight: 0,
backShiny: '',
frontShiny: '',
abilities: [],
stats: [],
}
@Injectable({
providedIn: 'root'
})
export class PokemonService {
private readonly pokemonIdSub = new BehaviorSubject(1);
private readonly retrievePokemon = retrievePokemonFn()
// Point 2: convert Observable to signal using toSignal
pokemon = toSignal(this.pokemonIdSub.pipe(
switchMap((id) => this.retrievePokemon(id)),
map((pokemon) => pokemonTransformer(pokemon)),
), { initialValue });
// Point 3: compute a signal from an existing signal
personalData = computed(() => {
const { id, name, height, weight } = this.pokemon();
return [
{ text: 'Id: ', value: id },
{ text: 'Name: ', value: name },
{ text: 'Height: ', value: height },
{ text: 'Weight: ', value: weight },
];
});
updatePokemonId(input: PokemonDelta | number) {
if (typeof input === 'number') {
this.pokemonIdSub.next(input);
} else {
const newId = this.pokemonIdSub.getValue() + input.delta;
const nextPokemonId = Math.min(input.max, Math.max(input.min, newId));
this.pokemonIdSub.next(nextPokemonId);
}
}
}
pokemonIdSub یک BehaviorSubject است که شناسه Pokemon را ذخیره می کند. هنگامی که BehaviorSubject یک شناسه منتشر می کند، Observable فراخوانی می کند this.retrievePokemon برای بازیابی پوکمون و pokemonTransformer برای تبدیل داده ها سپس، toSignal Pokemon Observable را به سیگنال Pokemon تبدیل می کند.
personalData یک سیگنال محاسبه شده است که از this.pokemon() مقدار سیگنال این سیگنالی است که شناسه، نام، قد و وزن یک پوکمون را برمی گرداند.
در مرحله بعد، من قصد دارم کامپوننت ها را برای استفاده از سیگنال ها به جای Observable تغییر دهم.
برای استفاده از سیگنال، کامپوننت پوکمون را تغییر دهید
export class PokemonComponent {
service = inject(PokemonService);
pokemon = this.service.pokemon;
personalData = this.service.personalData;
}
PokemonComponent تزریق می کند PokemonService برای دسترسی pokemon و personalData سیگنال ها بدون pokemon$ قابل مشاهده است، من الگوی درون خطی را برای ارائه مقدار سیگنال و ارسال مقادیر سیگنال به مؤلفههای فرزند اصلاح میکنم.
@Component({
selector: 'app-pokemon',
standalone: true,
imports: [PokemonControlsComponent, PokemonAbilitiesComponent, PokemonStatsComponent, PokemonPersonalComponent],
template: `
<h2>
Display the first 100 pokemon images
</h2>
<div>
<ng-container>
<div class="container">
<img [src]="pokemon().frontShiny" />
<img [src]="pokemon().backShiny" />
</div>
<app-pokemon-personal [personalData]="personalData()"></app-pokemon-personal>
<app-pokemon-stats [stats]="pokemon().stats"></app-pokemon-stats>
<app-pokemon-abilities [abilities]="pokemon().abilities"></app-pokemon-abilities>
</ng-container>
</div>
<app-pokemon-controls></app-pokemon-controls>
`,
styles: [`...omitted due to brevity...`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonComponent { ... }
یکی از تغییرات آشکار این است که الگوی درون خطی ngContainer، ngIf و لوله async را حذف می کند. همچنین منجر به حذف می شود AsyncPipe و NgIf از آرایه واردات
قالب درون خطی فراخوانی می کند pokemon() چندین بار برای دسترسی به ویژگی های frontShiny، backShiny، آمار و توانایی ها. آمار و توانایی ها متعاقباً به ورودی تبدیل می شوند PokemonStatsComponent و PokemonAbilitiesComponent به ترتیب.
به طور مشابه، نتیجه از personalData() منتقل می شود به personalData ورودی از PokemonPersonalComponent.
اجزای فرزند را برای پذیرش مقدار سیگنال ورودی تغییر دهید
پس از تغییر کد، برنامه خراب می شود PokemonComponent. به این دلیل است که ورودی از PokemonPersonalComponent دارای انواع مختلف برای رفع مشکل، مقدار ورودی مولفه فرزند را اصلاح می کنم.
// pokemon-personal.component.ts
@Component({
selector: 'app-pokemon-personal',
standalone: true,
imports: [NgTemplateOutlet, NgFor],
template:`
<div class="pokemon-container" style="padding: 0.5rem;">
<ng-container *ngTemplateOutlet="details; context: { $implicit: personalData }"></ng-container>
</div>
<ng-template #details let-personalData>
<label *ngFor="let data of personalData">
<span style="font-weight: bold; color: #aaa">{{ data.text }}</span>
<span>{{ data.value }}</span>
</label>
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PokemonPersonalComponent {
@Input({ required: true })
personalData: ({ text: string; value: string; } | { text: string; value: number })[];;
}
جایگزین می کنم pokemon ورودی با personalData و از دومی در قالب درون خطی برای ارائه مقادیر آرایه استفاده کنید.
اگر از Observable در استفاده کنم PokemonComponent، من نمی توانم بسازم personalData به صورت واکنشی من اشتراک Pokemon Observable را میکنم و میسازم personaData در پاسخ به تماس علاوه بر این، من با استفاده از Observable کامل میکنم takeUntilDestroyed برای جلوگیری از نشت حافظه
این است و من سرویس پوکمون را از “سرویس با موضوع” به “سرویس با سیگنال” تبدیل کرده ام. سرویس Pokemon تماس HTTP را کپسوله میکند، Observable را به سیگنال تبدیل میکند و سیگنالها را به بیرون نمایش میدهد. در کامپوننتها، من توابع سیگنال را در قالبهای درون خطی فراخوانی میکنم تا مقادیر آنها را نمایش دهم. علاوه بر این، قطعات واردات را متوقف می کنند NgIf و AsyncPipe زیرا آنها نیازی به حل Observable ندارند.
این پایان پست وبلاگ است و امیدوارم از مطالب خوشتان بیاید و تجربه یادگیری من در Angular و سایر فناوری ها را دنبال کنید.
منابع:



