سرویس با سیگنال در 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 و سایر فناوری ها را دنبال کنید.
منابع: