【问题标题】:Best way to get intermediate data from an RXJS observable从 RXJS observable 获取中间数据的最佳方式
【发布时间】:2021-08-16 11:50:57
【问题描述】:

我想调用第一个服务来查询一些数据(返回一个结果数组),然后再次调用第二个服务来为每个结果获取更多数据。每个服务调用的结果都应该合并并返回并显示在一个列表中。我想我理解这部分的工作原理。

我不太确定的部分是要求能够在可用时立即显示第一次服务调用的结果。 UI 应该在数据可用之前显示来自第二个服务调用的字段的加载指示符。

我唯一能想到的是在第一个服务调用和第二个服务调用之后添加“tap”rxjs 操作符,它会在新主题上发出事件,并且 UI 可以监听 _documents$ 主题以显示之后的数据第一次调用,并在第二次调用后显示组合数据。

是否有处理这种情况的最佳做法?我是否遗漏了另一种模式或 rxjs 运算符来简化此操作?

  _documents$: BehaviorSubject<DocumentSearchSomething[]> = new BehaviorSubject([]);
  private documentSearchQuery$ = new Subject<string>();

  constructor(private store: Store, private http: HttpClient) {

    this.documentSearchQuery$.pipe(
      switchMap((query) => {
        return this.http.get<any[]>(`https://my_api.com​/service_1?q=${encodeURIComponent(query)}`)
          .pipe(
            tap(service_1_results => this._documents$.next(service_1_results)),
            switchMap(service_1_results => {
              // make call to service 2 to get more data for each result, pass in id of every result
              return this.http.post<any[]>(`https://my_api.com/service_2`, {
                ids: service_1_results.map(r => r.id)
              }).pipe(
                map(service_2_results => {
                  // combine service_1_results with service_2_results
                  const combined = service_1_results.map(r1 => {
                    const r2 = service_2_results.find(c => c.id === r1.id);
                    return {...r1, ...r2};
                  });
                  return combined;
                }),
                tap(combined_results => this._documents$.next(combined_results))
              );
            })
          );
      }),
    ).subscribe();
  }

【问题讨论】:

    标签: javascript angular rxjs


    【解决方案1】:

    是的,您可以使用一个运算符来简化此操作。由于此管道设置了一个内部 observable(这会导致发出组合值),因此您可以使用 startWith() 立即从内部 observable 发出服务一的值。

        _documents$: Observable<DocumentSearchSomething[]>;
    private documentSearchQuery = new Subject<string>();
    
    constructor(private store: Store, private http: HttpClient) {
    
      this._documents$ = this.documentSearchQuery.pipe(
        switchMap(query => this.http.get<any[]>(`https://my_api.com​/service_1?q=${encodeURIComponent(query)}`)),
        switchMap(serviceOne => this.HTMLOutputElement.post<any[]>(`https://my_api.com/service_2`, {
          ids: serviceOne.map(r => r.id)
        }).pipe(
          map(serviceTwo => serviceOne.map(r1 => {
            const r2 = serviceTwo.find(c => c.id === r1.id);
            return { ...r1, ...r2 };
          })),
          startWith(serviceOne)
        ))
      );
    
    }
    

    【讨论】:

    • startWith 已弃用,但您可以使用 concat
    • 真的吗?它没有记录为已弃用。 rxjs.dev/api/operators/startWith
    • 我确实发现了一个奇怪的区别 b/n OPs 代码和答案。 OPs 代码在发出第一个响应之前不会等待第二个 API 响应。而这里只有在第二个 API 发出后才会发出第一个响应。但我认为 OP 对这种差异没有任何问题,因为他已经接受了答案。
    • @MichaelD 当内部 observable 在运行时声明时,它会在第二次 API 调用之前立即发出 startWith() 中的值。
    • @digclo:这是我不知道的startWith 的一个很好的用例。谢谢。
    【解决方案2】:

    这样的?

      private documentSearchQuery$ = new Subject<string>();
    
      private documents1$: Observable<DocumentSearchSomething[]> =
        this.documentSearchQuery$.pipe(
          switchMap((query) =>
            this.http.get<DocumentSearchSomething[]>(
              `https://my_api.com​/service_1?q=${encodeURIComponent(query)}`
            )
          ),
          shareReplay({ bufferSize: 1, refCount: true })
        );
    
      private documents2$: Observable<DocumentSearchSomething[]> =
        this.documents1$.pipe(
          switchMap((res) =>
            this.http.post<any[]>(`https://my_api.com/service_2`, {
              ids: res.map((r) => r.id),
            })
          ),
          shareReplay({ bufferSize: 1, refCount: true })
        );
    
      private combinedResults$ = combineLatest([documents1$, documents2$]).pipe(
        map(([res1, res2]) =>
          res1.map((r1) => {
            const r2 = res2.find((c) => c.id === r1.id);
            return { ...r1, ...r2 };
          })
        )
      );
    

    【讨论】:

    • 注意:这假设您需要访问 (documents1$ 和/或 documents2$) 和 combinedResults$。如果您只关心combinedResults$,您可以简化为 digclo 所做的事情
    【解决方案3】:

    如果您正在处理具有任意长度的数组响应,并且需要响应中每个项目的详细信息,那么无需进入索引跟踪、主题和合并的 Observable 的更高级配置,您可以尝试类似这样的操作:

    https://stackblitz.com/edit/angular-ivy-ae7sdt?file=src/app/app.component.ts

    在本例中,details 键被添加到预期响应数组中的每个元素。这个属性被映射到一个带有初始值的 Observable,表示正在加载。

        this.todos$ = this.getTodos().pipe(
          map((todos: Todo[]) =>
            todos.map(todo => ({
              ...todo,
              details: this.getTodo(todo.id).pipe(
                startWith({ completed: 'loading' }),
                map(resp => String(resp.completed))
              )
            }))
          )
        );
    

    然后,在模板中,您将使用异步管道绑定到结果。我认为这不是最佳做法,但对于一个简短、简单的解决方案,它应该可以工作。

    更可靠的答案是将BehaviorSubject 与一个数组和一个暴露其值的可观察对象一起使用。该数组将映射到父数组中每个项目的附加详细信息。然后,您可以像这样在模板中迭代两者:

    <ng-container *ngIf="{
        todos: todos$ | async,
        todoDetails: todoDetails$ | async
    } as state ">
    
      <div *ngFor="let todo of state.todos; let i =index">
        {{ todo.title }}
        {{ state.todoDetails[i] }}
      </div>
    </ng-container>
    

    【讨论】:

      猜你喜欢
      • 2017-07-11
      • 2017-07-17
      • 2021-07-17
      • 1970-01-01
      • 1970-01-01
      • 2018-10-12
      • 2016-10-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多