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

Angular HTTP请求的拦截器实现与应用

2021-03-025.1k 阅读

Angular HTTP 请求拦截器基础概念

在 Angular 应用开发中,HTTP 请求是与后端服务器进行数据交互的重要方式。然而,在实际项目里,常常需要对所有的 HTTP 请求或响应执行一些通用操作,例如添加身份验证头、记录日志、错误处理等。这时候,Angular 的 HTTP 请求拦截器就派上用场了。

HTTP 拦截器是 Angular 提供的一种强大机制,它允许开发者在 HTTP 请求发送之前以及响应返回之后,对请求或响应进行拦截并执行自定义操作。拦截器本质上是一个类,实现了 HttpInterceptor 接口。这个接口定义了一个 intercept 方法,该方法接收 HttpRequestHttpHandler 作为参数,并返回一个 Observable<HttpEvent<any>>

创建基本的 HTTP 拦截器

  1. 生成拦截器类 首先,使用 Angular CLI 来生成一个拦截器类。在项目根目录下执行以下命令:
ng generate interceptor my - intercept

这将在 src/app 目录下生成一个名为 my - intercept.ts 的文件,内容如下:

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

@Injectable()
export class MyInterceptInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(request);
  }
}
  1. 理解 intercept 方法参数

    • request:类型为 HttpRequest<any>,它代表即将发送的 HTTP 请求。这个对象是不可变的,意味着如果需要对请求进行修改,需要通过 clone 方法创建一个新的请求实例。
    • next:类型为 HttpHandler,它是一个处理请求并返回响应的函数。调用 next.handle(request) 会将请求传递给下一个拦截器(如果存在),或者最终传递给 HTTP 后端处理。
  2. 简单示例:记录请求日志 我们可以在拦截器中记录每个请求的信息,例如请求 URL 和请求方法。修改 intercept 方法如下:

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

@Injectable()
export class MyInterceptInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    console.log('Request URL:', request.url);
    console.log('Request Method:', request.method);
    return next.handle(request).pipe(
      tap(() => { },
        (error) => {
          console.error('Request Error:', error);
        })
    );
  }
}

在这个例子中,我们在请求发送前打印了请求的 URL 和方法。同时,通过 rxjstap 操作符,在响应返回后(无论是成功还是失败),如果出现错误,打印错误信息。

注册拦截器

要使拦截器生效,需要将其注册到 Angular 的依赖注入系统中。有两种常见的注册方式:

  1. app.module.ts 中注册app.module.tsproviders 数组中添加拦截器:
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 { MyInterceptInterceptor } from './my - intercept.interceptor';

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

这里通过 provide: HTTP_INTERCEPTORS 来指定我们要注册的是 HTTP 拦截器。useClass 指向我们创建的拦截器类 MyInterceptInterceptormulti: true 表示可以有多个拦截器,它们会按照注册的顺序依次执行。

  1. HttpClientModule 中注册 也可以在导入 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';
import { MyInterceptInterceptor } from './my - intercept.interceptor';

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

这种方式更为简洁,直接在 HttpClientModulewithInterceptors 方法中传入拦截器数组。

拦截器应用场景 - 添加身份验证头

在许多实际应用中,需要对每个请求添加身份验证头,通常是 JWT(JSON Web Token)。假设我们从本地存储中获取 JWT 并添加到请求头中。

  1. 修改拦截器类
import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const token = localStorage.getItem('token');
    if (token) {
      request = request.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
    }
    return next.handle(request);
  }
}

在这个拦截器中,首先从本地存储获取 token。如果 token 存在,通过 request.clone 方法创建一个新的请求,并在新请求的头中添加 Authorization 字段,其值为 Bearer 加上 token

  1. 注册新拦截器app.module.tsproviders 中注册这个新的拦截器:
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 { AuthInterceptor } from './auth.interceptor';

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

这样,所有的 HTTP 请求都会带上 Authorization 头,前提是本地存储中有 token

拦截器应用场景 - 错误处理

拦截器也非常适合统一处理 HTTP 错误。例如,当后端返回 401(未授权)错误时,我们可以自动将用户重定向到登录页面。

  1. 创建错误处理拦截器
import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { Router } from '@angular/router';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor(private router: Router) { }

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
      tap(() => { },
        (error) => {
          if (error.status === 401) {
            this.router.navigate(['/login']);
          }
          throw error;
        })
    );
  }
}

在这个拦截器中,通过 tap 操作符监听响应的错误。如果错误状态码是 401,使用 Router 导航到登录页面,然后重新抛出错误,以便其他部分的应用可以继续处理该错误。

  1. 注册错误处理拦截器 同样在 app.module.tsproviders 中注册:
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({
  declarations: [AppComponent],
  imports: [BrowserModule, HttpClientModule],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: ErrorInterceptor,
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

现在,当应用中任何 HTTP 请求返回 401 错误时,用户会被自动重定向到登录页面。

多个拦截器的执行顺序

当应用中有多个拦截器时,它们的执行顺序是按照在 providers 数组中注册的顺序来的。

  1. 请求阶段 假设我们有三个拦截器 InterceptorAInterceptorBInterceptorC 按照顺序注册。当一个 HTTP 请求发起时,InterceptorAintercept 方法首先被调用,它可以对请求进行修改,然后调用 next.handle(request) 将请求传递给 InterceptorBInterceptorB 同样可以处理请求并传递给 InterceptorC,最后 InterceptorC 将请求传递给 HTTP 后端。

  2. 响应阶段 当后端返回响应时,响应会按照相反的顺序经过拦截器。即 InterceptorC 首先处理响应,然后是 InterceptorB,最后是 InterceptorA。每个拦截器都可以对响应进行处理,例如修改响应数据、处理错误等。

条件性拦截器

有时候,我们可能只希望在特定条件下应用拦截器。例如,只对特定 URL 前缀的请求添加身份验证头。

  1. 修改身份验证拦截器
import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class ConditionalAuthInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const restrictedUrls = ['/api', '/admin'];
    const shouldAddAuthHeader = restrictedUrls.some(url => request.url.startsWith(url));
    if (shouldAddAuthHeader) {
      const token = localStorage.getItem('token');
      if (token) {
        request = request.clone({
          setHeaders: {
            Authorization: `Bearer ${token}`
          }
        });
      }
    }
    return next.handle(request);
  }
}

在这个拦截器中,定义了一个 restrictedUrls 数组,包含需要添加身份验证头的 URL 前缀。通过 some 方法检查请求的 URL 是否以这些前缀开头,如果是,则添加 Authorization 头。

  1. 注册条件性拦截器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 { ConditionalAuthInterceptor } from './conditional - auth.interceptor';

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

这样,只有对以 /api/admin 开头的 URL 的请求,才会添加身份验证头。

拦截器与响应式编程

由于拦截器的 intercept 方法返回一个 Observable<HttpEvent<any>>,这使得我们可以充分利用 RxJS 的操作符来处理请求和响应。

  1. 合并多个请求 假设我们有一个场景,需要在发送某个请求前,先获取另一个数据。例如,在获取用户详细信息前,先获取用户的基本信息来确定是否有权限。
import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import { Observable, forkJoin } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class CompositeRequestInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    if (request.url === '/user/details') {
      const baseInfoRequest = new HttpRequest('GET', '/user/base - info');
      return forkJoin([
        next.handle(baseInfoRequest),
        next.handle(request)
      ]).pipe(
        map(([baseInfoResponse, detailsResponse]) => {
          // 这里可以根据 baseInfoResponse 处理 detailsResponse
          return detailsResponse;
        })
      );
    }
    return next.handle(request);
  }
}

在这个拦截器中,当请求的 URL 是 /user/details 时,创建一个获取用户基本信息的请求 baseInfoRequest。通过 forkJoin 将两个请求合并,在 map 操作符中可以根据基本信息的响应来处理详细信息的响应。

  1. 重试失败的请求 有时候,请求可能因为网络波动等原因失败,我们可以通过拦截器实现自动重试机制。
import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import { Observable, retry } from 'rxjs';

@Injectable()
export class RetryInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
      retry(3)
    );
  }
}

在这个拦截器中,使用 retry 操作符,当请求失败时,最多重试 3 次。

拦截器在跨域请求中的应用

在实际开发中,跨域请求是常见的问题。虽然通常可以在后端配置 CORS(跨域资源共享),但在前端也可以通过拦截器做一些辅助处理。

  1. 添加跨域请求头 某些情况下,后端可能需要特定的跨域请求头。我们可以通过拦截器添加这些头。
import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class CorsInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    request = request.clone({
      setHeaders: {
        'X - Requested - With': 'XMLHttpRequest'
      }
    });
    return next.handle(request);
  }
}

在这个拦截器中,为请求添加了 X - Requested - With 头,这在一些跨域场景中可能是后端所需要的。

  1. 处理跨域预检请求 对于复杂的跨域请求(例如 PUT、DELETE 方法,或者包含自定义头的请求),浏览器会先发送一个预检请求(OPTIONS 方法)。我们可以通过拦截器来处理这个预检请求。
import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class PreflightInterceptor implements HttpInterceptor {
  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    if (request.method === 'OPTIONS') {
      request = request.clone({
        headers: request.headers.set('Access - Control - Allow - Origin', '*')
      });
    }
    return next.handle(request);
  }
}

在这个拦截器中,当请求方法是 OPTIONS 时,设置 Access - Control - Allow - Origin 头为 *,以模拟后端的 CORS 响应(实际生产中应根据后端配置设置正确的源)。

拦截器的性能考虑

虽然拦截器非常强大,但在使用时也需要考虑性能问题。

  1. 避免不必要的操作 在拦截器的 intercept 方法中,应尽量避免执行复杂、耗时的操作。例如,不要在拦截器中进行大量的计算或者数据库查询(如果可能的话)。因为每个 HTTP 请求都会经过拦截器,如果拦截器执行缓慢,会影响整个应用的响应速度。

  2. 合理使用 RxJS 操作符 在使用 RxJS 操作符处理请求和响应时,要注意操作符的性能影响。例如,一些操作符可能会创建大量的中间 Observable 对象,增加内存开销。尽量使用简单、高效的操作符来完成所需的功能。

  3. 缓存数据 如果拦截器需要获取一些数据(例如身份验证的 token),可以考虑缓存这些数据,而不是每次请求都重新获取。例如,将 token 存储在本地存储中,并在拦截器中直接从本地存储读取,而不是每次都从服务器获取。

拦截器与 Angular 服务的结合使用

拦截器可以与 Angular 服务紧密结合,以实现更复杂的功能。

  1. 使用服务提供配置信息 假设我们有一个配置服务 ConfigService,它提供一些与 HTTP 请求相关的配置,例如 API 前缀。拦截器可以使用这个服务来动态修改请求 URL。
import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { ConfigService } from './config.service';

@Injectable()
export class ApiPrefixInterceptor implements HttpInterceptor {
  constructor(private configService: ConfigService) { }

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const apiPrefix = this.configService.getApiPrefix();
    if (!request.url.startsWith('http')) {
      request = request.clone({
        url: apiPrefix + request.url
      });
    }
    return next.handle(request);
  }
}

在这个拦截器中,通过构造函数注入 ConfigService,并从该服务获取 API 前缀,然后将其添加到请求 URL 中。

  1. 通过服务处理业务逻辑 我们可以创建一个专门处理错误业务逻辑的服务,在错误拦截器中调用这个服务来处理不同类型的错误。
import { Injectable } from '@angular/core';
import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { ErrorService } from './error.service';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor(private errorService: ErrorService) { }

  intercept(
    request: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
      tap(() => { },
        (error) => {
          this.errorService.handleError(error);
          throw error;
        })
    );
  }
}

在这个拦截器中,当出现错误时,调用 ErrorServicehandleError 方法来处理错误,这样可以将错误处理逻辑集中在一个服务中,便于维护和扩展。

拦截器在微前端架构中的应用

在微前端架构中,多个前端应用可能共享一些 HTTP 拦截器的逻辑。

  1. 共享拦截器模块 可以将通用的拦截器封装成一个独立的模块,供各个微前端应用使用。例如,创建一个 shared - interceptors 模块,其中包含身份验证拦截器、错误处理拦截器等。
import { NgModule } from '@angular/core';
import { AuthInterceptor } from './auth.interceptor';
import { ErrorInterceptor } from './error.interceptor';
import { HTTP_INTERCEPTORS } from '@angular/common/http';

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: ErrorInterceptor,
      multi: true
    }
  ]
})
export class SharedInterceptorsModule { }

然后,各个微前端应用可以导入这个模块来使用其中的拦截器。

  1. 微前端特定拦截器 每个微前端应用也可以有自己特定的拦截器。例如,某个微前端应用需要对特定的 API 进行额外的处理,可以创建一个只在该微前端应用中使用的拦截器,并在该应用的模块中注册。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform - browser';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { MicroFrontendAppComponent } from './micro - frontend - app.component';
import { MicroFrontendSpecificInterceptor } from './micro - frontend - specific.interceptor';

@NgModule({
  declarations: [MicroFrontendAppComponent],
  imports: [BrowserModule, HttpClientModule],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: MicroFrontendSpecificInterceptor,
      multi: true
    }
  ],
  bootstrap: [MicroFrontendAppComponent]
})
export class MicroFrontendAppModule { }

这样,在微前端架构中,可以灵活地管理和复用 HTTP 拦截器逻辑。

通过以上对 Angular HTTP 请求拦截器的详细介绍,包括其基础概念、创建、注册、各种应用场景、与其他 Angular 特性的结合以及在不同架构中的应用等方面,相信开发者能够更好地利用拦截器来优化和扩展 Angular 应用的 HTTP 通信功能。在实际项目中,根据具体需求合理设计和使用拦截器,可以提高代码的可维护性、复用性以及应用的整体性能。