我目前正在重构一个使用 RxJS 的 Angular 组件,旨在实现信号。该组件的要求是:
这是我的组件的当前实现:
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();
}
}
受到我发现的示例(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)
)
)
);
}
}
这种方法看起来很干净,但我不确定如何有效地显示加载指示器,特别是当错误或内容已经存在时。
或者,我考虑了一种稍微冗长的方法:
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
,但现在情况已不再是这样了。
我认为最好的方案是将 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(),
],
});