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
运营商中。
我的问题是:根据反应式编程,这是一种很好的方法吗?我可以做得更好吗?
@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)
有一种更简单的方法来实现您想要的行为。但首先,让我们从一个非常基本的实现开始,它可以让您完成 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
就调用以获取数据。然后我们将map
api 调用的结果传递给您的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
归档时间: |
|
查看次数: |
330 次 |
最近记录: |