使用 RxJS 重构角度分量以使用信号

问题描述 投票:0回答:1

我目前正在重构一个使用 RxJS 的 Angular 组件,旨在实现信号。该组件的要求是:

  1. 检索并呈现业务对象。
  2. 显示加载指示器。
  3. 如果出现问题,则显示错误屏幕,并提供重试/刷新选项。

当前实施情况

这是我的组件的当前实现:

class BusinessDetailPage implements OnInit {
  business: Business = null;
  loading = true;
  error = false;

  trigger$ = new Subject<void>();

  constructor(/** ... */) {}

  ngOnInit() {
    from(this.trigger$)
      .pipe(
        untilDestroyed(this),
        tap(() => (this.loading = true)),
        switchMap(() =>
          this.businessService
            .getBusiness(parseInt(this.route.snapshot.params.id))
            .pipe(
              first(),
              catchError(() => {
                this.error = true;
                this.loading = false;
                return of(null);
              })
            )
        )
      )
      .subscribe((business) => {
        this.business = business;
        this.loading = false;
      });
  }

  retry() {
    this.trigger$.next();
  }
}

方法1

受到我发现的示例(signal-error-example)的启发,我重构了我的组件,如下所示:

export class BusinessDetailPage {
  trigger$ = new Subject<void>();
  business = toSignalWithError(this.fetchBusiness());
  isLoading = computed(() => !this.business()?.value && !this.business()?.error);

  constructor(/** ... */) {
    this.trigger$.next();
  }

  refresh() {
    this.trigger$.next();
  }

  private fetchBusiness() {
    return from(this.trigger$).pipe(
      untilDestroyed(this),
      switchMap(() =>
        this.businessService.getBusiness(
          parseInt(this.route.snapshot.params.id)
        )
      )
    );
  }
}

这种方法看起来很干净,但我不确定如何有效地显示加载指示器,特别是当错误或内容已经存在时。

方法2

或者,我考虑了一种稍微冗长的方法:

export class BusinessDetailPage {
  trigger$ = new Subject<void>();
  business = toSignal(this.fetchBusiness());
  error = signal(false);
  isLoading = signal(false);

  constructor(/** ... */) {
    this.trigger$.next();
  }

  refresh() {
    this.trigger$.next();
  }

  private fetchBusiness() {
    return from(this.trigger$).pipe(
      untilDestroyed(this),
      tap(() => {
        this.error.set(false);
        this.isLoading.set(true);
      }),
      switchMap(() =>
        this.businessService
          .getBusiness(parseInt(this.route.snapshot.params.id))
          .pipe(
            catchError(() => {
              this.error.set(true);
              return of(null);
            }),
            finalize(() => this.isLoading.set(false))
          )
      )
    );
  }
}

这种方法似乎涵盖了所有要求,但我的目标是从信号中导出(计算)

isLoading
,但现在情况已不再是这样了。

问题

  1. 这两种方法之间,就代码效率和可读性而言,哪种被认为是最佳实践?
  2. 在使用信号的情况下,是否有更优化的方法来处理加载和错误状态?
  3. 对于改进此场景的重构过程有什么建议吗?
angular rxjs signals
1个回答
0
投票

我认为最好的方案是将 rxjs 和信号结合起来,以实现高效的反应性。

看到这个stackblitz兄弟。

部分代码:

服务

@Injectable({ providedIn: 'root' })
export class BusinessService {
  businessData = signal<any | null>(null);
  loading = signal(false);
  error = signal(false);

  router = inject(Router);
  http = inject(HttpClient);

  #loadingQueue = new Subject<number>();

  loadingQueue = this.#loadingQueue.pipe(
    switchMap((id) => {
      this.error.set(false);
      this.loading.set(true);
      const simulateError = Math.random() < 0.5;
      return this.http
        .get(
          `https://jsonplaceholder.typicode.com/todos${
            simulateError ? 'x' : ''
          }/${id}`
        )
        .pipe(
          catchError(() => {
            this.error.set(true);
            return of(null);
          }),
          tap((response) => {
            this.businessData.set(response);
          }),
          finalize(() => this.loading.set(false))
        );
    })
  );

  loadBusiness(id: number) {
    this.loading.set(true);
    this.#loadingQueue.next(id);
  }

  changeBusiness() {
    this.router.navigate(['business', Math.floor(Math.random() * 100)]);
  }
}

组件

@Component({
  selector: 'app-business',
  standalone: true,
  templateUrl: './business.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [NgIf, JsonPipe],
})
export class BusinessComponent {
  id = input.required<number>();
  businessService = inject(BusinessService);

  constructor() {
    this.businessService.loadingQueue.pipe(takeUntilDestroyed()).subscribe();
    effect(() => {
      const id = this.id();
      untracked(() => this.businessService.loadBusiness(id));
    });
  }
}

HTML

<h3>The business {{ id() }}</h3>
<h5>
  <button
    [disabled]="businessService.loading()"
    (click)="businessService.changeBusiness()"
  >
    Change business
  </button>
</h5>
<p *ngIf="businessService.error()" style="color: red; font-weight: bolder;">
  Error occured!
</p>
<pre *ngIf="businessService.businessData() as data">{{ data | json }}</pre>

引导应用程序

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <h1>Hello from {{ name }}!</h1>
    <router-outlet></router-outlet>
  `,
  imports: [RouterOutlet],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  name = 'Angular';
}

bootstrapApplication(App, {
  providers: [
    provideRouter(
      [
        {
          path: '',
          pathMatch: 'full',
          redirectTo: 'business/1',
        },
        {
          path: 'business/:id',
          loadComponent: () =>
            import('./business.component').then((c) => c.BusinessComponent),
        },
      ],
      withComponentInputBinding()
    ),
    provideHttpClient(),
  ],
});
© www.soinside.com 2019 - 2024. All rights reserved.