原文在此,也可以看原文哦

有许多种方式使用拦截器,我确定我们大多数人使用的很浅显。在这篇文章中,我将介绍在 Angular 中我最喜欢的 10 种使用拦截器的方式。

我使例子尽可能的简洁。我希望他们能够启发你们去思考使用拦截器的新方式。这篇文章不是关于拦截器教学的,因为已经有很多好的文章了。但是,在开始倒数之前,让我们以一些基础的知识点开始。

HttpInterceptor 101

HttpInterceptor 是在 Angular 4.3 引入。它提供一种方式拦截 HTTP 请求和响应,在传递他们之前转换或者处理他们。

尽管拦截器能够改变请求和响应,但是 HttpRequestHttpResponse 实例属性是 只读 的,从而使它们在很大程度上不可变。 — Angular Docs

这是因为我们可能想要在某个请求第一次没成功后重试。不变性确保了拦截器链能够多次重新处理相同的请求。

你可以使用多个拦截器,但是这个记心中:

Angular 通过你定义他们的顺序应用拦截器。如果你定义的拦截器的顺序是 A->B->C,请求将按 A->B->C 的顺序流入,响应将按 C->B->A 的顺序流入。

之后,不能改变顺序或者移除拦截器。如果你需要动态启用、禁用拦截器,你不得不在拦截器本身增加这个功能。— Angular Docs

在示例 APP 中,我们提供了全部拦截器,但是一次仅使用一个。这通过检查路径实现(代码在这里)。如果不是我们找的请求,我们通过 next.handle(req) 传递给下一个拦截器。

在 Angular 中使用拦截器的方式 Top 10 - 图1

拦截器的另一个好处是他们能够一起处理请求和响应。我们将看到,这给我们很好的可能性。

更多深度知识,可以看 Max Koretskyi aka Wizard 这篇很棒的文章:

在示例的 HTTP 请求中,我使用了 JSONPlaceholder 这个网站。如果你想看代码,你可以从这里找到:

现在,让我们开始倒数吧!

在 Angular 中使用拦截器的方式 Top 10 - 图2

10.URL

操纵 URL。当我大声说出来时,听起来有些风险,但是让我们看下在拦截器下做这个事情是多么简单。

例如,我们想从 HTTP 变为 HTTPS。

就像克隆请求同时使用 https:// 替换 http:// 一样简单。然后我们将克隆的 HTTPS 请求发送到下一个 handler。

  1. // 克隆请求,同时使用 https:// 替换 http://
  2. const httpsReq = req.clone({
  3. url: req.url.replace('http://', 'https://')
  4. });
  5. return next.handler(httpsReq)

这个例子中,我们设置 URL 为 HTTP,但是当我们检查请求时,我们能看见它已经变成了 HTTPS。

  1. const url = `http://jsonplaceholder.typicode.com/todos/1`;
  2. this.response = this.http.get(url);

在 Angular 中使用拦截器的方式 Top 10 - 图3

自动化魔术 https,为什么这个不高明呢。通常你可以通过 web 服务器设置这些事情。或者你想在开发环境从 HTTP 切换到 HTTPS,你可以使用这个 CLI:

  1. ng serve -ssl

类似,你可以修改一点 URL,并且成它为 API 前缀拦截器:

  1. req.clone({
  2. url: environment.serverUrl + request.url
  3. });

或者你可以再次通过 CLI 来实现:

  1. ng serve - serve-path=<path> - base-href <path>/

感谢 David Herges 的 CLI 提示。

9.Loader

当我们等待响应时,每个人都希望看见命运的纺纱轮(表示旋转的loading)。只要在请求活跃的时候,我们在拦截器中统一设置,这样我们就能够看见 loader 了。

首先,我们能够使用 loader 服务,这样就有了展示和隐藏 loader 的功能。在处理请求前,我们调用展示方法并通过 finalize 完成后隐藏 loader。

  1. const loaderService = this.injector.get(LoaderService);
  2. loaderService.show();
  3. return next.handle(req).pipe(
  4. delay(5000),
  5. finalize(() => loaderService.hide())
  6. )

这个例子很简单,在真实的解决方案中,我们应该考虑到会有多个 HTTP 请求被拦截。这可以通过一个请求(+1)响应(-1)的计数器来解决这个问题。

当然,我添加了一个延迟来让我们有时间能够看到 loader。

在 Angular 中使用拦截器的方式 Top 10 - 图4

全局的 loader 听起来是个不错的主意,但是这个为什么不在列表中呢?可能它适合特定的应用,但是如果你同时加载多个,你可能想要对 loader 定制化。

我将给你留下一些思考的空间。如果你用 switchMap 去取消请求将会发生什么?

8.转换

当 API 返回一个我们不赞同的格式,我们能够使用拦截器去格式化成我们想要的样子。

这能够从 XML 转换到 JSON,或者像例子中的属性名字从大驼峰拼写到小驼峰拼写。如果后端不关心 JSON/JS 转换,我们能够使用拦截器将全部属性名重命名为小驼峰。

检查是否有 npm 包能够为你完成繁重的工作。在这个例子中我使用 loadshmapKeyscamelCase

  1. return next.handle(req).pipe(
  2. map((event: HttpEvent<any>) => {
  3. if (event instanceof HttpResponse) {
  4. let camelCaseObject = mapKeys(event.body, (v,k) => camelCase(k));
  5. const modEvent = event.clone({ body: camelCaseObjec });
  6. return modEvent;
  7. }
  8. })
  9. )

这个事情通常是后端来做,所以我通常不这么做。但是将这个加入到的兵器库,这样你需要的时候就能够使用了。

在 Angular 中使用拦截器的方式 Top 10 - 图5

7.Headers

通过操纵 headers 我们能够做许多事,例如:

  • 认证(authentication)/ 授权(authorization)
  • 缓存行为;例如,If-Modified-Since
  • XSRF 保护

我们能够通过拦截器轻而易举的添加 headers。

  1. cosnt modified = req.clone({
  2. setHeaders: { "X-Man": "wolverine" }
  3. });
  4. return next.handle(modified);

然后我们在开发者工具中就能够看到它被添加到了请求头中。

在 Angular 中使用拦截器的方式 Top 10 - 图6

Angular 使用拦截器来防范 跨站请求伪造(XSRF)。通过读取 cookie 中的 XSRF-TOKEN 并设置一个 X-XSRF-TOKEN 请求头来实现。仅仅运行在你的域名中的代码才能够读取 cookie,这样后端能够确定 HTTP 请求来自己客户端程序而不是攻击者。

如你所见,在拦截器中能够直接操纵 headers。接下来我们将看到更多操纵 headers 的例子。

6.通知

这里有很多不同的例子用于展示消息。在我的例子中,每次从服务器获得 201 创建状态时,我会展示“Object created.”。

  1. return next.hadle(req).pipe(
  2. tap((event: HttpEvent<any>) => {
  3. if (event instanceof HttpResponse && event.status === 201) {
  4. this.toastr.success('Object created.')
  5. }
  6. })
  7. );

或者我们检查对象的类型来展示“Type created”。或者通过将数据和消息包裹到对象中创建一个定制的消息。

  1. {
  2. data: T,
  3. message: string;
  4. }

在 Angular 中使用拦截器的方式 Top 10 - 图7

当发生错误的时候,我们也可以通过拦截器展示通知。

5.Errors

在这个拦截器中我们实现了两个关于错误的用例。

首先,我们能够重试 HTTP 请求。例如,网络中断在移动端场景很常见,再试一次可能会成功。值得考虑的事情是在放弃之前的重试次数。我们应该在重试前等待吗,或者立即重试?

对于这点,我们使用 RxJS 中的 retry 操作符重新订阅 observable。HttpClient 方法调用的重新订阅具有再次发出 HTTP 请求的效果。

这种行为的更高级的示例:

第二点,检查异常的状态。基于状态,决定应该做什么。

  1. return next.handle(req).pipe(
  2. retry(2),
  3. catchError((error: HttpErrorResponse) => {
  4. if (error.status !== 401) {
  5. // 401 在 auth.interceptor 中处理了
  6. this.toastr.error(error.message);
  7. }
  8. return throwError(error)
  9. })
  10. )

这个例子中,在检查错误状态前,我们重试了两次。如果状态不是 401,我们已弹出(toastr)的形式展示错误。所有的错误将重新抛出来进一步处理。

在 Angular 中使用拦截器的方式 Top 10 - 图8

在 Angular 中使用拦截器的方式 Top 10 - 图9

更多的错误处理的知识,你可以在这里阅读我早期的文章:

4.分析

因为拦截器能够同时处理请求和响应,能够在一次完整的 HTTP 操作中计时和记日志。所以我们能够捕获请求和响应时间,记录经过的时间结果。

  1. const started = Date.now()
  2. let ok: string;
  3. return next.handle(req).pipe(
  4. tap(
  5. (event: HttpEvent<any>) => ok = event instanceof HttpResponse ? 'successed' : '',
  6. (error: HttpEventResponse) => ok = 'failed'
  7. ),
  8. // 响应 observable 结束或者完成的时候记日志
  9. finalize(() => {
  10. const elapsed = Date.now() - started;
  11. const msg = `${req.method} "${req.urlWithParams}" ${ok} in ${elapsed} ms.`
  12. console.log(msg);
  13. })
  14. )

这有许多中可能,例如,我们能记录分析日志到数据库中做统计。这个例子中,我们输出到 console。

在 Angular 中使用拦截器的方式 Top 10 - 图10

3. 伪造后端

当没有后端服务时,可以在开发中模拟或者伪装后端。你也可以将其用于 StackBlitz 中托管的代码。

我们基于请求模拟返回,然后返回一个 HttpResponse observable。

  1. const body = {
  2. firstName: 'mock',
  3. lastName: 'Faker',
  4. };
  5. return of(new HttpResponse(
  6. { status: 200, body: body }
  7. ));

在 Angular 中使用拦截器的方式 Top 10 - 图11

2. 缓存

因为拦截器能够自己处理请求,没有转发到 next.handle(),所以我们利用这一点来缓存请求。

在 key-value map 构成的缓存中,我们使用 URL 作为 key。如果我们响应在 map 中,我们能够通过 next handler 返回这个 observable 响应。

当你已经有响应缓存时,你不需要一路走到后端,这个提升了性能。

  1. import { Injectable } from '@angular/core';
  2. import { HttpEvent, HttpRequest, HttpHandler, HttpInterceptor, HttpResponse } from '@angular/common/http';
  3. import { Observable, of } from 'rxjs';
  4. import { tap, shareReplay } from 'rxjs/operators';
  5. @Injectable()
  6. export class CacheInterceptor implements HttpInterceptor {
  7. private cache = new Map<string, any>();
  8. intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  9. if (request.method !== 'Get') {
  10. return next.handle(request);
  11. }
  12. const cacheResponse = this.cache.get(request.url);
  13. if (cacheResponse) {
  14. return of(cacheResponse);
  15. }
  16. return next.handle(request).pipe(
  17. tap(event => {
  18. if (event instanceof HttpResponse) {
  19. this.cache.set(request.url, event);
  20. }
  21. })
  22. )
  23. }
  24. }

如果我们运行这个请求,清空响应然后再次运行将使用缓存。

在 Angular 中使用拦截器的方式 Top 10 - 图12

在 Angular 中使用拦截器的方式 Top 10 - 图13

如果数据更新了,你需要使缓存失效,这会引入一些复杂性。但是现在不用担心!缓存生效的时候是真的爽!

关于缓存的更多知识可以读 Dominic E. 的这篇很帮的文章:

1.认证

清单中的第一个是认证!对于很多应用来说他是基本的,我们已经有了适当的认证系统。这是拦截器最常见的用例之一,有充分的理由。恰到好处!

有几个和权限相关的事情我们能做:

  1. 添加 bearer token
  2. 重新刷新 token
  3. 重定向到登录页

当我们发送 bearer token 时,我们也应该有些过滤。如果我们还没 token,我们可能在登录,并不需要添加 token。或者如果我们调用其他域名,我们也不希望添加 token。例如,如果向 Slack 发送错误信息

相比于其他拦截器这个会有点复杂。这是一个带有一些解释性注释的例子:

  1. import { Injectable } from '@angular/core';
  2. import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
  3. import { throwError, Observable, BehaviorSubject, of } from 'rxjs';
  4. import { catchError, filter, take, switchMap } from 'rxjs/operators';
  5. @Injectable()
  6. export class AuthInterceptor implements HttpInterceptor {
  7. private AUTH_HEADER = 'Authorization';
  8. private token = 'secrettoken';
  9. private refreshTokenInProgress = false;
  10. private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
  11. intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  12. if (!req.headers.has('Content-type')) {
  13. req = req.clone({
  14. headers: req.headers.set('Content-Type', 'application/json')
  15. });
  16. }
  17. req = this.addAuthenticationToken(req);
  18. return next.handle(req).pipe(
  19. cacheError((error: HttpErrorResponse) => {
  20. if (error && error.status === 401) {
  21. if (this.refreshTokenInProgress) {
  22. return this.refreshTokenSubject.pipe(
  23. filter(result => result !== null),
  24. take(1),
  25. switchMap(() => next.handle(this.addAuthenticationToken(req)))
  26. );
  27. } else {
  28. this.refreshTokenInProgress = true;
  29. // 设置 refreshTokenSubject 为 null,这样随后的 API 将等到新的 token 被取回时才调用。
  30. this.refreshTokenSubject.next(null);
  31. return this.refreshAccessToken().pipe(
  32. switchMap((success: boolean) => {
  33. this.refreshTokenSubject.next(success);
  34. return next.handle(this.addAuthenticationToken(req));
  35. }),
  36. // 当我们调用刷新 token 方法完成时,重置 refreshTokenInProgress 为 false,
  37. // 这是为了下次 token 需要再次被刷新
  38. finalize(() => this.refreshTokenInProgress = false)
  39. );
  40. }
  41. } else {
  42. return throwError(error)
  43. }
  44. })
  45. )
  46. }
  47. private refreshAccessToken(): Observable<any> {
  48. return of('secret token');
  49. }
  50. private addAuthenticationToken(request: HttpRequest<any>): HttpRequest<any> {
  51. // 如果还没有 token,不应该在 header 中设置 token。
  52. // 首先我们应该从存储 token 的地方取回
  53. if (!this.token) {
  54. return request;
  55. }
  56. // 如果访问外部域名不应该添加 token
  57. if (!request.url.match(/www.mydomain.com\//)) {
  58. return request;
  59. }
  60. return request.clone({
  61. headers: request.headers.set(this.AUTH_HEADER, 'Bearer ' + this.token)
  62. })
  63. }
  64. }

大吉大利,今晚吃鸡!🚀

总结

拦截器是 Angular 4.3 中一个重要的功能,这里我们看到了很多很棒的功能。现在发挥你的创造力,我相信你可以想出一些有趣的东西!

记住,通过使用拦截器,你可以像蝙蝠侠一样棒!

在 Angular 中使用拦截器的方式 Top 10 - 图14

感谢 Angular In Depth 提供想法和帮助编辑文档。希望我没有忘记任何一个人,谢谢 Max Koretskyi aka WizardTim DeschryverAlex OkrushkoAlexander PoshtarukLars Gyrup Brink NielsenNacho Vazquez Callejathekiba & Alexey Zuev!

资源

译者参考

感谢阅读

感谢你阅读到这里,翻译的不好的地方,还请指点。希望我的内容能让你受用,再次感谢。by llccing 千里