有许多种方式使用拦截器,我确定我们大多数人使用的很浅显。在这篇文章中,我将介绍在 Angular 中我最喜欢的 10 种使用拦截器的方式。
我使例子尽可能的简洁。我希望他们能够启发你们去思考使用拦截器的新方式。这篇文章不是关于拦截器教学的,因为已经有很多好的文章了。但是,在开始倒数之前,让我们以一些基础的知识点开始。
HttpInterceptor 101
HttpInterceptor 是在 Angular 4.3 引入。它提供一种方式拦截 HTTP 请求和响应,在传递他们之前转换或者处理他们。
尽管拦截器能够改变请求和响应,但是 HttpRequest 和 HttpResponse 实例属性是
只读
的,从而使它们在很大程度上不可变。 — Angular Docs
这是因为我们可能想要在某个请求第一次没成功后重试。不变性确保了拦截器链能够多次重新处理相同的请求。
你可以使用多个拦截器,但是这个记心中:
Angular 通过你定义他们的顺序应用拦截器。如果你定义的拦截器的顺序是 A->B->C,请求将按 A->B->C 的顺序流入,响应将按 C->B->A 的顺序流入。
之后,不能改变顺序或者移除拦截器。如果你需要动态启用、禁用拦截器,你不得不在拦截器本身增加这个功能。— Angular Docs
在示例 APP 中,我们提供了全部拦截器,但是一次仅使用一个。这通过检查路径实现(代码在这里)。如果不是我们找的请求,我们通过 next.handle(req)
传递给下一个拦截器。
拦截器的另一个好处是他们能够一起处理请求和响应
。我们将看到,这给我们很好的可能性。
更多深度知识,可以看 Max Koretskyi aka Wizard 这篇很棒的文章:
在示例的 HTTP 请求中,我使用了 JSONPlaceholder 这个网站。如果你想看代码,你可以从这里找到:
- 10.x 版本,译者版本
GitHub 示例代码 10.x版本 📜
StackBlitz 线上运行 🏃 - 8.x 版本,作者版本
GitHub 示例代码 8.x版本 📜
StackBlitz 线上运行 🏃
现在,让我们开始倒数吧!
10.URL
操纵 URL。当我大声说出来时,听起来有些风险,但是让我们看下在拦截器下做这个事情是多么简单。
例如,我们想从 HTTP 变为 HTTPS。
就像克隆请求同时使用 https:// 替换 http:// 一样简单。然后我们将克隆的 HTTPS 请求发送到下一个 handler。
// 克隆请求,同时使用 https:// 替换 http://
const httpsReq = req.clone({
url: req.url.replace('http://', 'https://')
});
return next.handler(httpsReq)
这个例子中,我们设置 URL 为 HTTP,但是当我们检查请求时,我们能看见它已经变成了 HTTPS。
const url = `http://jsonplaceholder.typicode.com/todos/1`;
this.response = this.http.get(url);
自动化魔术 https,为什么这个不高明呢。通常你可以通过 web 服务器设置这些事情。或者你想在开发环境从 HTTP 切换到 HTTPS,你可以使用这个 CLI:
ng serve -ssl
类似,你可以修改一点 URL,并且成它为 API 前缀拦截器:
req.clone({
url: environment.serverUrl + request.url
});
或者你可以再次通过 CLI 来实现:
ng serve - serve-path=<path> - base-href <path>/
感谢 David Herges 的 CLI 提示。
9.Loader
当我们等待响应时,每个人都希望看见命运的纺纱轮(表示旋转的loading)。只要在请求活跃的时候,我们在拦截器中统一设置,这样我们就能够看见 loader 了。
首先,我们能够使用 loader 服务,这样就有了展示和隐藏 loader 的功能。在处理请求前,我们调用展示
方法并通过 finalize 完成后隐藏
loader。
const loaderService = this.injector.get(LoaderService);
loaderService.show();
return next.handle(req).pipe(
delay(5000),
finalize(() => loaderService.hide())
)
这个例子很简单,在真实的解决方案中,我们应该考虑到会有多个 HTTP 请求被拦截。这可以通过一个请求(+1)响应(-1)的计数器来解决这个问题。
当然,我添加了一个延迟来让我们有时间能够看到 loader。
全局的 loader 听起来是个不错的主意,但是这个为什么不在列表中呢?可能它适合特定的应用,但是如果你同时加载多个,你可能想要对 loader 定制化。
我将给你留下一些思考的空间。如果你用 switchMap 去取消请求将会发生什么?
8.转换
当 API 返回一个我们不赞同的格式,我们能够使用拦截器去格式化成我们想要的样子。
这能够从 XML 转换到 JSON,或者像例子中的属性名字从大驼峰拼写到小驼峰拼写。如果后端不关心 JSON/JS 转换,我们能够使用拦截器将全部属性名重命名为小驼峰。
检查是否有 npm 包能够为你完成繁重的工作。在这个例子中我使用 loadsh 的 mapKeys 和 camelCase。
return next.handle(req).pipe(
map((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
let camelCaseObject = mapKeys(event.body, (v,k) => camelCase(k));
const modEvent = event.clone({ body: camelCaseObjec });
return modEvent;
}
})
)
这个事情通常是后端来做,所以我通常不这么做。但是将这个加入到的兵器库,这样你需要的时候就能够使用了。
7.Headers
通过操纵 headers 我们能够做许多事,例如:
- 认证(authentication)/ 授权(authorization)
- 缓存行为;例如,If-Modified-Since
- XSRF 保护
我们能够通过拦截器轻而易举的添加 headers。
cosnt modified = req.clone({
setHeaders: { "X-Man": "wolverine" }
});
return next.handle(modified);
然后我们在开发者工具中就能够看到它被添加到了请求头中。
Angular 使用拦截器来防范 跨站请求伪造(XSRF)。通过读取 cookie 中的 XSRF-TOKEN
并设置一个 X-XSRF-TOKEN
请求头来实现。仅仅运行在你的域名中的代码才能够读取 cookie,这样后端能够确定 HTTP 请求来自己客户端程序而不是攻击者。
如你所见,在拦截器中能够直接操纵 headers。接下来我们将看到更多操纵 headers 的例子。
6.通知
这里有很多不同的例子用于展示消息。在我的例子中,每次从服务器获得 201 创建状态时,我会展示“Object created.”。
return next.hadle(req).pipe(
tap((event: HttpEvent<any>) => {
if (event instanceof HttpResponse && event.status === 201) {
this.toastr.success('Object created.')
}
})
);
或者我们检查对象的类型来展示“Type created”。或者通过将数据和消息包裹到对象中创建一个定制的消息。
{
data: T,
message: string;
}
当发生错误的时候,我们也可以通过拦截器展示通知。
5.Errors
在这个拦截器中我们实现了两个关于错误的用例。
首先,我们能够重试 HTTP 请求
。例如,网络中断在移动端场景很常见,再试一次可能会成功。值得考虑的事情是在放弃之前的重试次数。我们应该在重试前等待吗,或者立即重试?
对于这点,我们使用 RxJS 中的 retry 操作符重新订阅 observable。HttpClient 方法调用的重新订阅具有再次发出 HTTP 请求的效果。
这种行为的更高级的示例:
第二点,检查异常的状态。基于状态,决定应该做什么。
return next.handle(req).pipe(
retry(2),
catchError((error: HttpErrorResponse) => {
if (error.status !== 401) {
// 401 在 auth.interceptor 中处理了
this.toastr.error(error.message);
}
return throwError(error)
})
)
这个例子中,在检查错误状态前,我们重试了两次。如果状态不是 401,我们已弹出(toastr)的形式展示错误。所有的错误将重新抛出来进一步处理。
更多的错误处理的知识,你可以在这里阅读我早期的文章:
4.分析
因为拦截器能够同时处理请求和响应,能够在一次完整的 HTTP 操作中计时和记日志。所以我们能够捕获请求和响应时间,记录经过的时间结果。
const started = Date.now()
let ok: string;
return next.handle(req).pipe(
tap(
(event: HttpEvent<any>) => ok = event instanceof HttpResponse ? 'successed' : '',
(error: HttpEventResponse) => ok = 'failed'
),
// 响应 observable 结束或者完成的时候记日志
finalize(() => {
const elapsed = Date.now() - started;
const msg = `${req.method} "${req.urlWithParams}" ${ok} in ${elapsed} ms.`
console.log(msg);
})
)
这有许多中可能,例如,我们能记录分析日志到数据库中做统计。这个例子中,我们输出到 console。
3. 伪造后端
当没有后端服务时,可以在开发中模拟或者伪装后端。你也可以将其用于 StackBlitz 中托管的代码。
我们基于请求模拟返回,然后返回一个 HttpResponse observable。
const body = {
firstName: 'mock',
lastName: 'Faker',
};
return of(new HttpResponse(
{ status: 200, body: body }
));
2. 缓存
因为拦截器能够自己处理请求,没有转发到 next.handle()
,所以我们利用这一点来缓存请求。
在 key-value map 构成的缓存中,我们使用 URL 作为 key。如果我们响应在 map 中,我们能够通过 next
handler 返回这个 observable 响应。
当你已经有响应缓存时,你不需要一路走到后端,这个提升了性能。
import { Injectable } from '@angular/core';
import { HttpEvent, HttpRequest, HttpHandler, HttpInterceptor, HttpResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap, shareReplay } from 'rxjs/operators';
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
private cache = new Map<string, any>();
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (request.method !== 'Get') {
return next.handle(request);
}
const cacheResponse = this.cache.get(request.url);
if (cacheResponse) {
return of(cacheResponse);
}
return next.handle(request).pipe(
tap(event => {
if (event instanceof HttpResponse) {
this.cache.set(request.url, event);
}
})
)
}
}
如果我们运行这个请求,清空响应然后再次运行将使用缓存。
如果数据更新了,你需要使缓存失效,这会引入一些复杂性。但是现在不用担心!缓存生效的时候是真的爽!
关于缓存的更多知识可以读 Dominic E. 的这篇很帮的文章:
1.认证
清单中的第一个是认证!对于很多应用来说他是基本的,我们已经有了适当的认证系统。这是拦截器最常见的用例之一,有充分的理由。恰到好处!
有几个和权限相关的事情我们能做:
- 添加 bearer token
- 重新刷新 token
- 重定向到登录页
当我们发送 bearer token 时,我们也应该有些过滤。如果我们还没 token,我们可能在登录,并不需要添加 token。或者如果我们调用其他域名,我们也不希望添加 token。例如,如果向 Slack 发送错误信息。
相比于其他拦截器这个会有点复杂。这是一个带有一些解释性注释的例子:
import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse } from '@angular/common/http';
import { throwError, Observable, BehaviorSubject, of } from 'rxjs';
import { catchError, filter, take, switchMap } from 'rxjs/operators';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private AUTH_HEADER = 'Authorization';
private token = 'secrettoken';
private refreshTokenInProgress = false;
private refreshTokenSubject: BehaviorSubject<any> = new BehaviorSubject<any>(null);
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!req.headers.has('Content-type')) {
req = req.clone({
headers: req.headers.set('Content-Type', 'application/json')
});
}
req = this.addAuthenticationToken(req);
return next.handle(req).pipe(
cacheError((error: HttpErrorResponse) => {
if (error && error.status === 401) {
if (this.refreshTokenInProgress) {
return this.refreshTokenSubject.pipe(
filter(result => result !== null),
take(1),
switchMap(() => next.handle(this.addAuthenticationToken(req)))
);
} else {
this.refreshTokenInProgress = true;
// 设置 refreshTokenSubject 为 null,这样随后的 API 将等到新的 token 被取回时才调用。
this.refreshTokenSubject.next(null);
return this.refreshAccessToken().pipe(
switchMap((success: boolean) => {
this.refreshTokenSubject.next(success);
return next.handle(this.addAuthenticationToken(req));
}),
// 当我们调用刷新 token 方法完成时,重置 refreshTokenInProgress 为 false,
// 这是为了下次 token 需要再次被刷新
finalize(() => this.refreshTokenInProgress = false)
);
}
} else {
return throwError(error)
}
})
)
}
private refreshAccessToken(): Observable<any> {
return of('secret token');
}
private addAuthenticationToken(request: HttpRequest<any>): HttpRequest<any> {
// 如果还没有 token,不应该在 header 中设置 token。
// 首先我们应该从存储 token 的地方取回
if (!this.token) {
return request;
}
// 如果访问外部域名不应该添加 token
if (!request.url.match(/www.mydomain.com\//)) {
return request;
}
return request.clone({
headers: request.headers.set(this.AUTH_HEADER, 'Bearer ' + this.token)
})
}
}
大吉大利,今晚吃鸡!🚀
总结
拦截器是 Angular 4.3 中一个重要的功能,这里我们看到了很多很棒的功能。现在发挥你的创造力,我相信你可以想出一些有趣的东西!
记住,通过使用拦截器,你可以像蝙蝠侠一样棒!
- 10.x 版本,译者版本
GitHub 示例代码 10.x版本 📜
StackBlitz 线上运行 🏃 - 8.x 版本,作者版本
GitHub 示例代码 8.x版本 📜
StackBlitz 线上运行 🏃
感谢 Angular In Depth 提供想法和帮助编辑文档。希望我没有忘记任何一个人,谢谢 Max Koretskyi aka Wizard,Tim Deschryver,Alex Okrushko,Alexander Poshtaruk,Lars Gyrup Brink Nielsen,Nacho Vazquez Calleja,thekiba & Alexey Zuev!
资源
- Angular 拦截器内部指南和 HttpClient 机制 by Max Koretskyi,aka Wizard
- Angular 文档
- 许多其他好文章中的点点滴滴
译者参考
感谢阅读
感谢你阅读到这里,翻译的不好的地方,还请指点。希望我的内容能让你受用,再次感谢。by llccing 千里