برنامه نویسی

مدیریت حالت بارگذاری واکنشی با 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);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در کد بالا:

  1. ما استفاده می کنیم map برای تغییر داده های خام از درخواست HTTP به یک شی. این شی شامل داده ها و a loading پرچمی که تنظیم شده است false.

  2. ما استفاده می کنیم catchError برای مدیریت هر گونه خطایی که در طول درخواست HTTP رخ می دهد. اگر خطایی رخ دهد، یک Observable را با یک شی برمی گرداند. این شی شامل خطا و a loading پرچمی که تنظیم شده است false.

  3. ما استفاده می کنیم startWith برای شروع جریان با یک مقدار خاص. در اینجا، با یک شی شروع می شود که شامل a است null خطا و الف loading پرچم تنظیم شده است true. این اطمینان حاصل می کند که کاربر قبل از بارگیری هر گونه داده، وضعیت بارگذاری را می بیند.

  4. scan مانند کار می کند Array.reduce. حالت قبلی و تغییرات جدید را می گیرد و آنها را در حالت جدید مخلوط می کند. در اینجا، حالت بارگذاری قبلی را می گیرد و آن را با تغییرات نتیجه درخواست HTTP ترکیب می کند. به این ترتیب، ما یک جریان ثابت از تغییرات حالت را نگه می‌داریم که هر کدام تغییرات حالت قبل را اضافه می‌کند.

  5. اکنون، در قالب خود، می توانید از لوله 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 به آن ستاره بدهید. ما دوست داریم نظرات و تجربیات شما را در مورد این دستورالعمل بشنویم!

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا