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

Angular HTTP请求的并发请求处理与控制

2023-02-133.4k 阅读

一、Angular HTTP 模块简介

在深入探讨 Angular HTTP 请求的并发处理与控制之前,我们先来简单回顾一下 Angular 的 HTTP 模块。Angular 提供了 @angular/common/http 模块来处理 HTTP 请求。这个模块基于 RxJS,提供了简洁且功能强大的 API 来发起各种类型的 HTTP 请求,如 GET、POST、PUT、DELETE 等。

首先,在使用 HTTP 模块之前,需要在 Angular 应用的模块中导入 HttpClientModule。例如,在 app.module.ts 中:

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 实例来发起请求。例如:

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

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

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

二、并发请求的场景与问题

在实际的前端开发中,经常会遇到需要同时发起多个 HTTP 请求的场景。比如,一个页面可能需要从不同的 API 端点获取用户信息、产品列表以及最新的公告等数据。这时候就需要进行并发请求。

然而,并发请求也带来了一些问题:

  1. 资源竞争:多个请求同时占用网络资源,如果请求过多,可能导致网络拥堵,使每个请求的响应时间变长。
  2. 响应顺序不确定:由于网络环境等因素,多个并发请求的响应顺序可能与发起顺序不同。这可能会给前端处理数据带来困扰,特别是当某些数据依赖于其他请求的结果时。
  3. 错误处理复杂:当多个请求并发执行时,一个请求出错可能需要特殊处理,同时还要考虑其他请求是否继续执行或者如何回滚已经执行的操作。

三、使用 RxJS 操作符处理并发请求

RxJS(Reactive Extensions for JavaScript)是 Angular 处理异步操作的核心库。它提供了一系列强大的操作符来处理并发请求。

  1. forkJoin
    • 原理forkJoin 操作符会等待所有传入的 Observable 都发出完成(complete)通知后,将所有 Observable 发出的最后一个值以数组的形式作为结果发出。如果其中任何一个 Observable 发出错误(error),forkJoin 会立即发出错误并停止等待其他 Observable。
    • 示例:假设我们有两个 API 端点,一个获取用户信息,另一个获取用户的订单列表。
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { forkJoin } from 'rxjs';

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

  makeConcurrentRequests() {
    const userInfo$ = this.http.get('https://example.com/api/user');
    const orderList$ = this.http.get('https://example.com/api/orders');

    forkJoin([userInfo$, orderList$]).subscribe(([userInfo, orderList]) => {
      console.log('User Info:', userInfo);
      console.log('Order List:', orderList);
    });
  }
}
  • 应用场景:适用于多个请求相互独立,并且所有请求的结果都需要在后续逻辑中使用的场景。比如在一个用户详情页面,需要同时获取用户基本信息、订单信息、收货地址等多个独立的数据。
  1. combineLatest
    • 原理combineLatest 操作符会在任何一个传入的 Observable 发出新值时,将所有 Observable 的最新值组合成一个数组并发出。与 forkJoin 不同的是,combineLatest 不需要所有 Observable 都完成,只要有一个 Observable 发出新值就会触发组合操作。
    • 示例:假设我们有一个组件,其中一个 Observable 实时监听用户的语言设置变化,另一个 Observable 获取当前用户的偏好设置。当语言设置或偏好设置变化时,我们需要重新加载相关的内容。
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { combineLatest } from 'rxjs';
import { BehaviorSubject } from 'rxjs';

@Component({
  selector: 'app - combined - component',
  templateUrl: './combined - component.html'
})
export class CombinedComponent {
  language$ = new BehaviorSubject<string>('en');
  constructor(private http: HttpClient) {}

  makeCombinedRequests() {
    const preference$ = this.http.get('https://example.com/api/preference');

    combineLatest([this.language$, preference$]).subscribe(([language, preference]) => {
      console.log('Language:', language);
      console.log('Preference:', preference);
      // 根据语言和偏好重新加载内容的逻辑
    });
  }
}
  • 应用场景:适用于多个数据流相互关联,任何一个数据流的变化都需要触发相关逻辑的场景。比如在一个多语言的应用中,用户偏好设置和语言设置都可能影响页面的显示,当其中任何一个变化时,都需要重新渲染页面。
  1. zip
    • 原理zip 操作符会将多个 Observable 发出的值按顺序组合成一个数组并发出。它会等待所有 Observable 都发出一个值后才开始组合并发出结果。如果其中一个 Observable 先完成,zip 会等待其他 Observable 继续发出值,直到所有 Observable 都完成。
    • 示例:假设我们有两个 Observable,一个按顺序发出数字 1、2、3,另一个按顺序发出字母 'a'、'b'、'c',我们希望将它们按顺序组合。
import { Component } from '@angular/core';
import { zip } from 'rxjs';
import { Observable } from 'rxjs';

@Component({
  selector: 'app - zip - component',
  templateUrl: './zip - component.html'
})
export class ZipComponent {
  constructor() {}

  makeZipRequests() {
    const number$ = new Observable<number>(observer => {
      setTimeout(() => { observer.next(1); }, 1000);
      setTimeout(() => { observer.next(2); }, 2000);
      setTimeout(() => { observer.next(3); }, 3000);
      setTimeout(() => { observer.complete(); }, 4000);
    });
    const letter$ = new Observable<string>(observer => {
      setTimeout(() => { observer.next('a'); }, 1500);
      setTimeout(() => { observer.next('b'); }, 2500);
      setTimeout(() => { observer.next('c'); }, 3500);
      setTimeout(() => { observer.complete(); }, 4500);
    });

    zip(number$, letter$).subscribe(([number, letter]) => {
      console.log('Combined:', number, letter);
    });
  }
}
  • 应用场景:适用于需要将多个 Observable 按顺序组合的场景,比如在一些数据同步的场景中,一个数据流表示时间戳,另一个数据流表示对应时间戳的数据,zip 可以将它们按顺序组合起来。

四、并发请求的控制

  1. 限制并发请求数量
    • 原理:在实际应用中,为了避免网络资源过度占用,我们可能需要限制同时执行的请求数量。可以使用 RxJS 的 concatMapbufferCount 操作符来实现。concatMap 会按顺序处理 Observable 发出的值,bufferCount 可以控制每次处理的数量。
    • 示例:假设我们有一个数组,数组中的每个元素都对应一个 API 请求,我们希望同时最多执行 3 个请求。
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { from } from 'rxjs';
import { concatMap, bufferCount } from 'rxjs/operators';

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

  makeLimitedRequests() {
    const urls = ['https://example.com/api/data1', 'https://example.com/api/data2', 'https://example.com/api/data3', 'https://example.com/api/data4', 'https://example.com/api/data5'];
    const requests$ = from(urls).pipe(
      bufferCount(3),
      concatMap(batch => {
        const batchRequests = batch.map(url => this.http.get(url));
        return forkJoin(batchRequests);
      })
    );

    requests$.subscribe(results => {
      console.log('Batch Results:', results);
    });
  }
}
  • 解释:首先,from(urls) 将数组转换为 Observable。bufferCount(3) 将 Observable 发出的值按 3 个一组进行分组。然后,concatMap 对每组数据进行处理,将每组中的每个 URL 转换为一个 HTTP 请求,并使用 forkJoin 等待这一组的所有请求完成。这样就实现了同时最多执行 3 个请求。
  1. 取消并发请求
    • 原理:在某些情况下,比如用户在请求还未完成时进行了页面切换,我们需要取消正在执行的并发请求。在 Angular 中,可以使用 Subscription 对象来管理请求,并在需要时调用 unsubscribe 方法取消请求。
    • 示例
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { forkJoin } from 'rxjs';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app - cancel - component',
  templateUrl: './cancel - component.html'
})
export class CancelComponent {
  private subscriptions: Subscription[] = [];
  constructor(private http: HttpClient) {}

  makeCancelableRequests() {
    const userInfo$ = this.http.get('https://example.com/api/user');
    const orderList$ = this.http.get('https://example.com/api/orders');

    const combined$ = forkJoin([userInfo$, orderList$]);
    const subscription = combined$.subscribe(([userInfo, orderList]) => {
      console.log('User Info:', userInfo);
      console.log('Order List:', orderList);
    });
    this.subscriptions.push(subscription);
  }

  cancelRequests() {
    this.subscriptions.forEach(sub => sub.unsubscribe());
    this.subscriptions = [];
  }
}
  • 解释:在 makeCancelableRequests 方法中,我们发起了两个并发请求并订阅了结果。同时,将订阅的 Subscription 对象存储在 subscriptions 数组中。在 cancelRequests 方法中,通过遍历 subscriptions 数组并调用 unsubscribe 方法来取消所有正在执行的请求。

五、并发请求中的错误处理

  1. 全局错误处理
    • 原理:可以在应用级别设置一个全局的 HTTP 错误处理机制,这样所有的 HTTP 请求错误都可以在这里统一处理。Angular 提供了 HttpInterceptor 来实现这一点。
    • 示例:首先创建一个 HTTP 拦截器。
import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable, catchError } from 'rxjs';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
      catchError(error => {
        console.error('Global Error:', error);
        // 可以在这里进行统一的错误提示,如弹出一个错误框
        throw error;
      })
    );
  }
}
  • 然后在 app.module.ts 中注册这个拦截器。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AppComponent } from './app.component';
import { ErrorInterceptor } from './error - interceptor';

@NgModule({
  imports: [BrowserModule, HttpClientModule],
  declarations: [AppComponent],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: ErrorInterceptor,
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
  • 解释ErrorInterceptor 实现了 HttpInterceptor 接口。在 intercept 方法中,通过 catchError 操作符捕获所有 HTTP 请求的错误,并进行统一的错误处理,如记录错误日志或显示错误提示。
  1. 并发请求中特定错误处理
    • 原理:对于并发请求,有时候需要对不同请求的错误进行特定处理。比如,在使用 forkJoin 时,可以在订阅中处理错误。
    • 示例
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { forkJoin } from 'rxjs';

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

  makeConcurrentRequestsWithSpecificErrorHandling() {
    const userInfo$ = this.http.get('https://example.com/api/user');
    const orderList$ = this.http.get('https://example.com/api/orders');

    forkJoin([userInfo$, orderList$]).subscribe({
      next: ([userInfo, orderList]) => {
        console.log('User Info:', userInfo);
        console.log('Order List:', orderList);
      },
      error: (error) => {
        if (error.status === 404) {
          console.log('One of the requests returned 404');
        } else {
          console.log('Other error:', error);
        }
      }
    });
  }
}
  • 解释:在这个示例中,我们通过 subscribeerror 回调函数来处理并发请求中的错误。根据错误的 status 码进行不同的处理,如果是 404 错误,进行特定提示,否则进行一般的错误提示。

六、性能优化与并发请求

  1. 缓存策略
    • 原理:对于一些不经常变化的数据,可以采用缓存策略,避免重复发起请求。在 Angular 中,可以通过自定义服务来实现简单的缓存功能。
    • 示例:创建一个缓存服务。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, shareReplay } from 'rxjs';

@Injectable()
export class CacheService {
  private cache: { [url: string]: Observable<any> } = {};

  constructor(private http: HttpClient) {}

  get<T>(url: string): Observable<T> {
    if (!this.cache[url]) {
      this.cache[url] = this.http.get<T>(url).pipe(
        shareReplay(1)
      );
    }
    return this.cache[url];
  }
}
  • 在组件中使用缓存服务。
import { Component } from '@angular/core';
import { CacheService } from './cache - service';

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

  makeCachedRequest() {
    const data$ = this.cacheService.get('https://example.com/api/static - data');
    data$.subscribe(data => {
      console.log('Cached Data:', data);
    });
  }
}
  • 解释CacheService 维护一个缓存对象 cache,其中键为请求的 URL,值为对应的 Observable。当请求一个 URL 时,先检查缓存中是否存在,如果不存在则发起请求,并使用 shareReplay(1) 操作符来缓存请求的结果,这样后续相同 URL 的请求就可以直接使用缓存中的数据。
  1. 请求合并
    • 原理:如果在短时间内有多个相同的请求,可以将这些请求合并为一个,避免重复请求服务器。同样可以通过自定义服务来实现。
    • 示例:创建一个请求合并服务。
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, Subject } from 'rxjs';

@Injectable()
export class RequestMergeService {
  private requestMap: { [url: string]: Subject<any> } = {};

  constructor(private http: HttpClient) {}

  get<T>(url: string): Observable<T> {
    if (!this.requestMap[url]) {
      this.requestMap[url] = new Subject<any>();
      this.http.get<T>(url).subscribe(data => {
        this.requestMap[url].next(data);
        this.requestMap[url].complete();
      });
    }
    return this.requestMap[url].asObservable();
  }
}
  • 在组件中使用请求合并服务。
import { Component } from '@angular/core';
import { RequestMergeService } from './request - merge - service';

@Component({
  selector: 'app - merge - component',
  templateUrl: './merge - component.html'
})
export class MergeComponent {
  constructor(private requestMergeService: RequestMergeService) {}

  makeMergedRequest() {
    const data1$ = this.requestMergeService.get('https://example.com/api/data');
    const data2$ = this.requestMergeService.get('https://example.com/api/data');

    data1$.subscribe(data => {
      console.log('Data from first subscription:', data);
    });
    data2$.subscribe(data => {
      console.log('Data from second subscription:', data);
    });
  }
}
  • 解释RequestMergeService 使用一个 requestMap 对象来存储相同 URL 的请求。当请求一个 URL 时,如果该 URL 不在 requestMap 中,则创建一个 Subject 并发起请求,请求成功后将数据通过 Subject 发送出去。后续相同 URL 的请求直接订阅这个 Subject,从而实现请求合并。

通过以上对 Angular HTTP 请求并发处理与控制的详细介绍,开发者可以更好地应对复杂的前端数据获取场景,优化应用性能,提供更好的用户体验。在实际项目中,应根据具体需求选择合适的方法和策略来处理并发请求。