مدیریت حالت بارگذاری واکنشی با RxJS و Angular

هنگامی که با Angular برنامه میسازید، زمانی که چیزی در برنامه شما تغییر میکند، اغلب نیاز به دریافت داده دارید. این تغییرات میتواند ناشی از عملکرد کاربر، مانند تایپ کردن در کادر جستجو، یا از خود برنامه، مانند تغییر در پارامترهای مسیر باشد. امروز، ما قصد داریم چند روش موثر برای واکشی واکنشی داده ها در پاسخ به این تغییرات را بیاموزیم.
پاسخ به ورودی کاربر
سناریویی را تصور کنید که در آن میخواهیم نتایج جستجو را هنگام تایپ کاربر در کادر جستجو نشان دهیم:
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<input [formControl]="searchControl" />
<div *ngIf="result$ | async as result">
{{ result }}
</div>
`,
})
export class App {
// We are using a FormControl to watch the user's input
searchControl = new FormControl('');
result$ = this.searchControl.valueChanges.pipe(
switchMap(keywords => this.http.get('/api/search', {params: { q: keywords }))
);
private http = inject(HttpClient);
}
در این مثال، searchControl.valueChanges
قابل مشاهده است. این Observable هر بار که کاربر متن را در کادر جستجو تغییر میدهد، رویدادی را ارسال میکند. سپس از چیزی به نام the استفاده می کنیم switchMap
اپراتور از RxJS. این اپراتور به ما امکان می دهد هر بار که کاربر متن را تغییر می دهد، درخواست داده جدید را ارائه دهیم. همچنین مطمئن می شود که ما فقط با نتایج آخرین ورودی متن کار می کنیم. این برای سریع نگه داشتن برنامه شما و لذت بردن از آن برای کاربر بسیار مهم است.
ما استفاده می کنیم *ngIf
با async
لوله به طوری که قالب به طور خودکار به ما متصل شود result$
قابل مشاهده است و خود به روز می شود. این اطمینان حاصل می کند که الگو همیشه جدیدترین نتایج جستجو را به کاربر در زمان تایپ در کادر جستجو نشان می دهد.
مدیریت بارگیری و حالت های خطا با RxJS
بخش کلیدی دریافت داده، دادن بازخورد به کاربر در طول کل فرآیند بارگیری است. این می تواند یک پیام بارگیری یا یک پیام خطا باشد. به جای استفاده از جدا isLoading
یا hasError
متغیرهای موجود در کامپوننت های خود، از برخی عملگرهای RxJS به نام استفاده می کنیم map
، catchError
، startWith
، و scan
.
بیایید به مثال زیر نگاهی بیندازیم:
interface LoadingState<T = unknown> {
loading: boolean;
error?: Error | null;
data?: T;
}
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<input [formControl]="searchControl" />
<div *ngIf="result$ | async as result">
<div *ngIf="result.loading">Loading...</div>
<div *ngIf="result.data as data">{{data}}</div>
<div *ngIf="result.error as error">{{error.message}}</div>
</div>
`,
})
export class App {
searchControl = new FormControl('');
result$ = this.searchControl.valueChanges.pipe(
switchMap((value) =>
this.http.get('/api/search', {params: { q: value }).pipe(
map((data) => ({ data, loading: false })),
catchError((error) => of({ error, loading: false })),
startWith({ error: null, loading: true })
)
),
scan((state: LoadingState<string>, change: LoadingState<string>) => ({
...state,
...change,
}))
);
private http = inject(HttpClient);
}
در کد بالا:
-
ما استفاده می کنیم
map
برای تغییر داده های خام از درخواست HTTP به یک شی. این شی شامل داده ها و aloading
پرچمی که تنظیم شده استfalse
. -
ما استفاده می کنیم
catchError
برای مدیریت هر گونه خطایی که در طول درخواست HTTP رخ می دهد. اگر خطایی رخ دهد، یک Observable را با یک شی برمی گرداند. این شی شامل خطا و aloading
پرچمی که تنظیم شده استfalse
. -
ما استفاده می کنیم
startWith
برای شروع جریان با یک مقدار خاص. در اینجا، با یک شی شروع می شود که شامل a استnull
خطا و الفloading
پرچم تنظیم شده استtrue
. این اطمینان حاصل می کند که کاربر قبل از بارگیری هر گونه داده، وضعیت بارگذاری را می بیند. -
scan
مانند کار می کندArray.reduce
. حالت قبلی و تغییرات جدید را می گیرد و آنها را در حالت جدید مخلوط می کند. در اینجا، حالت بارگذاری قبلی را می گیرد و آن را با تغییرات نتیجه درخواست HTTP ترکیب می کند. به این ترتیب، ما یک جریان ثابت از تغییرات حالت را نگه میداریم که هر کدام تغییرات حالت قبل را اضافه میکند. -
اکنون، در قالب خود، می توانید از لوله async Angular برای اشتراک استفاده کنید
result$
، و استفاده کنید*ngIf
برای نشان دادن قسمت های مختلف قالب بر اساس وضعیت بارگذاری.
این اپراتورها پیچیده به نظر می رسند! چرا فقط از متغیرهای جداگانه isLoading و hasError استفاده نمی کنید؟
حتی اگر استفاده از آن راحت تر به نظر برسد this.isLoading
یا this.hasError
متغیرهای موجود در کامپوننت شما، استفاده از عملگرهای RxJS به شما کنترل بهتری می دهد.
زمانی که حالتهای بارگیری و خطا در جریان داده قرار دارند، با فرآیند بارگیری مرتبط هستند. آنها را نمی توان با هیچ چیز دیگری تغییر داد. این کار از تغییرات غیرمنتظره و اشکالات جلوگیری می کند.
اگر از متغیرهای جداگانه استفاده می کنید، بخش های دیگر برنامه شما می توانند آنها را تغییر دهند. این می تواند منجر به اشکالاتی شود که ردیابی و رفع آنها دشوار است.
نگه داشتن این حالت ها در جریان داده، کد شما را پیچیده تر و ایمن تر می کند. همچنین اطمینان حاصل می کند که رابط کاربری به راحتی به روز می شود.
مهم: آن را خشک نگه دارید! اپراتور RxJS قابل استفاده مجدد خود را ایجاد کنید
این الگوی که در مورد آن بحث کردیم بسیار مورد استفاده قرار می گیرد، اما ما نمی خواهیم همان کد RxJS را بارها و بارها بنویسیم. خوشبختانه، ساخت اپراتورهای RxJS خودتان کار سختی نیست! میتوانیم کدی را که قبلاً استفاده میکردیم برداریم و یک اپراتور مفید RxJS بسازیم که میتوانیم از آن در سراسر برنامه خود استفاده کنیم.
بیایید به یک مثال نگاه کنیم: ما اپراتور خودمان را فراخوانی خواهیم کرد switchMapWithLoading
. ما آن را در یک فایل جداگانه با نام قرار می دهیم switch-map-with-loading.ts
.
// switch-map-with-loading.ts
interface LoadingState<T = unknown> {
loading: boolean;
error?: Error | null;
data?: T;
}
export function switchMapWithLoading<T>(
observableFunction: (value: any) => Observable<T>
): OperatorFunction<any, LoadingState<T>> {
return (source: Observable<any>) =>
source.pipe(
switchMap((value) =>
observableFunction(value).pipe(
map((data) => ({ data, loading: false })),
catchError((error) => of({ error, loading: false })),
startWith({ error: null, loading: true })
)
),
scan((state: LoadingState<T>, change: LoadingState<T>) => ({
...state,
...change,
}))
);
}
سپس می توانید آن را در کامپوننت خود وارد کرده و از آن به صورت زیر استفاده کنید:
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<input [formControl]="searchControl" />
<div *ngIf="result$ | async as result">
<div *ngIf="result.loading">Loading...</div>
<div *ngIf="result.data as data">{{data}}</div>
<div *ngIf="result.error as error">{{error.message}}</div>
</div>
`,
})
export class App {
searchControl = new FormControl('');
result$ = this.searchControl.valueChanges.pipe(
switchMapWithLoading((value) => this.http.get('/api/search', {params: { q: value })))
);
private http = inject(HttpClient);
}
با این کار، کد برنامه Angular خود را با انتزاع مکانیزم واکشی داده های واکنشی در یک عملگر سفارشی RxJS که می توانید در سراسر برنامه خود استفاده کنید، کارآمدتر و قابل نگهداری تر کرده اید. این نه تنها کد شما را DRY نگه میدارد (خودتان را تکرار نکنید) بلکه درک و مدیریت آن را آسانتر میکند.
حتی ساده تر کردن آن با *ngxLoadWith
اکنون، ممکن است از خود بپرسید که آیا راه ساده تری برای مدیریت همه اینها بدون سر و کار داشتن با عملگرهای پیچیده RxJS وجود دارد یا خیر. خبر خوب! معرفی *ngxLoadWith
دستورالعمل، که رویکرد ساده تری برای مدیریت وضعیت های بارگذاری در برنامه Angular شما ارائه می دهد.
برای شروع، باید آن را نصب کنید *ngxLoadWith
بسته بندی دستور زیر را اجرا کنید:
npm install ngx-load-with
پس از نصب، می توانید از *ngxLoadWith
دستورالعمل در کامپوننت Angular شما به این صورت است:
@Component({
selector: "my-app",
standalone: true,
imports: [CommonModule, ReactiveFormsModule, NgxLoadWithModule],
template: `
<input [formControl]="searchControl" />
<div
*ngxLoadWith="
getResult as data;
args: searchControl.value;
loadingTemplate: loading;
errorTemplate: error
"
>
{{ data }}
</div>
<ng-template #loading>Loading...</ng-template>
<ng-template #error let-error>{{ error.message }}</ng-template>
`,
})
export class App {
searchControl = new FormControl("");
getResult = (keywords: string) =>
this.http.get("/api/search", { params: { q: keywords } });
private http = inject(HttpClient);
}
بیایید آن را تجزیه کنیم. ما به سادگی یک تابع به نام تعریف می کنیم getResult
که طول می کشد keywords
پارامتر و یک درخواست HTTP را برمی گرداند. سپس این تابع را به *ngxLoadWith
بخشنامه، همراه با ارزش searchControl.value
به عنوان args
ورودی
در حال حاضر هر زمان که searchControl
تغییر ارزش، getResult
تابع با مقدار جدید فراخوانی می شود. را *ngxLoadWith
دستورالعمل سپس در نتیجه تابع مشترک می شود و نتیجه را در قالب نمایش می دهد.
سپس دو قالب تعریف می کنیم: loading
و error
. این الگوها زمانی که getResult
عملکرد در حال بارگیری است یا دارای خطا است.
خودشه! اکنون می توانید به راحتی و با خیال راحت وضعیت های بارگذاری را در برنامه Angular خود بدون نیاز به سر و کار داشتن با اپراتورهای پیچیده RxJS مدیریت کنید.
آماده هستید آن را امتحان کنید؟ کد منبع را در github.com/rensjaspers/ngx-load-with بررسی کنید. اگر پیدا کردی *ngxLoadWith
مفید است، فراموش نکنید که در Github به آن ستاره بدهید. ما دوست داریم نظرات و تجربیات شما را در مورد این دستورالعمل بشنویم!