在Angular中重试失败的HTTP请求

对服务器数据的访问组织是几乎所有一页应用程序的基础。 这些应用程序中的所有动态内容都是从后端下载的。

在大多数情况下,对服务器的HTTP请求可以可靠地工作并返回所需的结果。 但是,在某些情况下,请求可能会失败。

想象一下有人通过火车以每小时200公里的速度在全国各地行驶的访问点如何与您的网站合作。 在这种情况下,网络连接可能很慢,但是尽管如此,服务器请求还是可以完成任务。

但是,如果火车进入隧道怎么办? 与Internet的连接很有可能会中断,并且Web应用程序将无法“延伸”到服务器。 在这种情况下,列车离开隧道并恢复Internet连接后,用户将必须重新加载应用程序页面。

重新加载页面可能会影响应用程序的当前状态。 这意味着,例如,用户可能会丢失他输入到表单中的数据。

与其简单地与某个请求不成功这一事实相协调,不如将其重复几次并向用户显示相应的通知会更好。 使用这种方法,当用户意识到应用程序正在尝试解决该问题时,他很可能不会重新加载页面。



该材料(我们今天发布的翻译)致力于分析在Angular应用程序中重复未成功请求的几种方式。

重复失败的请求


让我们重现火车上使用Internet工作的用户可能遇到的情况。 我们将创建一个后端,该后端在访问请求的前三次尝试期间将错误地处理该请求,仅从第四次尝试返回数据。
通常,我们使用Angular创建服务,连接HttpClient并使用它从后端获取数据。

 import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError} from 'rxjs/operators'; @Injectable() export class GreetingService {  private GREET_ENDPOINT = 'http://localhost:3000';  constructor(private httpClient: HttpClient) {  }  greet(): Observable<string> {    return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(      catchError(() => {        //           return EMPTY;      })    );  } } 

这里没有什么特别的。 我们插入Angular HttpClient模块并执行一个简单的GET请求。 如果请求返回错误,我们将执行一些代码来处理该错误,并返回一个空的Observable (可观察对象),以告知发起该请求的原因。 这段代码说的是:“有一个错误,但是一切正常,我可以处理。”

大多数应用程序都以这种方式执行HTTP请求。 在上面的代码中,该请求仅执行一次。 之后,它要么返回从服务器接收到的数据,要么不成功。

如果端点/greet不可用或返回错误,如何重复请求? 也许有合适的RxJS语句? 当然存在。 RxJS具有所有操作符。

在这种情况下首先想到的是retry 。 让我们看一下它的定义:“返回一个播放原始Observable的Observable,但error除外。 如果原始Observable调用error ,则此方法将重新订阅原始Observable,而不是传播错误。

重新订阅的最大数量限制为count (这是传递给该方法的数字参数)。”

retry与我们需要的非常相似。 因此,让我们将其嵌入到我们的链中。

 import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {EMPTY, Observable} from 'rxjs'; import {catchError, retry, shareReplay} from 'rxjs/operators'; @Injectable() export class GreetingService {  private GREET_ENDPOINT = 'http://localhost:3000';  constructor(private httpClient: HttpClient) {  }  greet(): Observable<string> {    return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(      retry(3),      catchError(() => {        //           return EMPTY;      }),      shareReplay()    );  } } 

我们已经成功使用了retry运算符。 让我们看一下这如何影响在实验性应用程序中执行的HTTP请求的行为。 这是一个很大的GIF文件,显示了此应用程序的屏幕以及浏览器开发人员工具的“网络”标签。 您将在这里找到更多此类演示。

我们的应用程序非常简单。 单击PING THE SERVER按钮时,它只是发出一个HTTP请求。

如前所述,后端在执行前三个尝试执行对它的请求时返回错误,而当第四个请求到达时,它返回正常响应。

在网络开发人员的工具标签上,您可以看到retry解决了分配给它的任务,并重复执行了失败的请求3次。 最后一次尝试成功,应用程序收到响应,页面上显示相应的消息。

这一切都很好。 现在,应用程序可以重复失败的请求。

但是,此示例仍可以改进。 请注意,现在重复的请求将在执行失败的请求后立即执行。 系统的这种行为在我们的情况下不会带来太多好处-当火车进入隧道并且互联网连接暂时断开时。

延迟重试失败的请求


进入隧道的火车不会立即离开。 他在那里呆了一段时间。 因此,我们需要“延长”对服务器执行重复请求的时间。 您可以通过推迟重试来做到这一点。

为此,我们需要更好地控制执行重复请求的过程。 我们需要能够决定何时确切重复请求。 这意味着retry运算符的功能对我们而言已不再足够。 因此,我们再次转向RxJS上的文档。

文档包含对retryWhen的描述,这似乎很适合我们。 在文档中,其描述如下:“返回一个播放原始Observable的Observable,但error除外。 如果原始的Observable调用error ,则此方法将引发Throwable,从而导致错误,并从notifier返回的Observable。 如果此Observable调用completeerror ,则此方法将在子订阅上调用completeerror 。 否则,此方法将重新订阅原始的Observable。”

是的,定义并不简单。 让我们用一种更易于理解的语言来描述它。

retryWhen接受返回Observable的回调。 返回的Observable根据某些规则决定retryWhen运算符的行为。 即,这是retryWhen运算符的retryWhen

  • 如果返回的Observable引发错误,它将停止工作并引发错误。
  • 如果返回的Observable报告完成,则退出。
  • 在其他情况下,当Observable成功返回时,它将重复执行原始的Observable

仅当原始Observable首次引发错误时才调用回调。

现在,我们可以使用该知识通过RxJS retryWhen为失败的请求创建延迟的重试机制。

 retryWhen((errors: Observable<any>) => errors.pipe(    delay(delayMs),    mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxEntry))    )) ) 

如果原始的Observable(即我们的HTTP请求)返回错误,则retryWhen语句。 在回调中,我们可以访问导致失败的错误。 我们推迟errors ,减少重试次数,并返回引发错误的新Observable。

根据retryWhen的规则,此Observable由于retryWhen ,因此retryWhen请求。 如果重复多次失败,并且retries变量的值减小为0,则我们将以执行请求时发生的错误结束任务。

太好了! 显然,我们可以将上面的代码替换为链中的retry运算符。 但是在这里我们放慢了一点。

如何retries可变retries ? 此变量包含失败的请求重试系统的当前状态。 她在哪里宣布? 条件何时重置? 需要在流内部而不是外部对状态进行管理。

▍创建自己的delayRetry语句


通过将以上代码编写为单独的RxJS运算符,我们可以解决状态管理问题并提高代码的可读性。

有多种方法可以创建自己的RxJS运算符。 使用哪种方法取决于特定运算符的结构。

我们的运算符基于现有RxJS运算符。 结果,我们可以使用最简单的方法来创建我们自己的运算符。 在我们的例子中,RxJs运算符只是一个具有以下签名的函数:

 const customOperator = (src: Observable<A>) => Observable<B> 

该语句采用原始的Observable,并返回另一个Observable。

由于我们的运算符允许用户指定重复请求应执行的频率以及需要执行多少次,因此我们需要将上述函数声明包装在工厂函数中,该函数需要delayMsmaxRetry之间的延迟)和maxRetry (最大重复次数)。

 const customOperator = (delayMs: number, maxRetry: number) => {   return (src: Observable<A>) => Observable<B> } 

如果要创建不基于现有运算符的运算符,则需要注意处理错误和订阅。 此外,您将需要扩展Observable类并实现lift功能。

如果您有兴趣,请在这里看看。

因此,基于以上代码片段,让我们编写我们自己的RxJs运算符。

 import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) =>  `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up`; const DEFAULT_MAX_RETRIES = 5; export function delayedRetry(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES) {  let retries = maxRetry;  return (src: Observable<any>) =>    src.pipe(      retryWhen((errors: Observable<any>) => errors.pipe(        delay(delayMs),        mergeMap(error => retries-- > 0 ? of(error) : throwError(getErrorMessage(maxRetry))        ))      )    ); } 

太好了 现在,我们可以将此运算符导入客户端代码。 我们将在执行HTTP请求时使用它。

 return this.httpClient.get<string>(`${this.GREET_ENDPOINT}/greet`).pipe(        delayedRetry(1000, 3),        catchError(error => {            console.log(error);            //               return EMPTY;        }),        shareReplay()    ); 

我们将delayedRetry运算符放在链中,并传递数字1000和3作为参数,第一个参数设置delayedRetry重复请求之间的延迟(以毫秒为单位)。 第二个参数确定重复请求的最大数量。

重新启动应用程序,并查看新操作员的工作方式。

在使用浏览器开发人员的工具分析了程序的行为之后,我们可以看到重复执行请求的尝试的执行被延迟了一秒钟。 收到正确的请求答案后,相应的消息将出现在应用程序窗口中。

指数请求暂停


让我们提出延迟失败请求重试的想法。 以前,我们总是在同一时间延迟每个重复请求的执行。

这里我们讨论如何增加每次尝试后的延迟。 重试请求的第一次尝试是在一秒钟后进行,第二秒钟是在两秒钟后,第三遍是三秒钟。

创建一个新语句retryWithBackoff ,以实现此行为。

 import {Observable, of, throwError} from 'rxjs'; import {delay, mergeMap, retryWhen} from 'rxjs/operators'; const getErrorMessage = (maxRetry: number) =>  `Tried to load Resource over XHR for ${maxRetry} times without success. Giving up.`; const DEFAULT_MAX_RETRIES = 5; const DEFAULT_BACKOFF = 1000; export function retryWithBackoff(delayMs: number, maxRetry = DEFAULT_MAX_RETRIES, backoffMs = DEFAULT_BACKOFF) {  let retries = maxRetry;  return (src: Observable<any>) =>    src.pipe(      retryWhen((errors: Observable<any>) => errors.pipe(        mergeMap(error => {            if (retries-- > 0) {              const backoffTime = delayMs + (maxRetry - retries) * backoffMs;              return of(error).pipe(delay(backoffTime));            }            return throwError(getErrorMessage(maxRetry));          }        )))); } 

如果在应用程序中使用此运算符并对其进行测试,则可以看到每次执行新尝试后执行重复请求的延迟如何增加。

每次尝试后,我们都会等待一定时间,重复请求并增加等待时间。 在这里,照常,在服务器返回正确的请求答案之后,我们在应用程序窗口中显示一条消息。

总结


重复失败的HTTP请求可使应用程序更稳定。 当执行非常重要的查询时,如果没有获取到的数据,应用程序将无法正常运行,这尤其重要。 例如,它可以是包含应用程序需要与之交互的服务器地址的配置数据。

在大多数情况下,RxJs重试retry不足以为失败的请求提供可靠的重试系统。 retryWhen为开发人员提供了对重复请求的更高级别的控制。 它允许您配置重复请求的间隔。 由于该操作员的能力,可以实现延迟重复方案或指数延迟重复。

在实现适合在RxJS链中重用的行为模式时,建议将其格式化为新的运算符。 这是本文中使用代码存储库。

亲爱的读者们! 您如何解决重试失败的HTTP请求的问题?

Source: https://habr.com/ru/post/zh-CN459302/


All Articles