Angular: 如何return 从http 请求列表中第一个成功响应

Angular: how to return the first successful response from the list of http requests

我有一个服务器 url 列表,并在循环中向它们发出连续的 http 请求。当成功响应从当前请求到达时,我想打破循环而不是调用所有其他服务器。有人可以建议我如何在 Angular/RxJS 中处理吗?类似于:

  getClientData() {
        for(let server of this.httpsServersList) {
                    var myObservable = this.queryData(server)
                        .pipe(
                            map((response: any) => {
                                const data = (response || '').trim();
                                
                                if(data && this.dataIsCorrect(data)) {
                                    return data; // **here I want to break from the loop!**
                                }
                            })
                        );
                    return myObservable;
                 }
  }     

  private queryData(url: string) {        
     return this.http.get(url, { responseType: 'text' });
  }

在 angular 中,我们依赖 RxJS 运算符进行此类复杂调用 如果您想同时调用所有这些,那么一旦其中一个被满足或拒绝以取消您应该使用的其他调用 RxJS 竞赛 learnrxjs.io/learn-rxjs/operators/combination/race 或者没有 RxJS 你可以使用 Promise.race

但是,如果您想并行调用它们并等到第一次满足“未拒绝”或所有这些都被拒绝,Promise.any 就是这种情况 不幸的是,它没有 RxJS 运算符,但在下面的文章中,您可以看到如何为 Promise.any 实现此自定义运算符以及该运算符的示例 https://tmair.dev/blog/2020/08/promise-any-for-observables/

您不能将 race because it will call all URLs in parallel, but you can use switchMap 用于递归实现

import { of, Observable, throwError } from 'rxjs';
import { catchError, switchMap } from 'rxjs/operators'

function getClientData(urls: string[]) {
  // check if remaining urls
  if (!urls.length) throw throwError(new Error('all urls have a error'));  ;

  return queryData(urls[0]).pipe(
    switchMap((response) => {
      const data = (response || '').trim();
                                
      if(data && this.dataIsCorrect(data))
        // if response is correct, return an observable with the data
        // for that we use of() observable
        return of(data)

      // if response is not correct, we call one more time the function with the next url
      return getClientData(urls.slice(1))
    }),
    catchError(() => getClientData(urls.slice(1)))
  );
}

function queryData(url: string): Observable<unknown> {        
  return this.http.get(url, { responseType: 'text' });
}

创建这样的主题

responseArrived=new Subject();

并在管道后像这样添加 takeuntil

 var myObservable = this.queryData(server).pipe(takeUntil(responseArrived),map...

并且在代码行return数据中调用

responseArrived.next()

IMO 最好避免使用 for 循环来订阅多个可观察对象。它可能会导致多个开放订阅。这种情况下使用的常用函数是 RxJS forkJoin. But given your specific condition, I'd suggest using RxJS from function with concatMap operator to iterator each element in order and takeWhile 运算符,其 inclusive 参数设置为 true (感谢@Chris)以根据条件停止并 return 最后一个值.

import { from } from 'rxjs';
import { concatMap, filter, map, takeWhile } from 'rxjs/operators';

getClientData(): Observable<any> {
  return from(this.httpsServersList).pipe(
    concatMap((server: string) => this.queryData(server)),
    map((response: any) => (response || '').trim()),
    filter((data: string) => !!data && this.dataIsCorrect(data)) // <-- ignore empty or undefined and invalid data
    takeWhile(((data: string) =>                                 // <-- close stream when data is valid and condition is true
      !data || !this.dataIsCorrect(data)
    ), true)
  );
}

注意:尝试调整 takeWhile 谓词中的条件以满足您的要求。

编辑 1:在 takeWhile opeartor

中添加 inclusive 参数

编辑 2:在 filter 运算符中添加附加条件

如果您的唯一条件是在至少收到一个响应后取消请求,难道不能简单地取消订阅从 HttpClient 调用返回的可观察对象吗?

  getData() {
    const subscriptions = [];
    [
      'https://reqres.in/api/products/1',
      'https://reqres.in/api/products/2',
      'https://reqres.in/api/products/3',
    ].forEach((url, i) => {
      subscriptions[i] = this.getClientData(url).subscribe(() => {
        // Unsubscribe
        subscriptions.forEach((v, j) => {
          if (j !== i) {
            console.log('Unsubscribe from ', j);
            v.unsubscribe();
          }
        });
      });
    });
  }
  private getClientData(url: string) {
    return this.httpClient.get(url, { responseType: 'text' }).pipe(
      map((response: any) => {
        const data = (response || '').trim();
        if (data && true) return data;
        return null;
      })
    );
  }