MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Angular HTTP请求的错误处理与重试机制

2022-03-012.1k 阅读

一、Angular HTTP 请求基础回顾

在深入探讨错误处理与重试机制之前,我们先来简单回顾一下 Angular 中 HTTP 请求的基本使用。Angular 通过 @angular/common/http 模块提供了强大的 HTTP 客户端功能。

首先,在模块中导入 HttpClientModule

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';

@NgModule({
  imports: [BrowserModule, HttpClientModule],
  declarations: [AppComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

然后,在组件中注入 HttpClient 来发起请求。例如,发起一个 GET 请求:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app - component',
  templateUrl: './app.component.html'
})
export class AppComponent {
  constructor(private http: HttpClient) {}

  getData() {
    this.http.get('https://example.com/api/data').subscribe((response) => {
      console.log(response);
    });
  }
}

二、HTTP 请求错误类型

2.1 网络错误(Network Error)

网络错误通常发生在请求无法到达服务器时,比如网络连接中断、服务器地址错误等。在 Angular 中,当发生网络错误时,subscribeerror 回调会收到一个 HttpErrorResponse 对象,其 error 属性是一个 ProgressEvent 对象。例如:

this.http.get('https://nonexistent.example.com/api/data').subscribe(
  (response) => {
    console.log(response);
  },
  (error: HttpErrorResponse) => {
    if (error.error instanceof ProgressEvent) {
      console.log('Network error occurred:', error.message);
    }
  }
);

2.2 服务器端错误(Server - side Error)

服务器端错误是指服务器接收到请求,但处理过程中出现问题并返回了错误状态码。常见的状态码如 400(Bad Request)、401(Unauthorized)、404(Not Found)、500(Internal Server Error)等。HttpErrorResponse 对象的 status 属性会包含这些状态码,error 属性通常会包含服务器返回的错误信息(如果有)。

this.http.get('https://example.com/api/nonexistent - endpoint').subscribe(
  (response) => {
    console.log(response);
  },
  (error: HttpErrorResponse) => {
    if (error.status === 404) {
      console.log('Resource not found:', error.message);
    }
  }
);

2.3 客户端错误(Client - side Error)

客户端错误可能是由于代码逻辑问题导致的,比如传递了错误的请求参数。虽然这种错误不是直接由 HTTP 层面产生,但也会影响请求的成功执行。例如,在 POST 请求中传递了不符合服务器预期格式的数据:

const wrongData = { incorrectField: 'value' };
this.http.post('https://example.com/api/create - data', wrongData).subscribe(
  (response) => {
    console.log(response);
  },
  (error: HttpErrorResponse) => {
    if (error.status === 400) {
      console.log('Client - side error:', error.message);
    }
  }
);

三、错误处理策略

3.1 全局错误处理

在 Angular 中,可以通过创建一个全局的错误处理服务来统一处理 HTTP 请求错误。首先,创建一个错误处理服务:

import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable, catchError } from 'rxjs';

@Injectable()
export class GlobalErrorHandlerService implements HttpInterceptor {
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
      catchError((error: HttpErrorResponse) => {
        console.log('Global error handling:', error.message);
        // 可以在这里进行全局的错误记录、通知等操作
        throw error;
      })
    );
  }
}

然后,在模块中提供这个拦截器:

@NgModule({
  imports: [BrowserModule, HttpClientModule],
  declarations: [AppComponent],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: GlobalErrorHandlerService,
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

3.2 局部错误处理

除了全局错误处理,在每个具体的请求处也可以进行局部错误处理。在 subscribeerror 回调中处理错误:

this.http.get('https://example.com/api/data').subscribe(
  (response) => {
    console.log(response);
  },
  (error: HttpErrorResponse) => {
    if (error.status === 401) {
      console.log('Unauthorized. Please log in.');
    } else if (error.status === 500) {
      console.log('Server is having issues. Please try again later.');
    }
  }
);

四、重试机制原理

4.1 为什么需要重试机制

在实际应用中,HTTP 请求失败并不一定意味着是永久性的错误。例如,网络抖动可能导致短暂的连接中断,服务器可能因为临时负载过高而返回错误。通过重试机制,可以在一定程度上提高请求的成功率,增强应用的稳定性。

4.2 重试机制的实现思路

实现重试机制的关键在于使用 RxJS 的操作符。retry 操作符可以在 Observable 发出错误时,重新订阅该 Observable 一定次数。例如,使用 retry(3) 会在错误发生时重试请求 3 次。

import { catchError, retry } from 'rxjs/operators';

this.http.get('https://example.com/api/data').pipe(
  retry(3),
  catchError((error: HttpErrorResponse) => {
    console.log('All retries failed:', error.message);
    throw error;
  })
).subscribe((response) => {
  console.log(response);
});

五、实现自定义重试机制

5.1 基于延迟的重试

有时候,简单的重试可能不足以解决问题,因为错误可能是由于服务器暂时过载等原因导致的。在这种情况下,可以在重试之间引入延迟。我们可以使用 retryWhen 操作符来自定义重试逻辑。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';

@Injectable()
export class DataService {
  constructor(private http: HttpClient) {}

  getData(): Observable<any> {
    return this.http.get('https://example.com/api/data').pipe(
      retryWhen((errors) =>
        errors.pipe(
          delay(2000), // 每次重试延迟 2 秒
          take(3) // 最多重试 3 次
        )
      )
    );
  }
}

5.2 条件重试

在某些场景下,可能只希望对特定类型的错误进行重试。例如,只对网络错误(通常表现为 HttpErrorResponseerrorProgressEvent)进行重试。

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs';
import { retryWhen, delay, take, filter } from 'rxjs/operators';

@Injectable()
export class ConditionalRetryService {
  constructor(private http: HttpClient) {}

  getData(): Observable<any> {
    return this.http.get('https://example.com/api/data').pipe(
      retryWhen((errors) =>
        errors.pipe(
          filter((error: HttpErrorResponse) => error.error instanceof ProgressEvent),
          delay(1000),
          take(2)
        )
      )
    );
  }
}

六、错误处理与重试机制的结合使用

在实际项目中,通常会将错误处理和重试机制结合起来。首先进行重试,如果重试失败再进行详细的错误处理。

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, retryWhen, delay, take } from 'rxjs/operators';

@Component({
  selector: 'app - component',
  templateUrl: './app.component.html'
})
export class AppComponent {
  constructor(private http: HttpClient) {}

  getData() {
    this.http.get('https://example.com/api/data').pipe(
      retryWhen((errors) =>
        errors.pipe(
          delay(1500),
          take(3)
        )
      ),
      catchError((error: HttpErrorResponse) => {
        if (error.status === 401) {
          console.log('Unauthorized. Please log in.');
        } else if (error.status === 500) {
          console.log('Server is having issues. Please try again later.');
        }
        throw error;
      })
    ).subscribe((response) => {
      console.log(response);
    });
  }
}

七、处理复杂场景下的重试

7.1 重试与缓存结合

在一些场景中,可能希望在重试时能够利用缓存的数据。假设我们有一个简单的缓存服务:

import { Injectable } from '@angular/core';

@Injectable()
export class CacheService {
  private cache: { [key: string]: any } = {};

  setCache(key: string, value: any) {
    this.cache[key] = value;
  }

  getCache(key: string) {
    return this.cache[key];
  }
}

然后在请求中结合缓存和重试:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError, retryWhen, delay, take } from 'rxjs/operators';
import { CacheService } from './cache.service';

@Component({
  selector: 'app - component',
  templateUrl: './app.component.html'
})
export class AppComponent {
  constructor(private http: HttpClient, private cacheService: CacheService) {}

  getData() {
    const cacheKey = 'https://example.com/api/data';
    const cachedData = this.cacheService.getCache(cacheKey);
    if (cachedData) {
      console.log('Using cached data:', cachedData);
      return;
    }

    this.http.get(cacheKey).pipe(
      retryWhen((errors) =>
        errors.pipe(
          delay(1000),
          take(3)
        )
      ),
      catchError((error: HttpErrorResponse) => {
        console.log('Error after retries:', error.message);
        throw error;
      })
    ).subscribe((response) => {
      this.cacheService.setCache(cacheKey, response);
      console.log('New data received and cached:', response);
    });
  }
}

7.2 重试队列

当有多个请求需要重试时,可以创建一个重试队列。我们可以使用 RxJS 的 SubjectBehaviorSubject 来管理这个队列。

首先,创建一个重试队列服务:

import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';

@Injectable()
export class RetryQueueService {
  private retryQueue: { request: Observable<any>, attempts: number }[] = [];
  private retryQueueSubject = new Subject<void>();

  addToQueue(request: Observable<any>, attempts = 3) {
    this.retryQueue.push({ request, attempts });
    this.retryQueueSubject.next();
  }

  getQueueObservable(): Observable<void> {
    return this.retryQueueSubject.asObservable();
  }

  processQueue() {
    const { request, attempts } = this.retryQueue.shift() || { request: null, attempts: 0 };
    if (request) {
      request.pipe(
        retryWhen((errors) =>
          errors.pipe(
            delay(1000),
            take(attempts)
          )
        )
      ).subscribe(
        (response) => {
          console.log('Request in queue successful:', response);
        },
        (error) => {
          console.log('Request in queue failed after retries:', error.message);
        },
        () => {
          if (this.retryQueue.length > 0) {
            this.processQueue();
          }
        }
      );
    }
  }
}

在组件中使用重试队列服务:

import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { RetryQueueService } from './retry - queue.service';

@Component({
  selector: 'app - component',
  templateUrl: './app.component.html'
})
export class AppComponent {
  constructor(private http: HttpClient, private retryQueueService: RetryQueueService) {}

  makeRequest() {
    const request = this.http.get('https://example.com/api/data');
    this.retryQueueService.addToQueue(request);
  }

  ngOnInit() {
    this.retryQueueService.getQueueObservable().subscribe(() => {
      this.retryQueueService.processQueue();
    });
  }
}

八、测试错误处理与重试机制

8.1 单元测试错误处理

在测试错误处理时,可以使用 Angular 的测试工具和 RxJS 的测试助手。例如,测试一个组件的错误处理逻辑:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  let component: AppComponent;
  let fixture: ComponentFixture<AppComponent>;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      declarations: [AppComponent]
    });

    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should handle 404 error', () => {
    const errorResponse = new ErrorEvent('error', { message: 'Not Found' });
    const mockReq = httpMock.expectOne('https://example.com/api/data');
    mockReq.flush(null, { status: 404, statusText: 'Not Found' });

    spyOn(console, 'log');
    component.getData();

    expect(console.log).toHaveBeenCalledWith('Resource not found: Not Found');
  });
});

8.2 单元测试重试机制

测试重试机制时,需要模拟多次错误发生。

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { AppComponent } from './app.component';
import { of, throwError } from 'rxjs';

describe('AppComponent Retry', () => {
  let component: AppComponent;
  let fixture: ComponentFixture<AppComponent>;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      declarations: [AppComponent]
    });

    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should retry 3 times on error', () => {
    const mockReq = httpMock.expectOne('https://example.com/api/data');
    mockReq.flush(null, { status: 500, statusText: 'Internal Server Error' });

    const spy = spyOn(component, 'getData').and.callThrough();
    component.getData();

    const retries = 3;
    for (let i = 0; i < retries; i++) {
      mockReq = httpMock.expectOne('https://example.com/api/data');
      mockReq.flush(null, { status: 500, statusText: 'Internal Server Error' });
    }

    mockReq = httpMock.expectOne('https://example.com/api/data');
    mockReq.flush({ data: 'Success' });

    expect(spy).toHaveBeenCalledTimes(retries + 1);
  });
});

九、性能考虑

9.1 重试次数与延迟的权衡

重试次数过多或者重试延迟过长可能会导致性能问题。过多的重试会占用网络资源,延长用户等待时间。过长的延迟会让用户觉得应用反应迟钝。因此,需要根据实际业务场景和服务器性能来合理设置重试次数和延迟时间。例如,对于一些对实时性要求较高的请求,可以减少重试次数和缩短延迟;对于一些非关键数据的请求,可以适当增加重试次数和延迟时间。

9.2 避免无限重试循环

在实现重试机制时,一定要确保有终止条件,避免出现无限重试循环。这可以通过设置最大重试次数(如使用 take 操作符)来实现。如果没有终止条件,一旦请求一直失败,将会不断消耗系统资源,最终可能导致应用崩溃。

十、在生产环境中的注意事项

10.1 错误日志记录

在生产环境中,详细的错误日志记录至关重要。不仅要记录错误信息,还要记录请求的 URL、请求方法、请求参数以及错误发生的时间等信息。这有助于开发人员快速定位问题。可以使用一些日志记录服务,如 Sentry,它可以与 Angular 应用集成,方便地收集和管理错误日志。

10.2 重试机制的监控

对重试机制进行监控也是必要的。了解哪些请求经常需要重试,以及重试的成功率等指标,可以帮助优化系统。可以通过自定义埋点或者使用一些 APM(应用性能监控)工具来实现对重试机制的监控。

10.3 用户体验优化

虽然重试机制可以提高请求成功率,但也要注意对用户体验的影响。在重试过程中,可以向用户显示适当的加载提示,告知用户系统正在尝试重新获取数据。如果重试失败,要给出友好的错误提示,引导用户采取进一步的操作,如刷新页面或联系客服。