这个角度分量是以反应方式实现的吗?

Mar*_*sky 3 reactive-programming rxjs angular

我从反应式编程开始,我有以下应用程序: 测试应用程序

三个点的项目是一个<ul>标签。每次li单击某个项目时,应用程序都会使用 routerLink 附加查询字符串(/bands、 /bands?active=false 和 /bands?active=true)导航到路线。

网页中从 Bands 开始的部分(属于band-list组件)是 router-outlet 根据路由显示的内容。下面的网格是根据查询参数“active”加载的。然后,每次li单击某个项目时,网格都会重新加载。

此外,“刷新数据”按钮会重新加载网格。

我正在尝试使用反应式编程来实现以下逻辑:

  • 在初始化时,band-list组件应根据active路由中的查询参数加载网格。

  • 每次用户单击 a 时li,网格都应重新加载。如果在此之前单击li网格仍在加载(发出 http 请求),则应取消正在进行的 http 请求(在我看来,这就像一个switchMap运算符)。

  • 如果用户单击refresh data button并且网格已完成加载其数据,则网格应该加载。但是,如果网格仍在加载其数据(在 http 请求中间),则单击不应执行任何操作,即,进程内加载数据操作应完成,并且按钮中的单击实际上会被忽略(这看起来就像exhaustMap我一样:S)

  • 另外,每次执行http操作时,当http操作尚未完成时,应该显示一条消息告诉“正在加载...”,并且应该清空网格中的数据。当 http 请求完成时,“正在加载...”消息应被删除,并且数据应显示在网格中。

我在组件中实现了代码,band-list就像下一个代码块一样,但我不确定该defer运算符在我的解决方案中的使用。我使用了该defer运算符,因为如果当前请求正在使用 defer 运算符内的变量进行处理,我需要维护状态,isLoading并且还要更新此变量作为运算符中的副作用操作tap,并使用此变量作为副作用在filter运营商中。

我的问题是:根据反应式编程,这是一种很好的方法吗?我可以做得更好吗?

您可以在stackblitz' url中检查此解决方案

@Component({
    selector: 'app-band-list',
    templateUrl: 'band-list.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class BandListComponent {
    readonly initialState: BandState = { data: [], isLoading: false };
    #bandDataService = inject(BandDataService);

    refreshDataClickSubject = new Subject<void>();
    #activedRoute = inject(ActivatedRoute);

    bandState$;

    constructor() {
        this.bandState$ = this.getBands$();

        const destroyRef = inject(DestroyRef);
        destroyRef.onDestroy(() => {
            this.refreshDataClickSubject.complete();
            this.refreshDataClickSubject.unsubscribe();
        });
    }

    getBands$(): Observable<BandState> {
        const queryParams$ = this.#activedRoute.queryParams;
        const refreshDataClick$ = this.refreshDataClickSubject.asObservable();

        const isActiveFromQueryParams$ = queryParams$.pipe(
            map<any, boolean | undefined>(p => p["active"])
        );

        const final$ = defer(() => {
            let isLoading = false;

            const getBandsFn$ = (isActive: boolean | undefined) => concat(
                of([]).pipe(
                    tap(_ => isLoading = true), // <== here updating the outer state. 
                    map(data => ({data, isLoading: true}))
                ),
                this.#bandDataService.getBands$(isActive).pipe(
                    map(data => ({data, isLoading: false}))
                )
            ).pipe(
                finalize(() => isLoading = false) // <== again updating the outer state.
            );

            const obs$ = merge(
                queryParams$.pipe(map(_ => RequestOrigin.QueryParams)),
                refreshDataClick$.pipe(map(_ => RequestOrigin.RefreshButton))
            ).pipe(
                filter(requestOrigin => requestOrigin === RequestOrigin.QueryParams ||
                    (requestOrigin === RequestOrigin.RefreshButton && !isLoading)),
                withLatestFrom(isActiveFromQueryParams$),
                map(([_, isActive]) => isActive),
                switchMap(isActive => getBandsFn$(isActive)),
                startWith(this.initialState)
            );
            return obs$;
        });

        return final$;
    }
}

enum RequestOrigin {
    QueryParams, RefreshButton
}

interface BandState {
    data: Band[],
    isLoading: boolean
}
Run Code Online (Sandbox Code Playgroud)
<h2>Bands</h2>
<div class="row">
    <div class="col-sm-12">
        <div class="form-group">
            <button class="btn btn-primary" type="button" (click)="refreshDataClickSubject.next()">
                Refresh data
            </button>
        </div>

        @if(bandState$ | async; as bandState) {
            @if (bandState.isLoading) {
                Loading...
            } @else {
                <table class="table">
                    <thead>
                        <tr>
                            <th>Name</th>
                            <th>Formation year</th>
                            <th>Is active</th>
                        </tr>
                    </thead>
                    <tbody>
                        @for(band of bandState.data; track $index) {
                            <tr>
                                <td>{{ band.name }}</td>
                                <td>{{ band.formationYear }}</td>
                                <td>{{ band.isActive }}</td>
                            </tr>
                        } @empty {
                            <h3>Empty!</h3>
                        }
                    </tbody>
                </table>
            }
        } @else {
            BandState$ not initialized
        }
    </div>
</div>
Run Code Online (Sandbox Code Playgroud)

Biz*_*Bob 6

有一种更简单的方法来实现您想要的行为。但首先,让我们从一个非常基本的实现开始,它可以让您完成 90% 的任务:

private isActive$ = inject(ActivatedRoute).queryParamMap.pipe(
  map(params => params.get('active'))
);

bandState$: Observable<BandState> = this.isActive$.pipe(
  switchMap(isActive => this.#bandDataService.getBands$(isActive).pipe(
    map(data => ({ data, areBandsLoading: false })),
    startWith({ data: [], areBandsLoading: true })
  ))
);
Run Code Online (Sandbox Code Playgroud)

isActive$是一个发出查询参数值的可观察对象。当它发出值时,我们switchMap就调用以获取数据。然后我们将mapapi 调用的结果传递给您的BandState形状。最后,我们指定要发出的初始值,startWith就像您在代码中所做的那样。

这意味着每当查询参数发生变化时,都会进行调用以获取适当的数据,并且bandState$可观察对象将发出更新后的状态。

我首先展示这个简单实现的原因是因为它距离获得您想要的“刷新”行为的解决方案并不太远。


为了合并您的“刷新”功能,我们可以使用 aBehaviorSubject作为通知触发器。

refresh$ = new BehaviorSubject<void>(undefined);
Run Code Online (Sandbox Code Playgroud)

然后,我们不是将isActive$发射直接通过管道传输到 http 调用,而是将其发送到此中间触发器,然后用于exhaustMap处理调用。BehaviorSubject我们使用而不是的原因Subject是我们在订阅时收到发射,这意味着我们switchMap不需要等待refresh$第一次触发,它会自动发生:

bandState$: Observable<BandState> = this.isActive$.pipe(
  switchMap(isActive => this.refresh$.pipe(
    exhaustMap(() => this.#bandDataService.getBands(isActive).pipe(
      map(data => ({ data, areBandsLoading: false })),
      startWith({ data: [], areBandsLoading: true })
    ))
  ))
);
Run Code Online (Sandbox Code Playgroud)

看看这个正在运行的StackBlitz