Angular拦截器&应用场景

HTTP拦截器是@ angular / common / http的主要功能。 可以使用拦截器,去检查HTTP请求并将其从应用程序转换为服务器。

顾名思义,HttpInterceptor 会拦截 Angular 应用程序中发出的 Http请求。 拦截意味着他们在将Http请求传递到Web服务器之前捕获并引导了Http请求。

在Angular中,与服务器的通信是通过注入HttpClient服务类并调用以下方法来完成的:

  • get
  • post
  • delete
  • options
  • put

based on the action we want on the server.

创建一个拦截器

1
2
3
4
5
6
7
8
9
10
@Component({
...
})
class AppComponent {
feed$: Observable
constructor(private httpClient: HttpClient) {}
ngOnInit() {
this.feed$ = this.httpClient.get("/api/feed")
}
}

创建拦截器之前,先利用 HttpClient 创建一个请求

如果在应用程序上配置了HttpInterceptor,它将捕获到 / api / feed 的Http GET请求,并且可以使用它执行所有操作,从身份验证到记录所有开发人员。

要在Angular应用中创建HttpInterceptor,首先要创建一个服务类,并使该类实现HttpInterceptor接口的拦截方法。

1
2
3
4
5
6
@Injectable()
class AnHttpInterceptor implemenss HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req);
}
}

我们这里有一个HttpInterceptor,即AnHttpInterceptor,它实际上不执行任何操作,只是将请求向下传递给拦截器链(拦截器可以有多个,调用顺序与链表顺序一致)。

HttpInterceptor必须实现拦截方法,这是它在链中第一个参数req和下一个HttpHandler中接收Http请求的位置。 为了将Http请求传递到拦截器管道,它调用HttpHandler#handle方法。

这个AnHttpInterceptor不会拦截任何Http请求,需要将其添加到NgModule

1
2
3
4
5
6
7
@NgModule({
providers: [
{
provide: HTTP_INTERCEPTORS,useClass: AnHttpInterceptor, multi: true
}
]
})

AnHttpInterceptor将在我们的应用程序中拦截Http请求。

对HttpInterceptors的工作原理有了 了解,再来看看它能起到什么作用。

请求头修改

我们可以从HttpInterceptor修改Http标头。

headers是Angular的HttpRequest类的一个属性,它是HttpHeaders的对象。HttpHeaders类具有headersproperty,它是一个Map实例,用于以键值方式存储标头。

因此,从传递给HttpInterceptor拦截方法的HttpRequest对象中,我们可以引用标头:

1
2
3
4
5
6
7
8
9
10
11
12
@Injectable()
class AnHttpInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// we can set a new Header
req.headers.set({"MyHeader": 789})
// we can modify a Header
req.headers.append({"Content-Type": null})
// we can delete a Header
req.headers.delete("Content-Type")
return next.handle(req);
}
}

我们更改了HttpRequest请求。 官方说req HttpRequest应该保持不变。 这是为了避免HttpInterceptors多次重新处理同一请求。

因此,我们需要克隆req并对其进行修改。 HttpRequest具有方法克隆,用于创建原始请求的新副本。 我们使用了clone方法来复制一个http请求副本,并修改副本中的标头,然后将该副本向下传递到链中。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Injectable()
class AnHttpInterceptor implemenss HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const reqCopy = req.clone()
// we can set a new Header
reqCopy.headers.set({"MyHeader": 789})
// we can modify a Header
reqCopy.headers.append({"Content-Type": null})
// we can delete a Header
reqCopy.headers.delete("Content-Type")
return next.handle(reqCopy);
}
}

如果我们的Http请求是这样的:

1
2
3
4
5
6
7
8
{
headers: {
Content-Type: "aplication/json"
},
body: {...},
url: "",
method: ""
}

经过AnHttpInterceptor之后; 它将被修改成:

1
2
3
4
5
6
7
8
{
headers: {
"MyHeader": 789
},
body: {...},
url: "",
method: ""
}

这就是服务器将收到的内容。

请求体修改

我们已经了解了如何在req HttpRequest中修改header属性。我们还可以通过HttpInterceptor修改请求主体。在HttpRequest中,主体是一个属性,用于通过post方法向服务器发送附加信息。因此,通过传递给HttpInterceptor的HttpRequest对象,我们可以引用body属性并随意修改它。

假设我们的Http请求是这样的

1
httpClient.post("/api/login", { name: "tommy", password: "xxxx"})

这个{name: tommy, password: xxxx}是Http请求中的主体。在我们的AnHttpInterceptor中,我们可以修改任何属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Injectable()
class AnHttpInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// remove name
cont body = {password: req.body.password }
const reqCopy = req.clone({
body
})
// modfiy name to "chidume"
cont body = {...req.body, "name": "chidume"}
const reqCopy = req.clone({
body
})
return next.handle(reqCopy);
}
}

现在服务器将获得:

1
2
3
4
5
6
7
{
url: "/api/login",
body: {
name: "chidume",
password: "xxxx"
}
}

关于请求体(HttpRequest body)

由于可以获取HttpRequest,因此可以修改其中的任何属性:URL,方法,标头,报文和其他请求配置选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class HttpRequest {
/**
* The request body, or `null` if one isn't set.
*
* Bodies are not enforced to be immutable, as they can include a reference to any
* user-defined data type. However, interceptors should take care to preserve
* idempotence by treating them as such.
*/
readonly body: T|null = null;
/**
* Outgoing headers for this request.
*/
// TODO(issue/24571): remove '!'.
readonly headers !: HttpHeaders;
/**
* Whether this request should be made in a way that exposes progress events.
*
* Progress events are expensive (change detection runs on each event) and so
* they should only be requested if the consumer intends to monitor them.
*/
readonly reportProgress: boolean = false;
/**
* Whether this request should be sent with outgoing credentials (cookies).
*/
readonly withCredentials: boolean = false;
/**
* The expected response type of the server.
*
* This is used to parse the response appropriately before returning it to
* the requestee.
*/
readonly responseType: 'arraybuffer'|'blob'|'json'|'text' = 'json';
/**
* The outgoing HTTP request method.
*/
readonly method: string;
/**
* Outgoing URL parameters.
*/
// TODO(issue/24571): remove '!'.
readonly params !: HttpParams;
/**
* The outgoing URL with all URL parameters set.
*/
readonly urlWithParams: string;
}

通过将HttpRequest对象传递给HttpInterceptor的拦截方法,我们可以修改HttpRequest类中的任何属性。

认证/授权

HttpInterceptor可用于为前往特定域的HTTP请求设置授权标头。

当我们要保护服务器中的API端点时,通过HttpInterceptors授权非常有用。 我们创建一个HttpInterceptor,它为每个HTTP请求附加一个带有 “Bearer” 令牌的授权标头:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const token = localStorage.getItem("token");
if (token) {
const cloned = req.clone({
headers: req.headers.set("Authorization", "Bearer " + token)
});
return next.handle(cloned);
}
else {
return next.handle(req);
}
}
}

首先,它从localStroage获取令牌。 然后克隆Http请求(因为Http请求保持不变),并在请求上为 “Authorization” 标头设置了带有令牌的值 “Bearer” 。 最后将其传递到管道中。

让我们看看如何在服务器上验证此令牌。

为了验证请求,我们不得不从Authorization标头中提取JWT,并检查时间戳和用户标识符。 此身份验证应用于我们要授权的路由。

假设我们有一台Express-Backed节点服务器。 使用Express创建路由并添加身份验证中间件:

1
2
3
4
const app = express()
app
.route("api/refferals")
.get(authMidWare, refferalCtrl.getRefferals)

路线 “api/refferals” 受到保护。 在调用refferalCtrl.getRefferals以返回推荐列表之前,authMidWare必须先对HTTP请求中的Authorization标头进行身份验证,然后把访问权限传递给 “refferalCtrl.getRefferals” 。

我们使用express-jwt库来验证Bearer令牌。

1
2
3
4
5
6
7
8
9
npm i express-jwt
const app = express()
const jwt = require("express-jwt")
const authMidWare = jwt({
secret: YOUR_SECRET_KEY_HERE
})
app
.route("api/refferals")
.get(authMidWare, refferalCtrl.getRefferals)

向jwt传递了一个秘密密钥,它将用于认证Authorization标头中的Bearer令牌。 如果承载令牌未正确签名并且令牌已过期,则jwt将引发错误。 如果全部通过,将运行refferalCtrl.getRefferals中间件,并将返回refferals列表。

后端模拟

有时候,我们想搭建一个server来测试我们的Angular应用程序行为。 我们甚至会发现,用Nodejs或其他语言框架中最简单的服务还是挺麻烦。 我们只需要简单的东西,它们可以非常快速地为我们的JSON对象提供服务。

在这种情况下,HttpInterceptors将变得非常有用。 在这种情况下,我们不会通过HttpHandler#handle向下传递Http请求请求,而是返回并给HttpResponse提供所需的伪数据。

假设我们有一个从服务器请求用户列表的组件:

1
2
3
4
5
6
7
8
9
@Component({
...
})
class AppComponent {
users$: Observable
ngOnInit() {
this.users$ = this.httpClient.get("/api/users")
}
}

我们将使用HttpInterceptor截取对“/ api / users”的Http调用,并返回包含用户数组的Observable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const usersData = {
"users": [
{
"name": "tommy ki",
"age": 26
},
{
"name": "chisom",
"age": 46
},
{
"name": "elvis",
"age": 21
},
{
"name": "osy mattew",
"age": 21
},
{
"name": "valentine",
"age": 21
},
]
}
@Injectable()
class BackendInterceptor implemnets HttpInterceptor {
constructor(private injector: Injector) {}
intercept(request: HttpRequest, next: HttpHandler): Observable<HttpEvent<any>> {
return of(new HttpResponse({
status: 200,
body: usersData
}));
}
}

我们创建了一个新的HttpResponseObservable,而不是HttpHandler#handle,它传入了伪造的用户数组usersData,响应状态为200并返回了Observable,这不会对其他服务产生影响。

我们可以修改拦截器以在知道返回什么之前检查URL路径。
在请求之前对url判断一下就更好了

1
2
3
4
5
6
7
8
9
10
11
12
13
...
@Injectable()
class BackendInterceptor implemnets HttpInterceptor {
constructor(private injector: Injector) {}
intercept(request: HttpRequest, next: HttpHandler): Observable<HttpEvent<any>> {
if(request.url.includes("/api/users"))
return of(new HttpResponse({
status: 200,
body: usersData
}));
next.handle(request)
}
}

我们检查请求对象中的url是否包含 “/api/users” 端点,以便知道返回假用户数组。 如果没有,我们将请求向下传递。

缓存

我们可以缓存Http请求和响应以提高性能。

可以缓存Http请求的GET方法。

举例来说,在应用的个人资料部分。 用户可以在更新个人资料并刷新页面后更新执行POST操作的个人资料,该页面执行GET请求以获取新的个人资料。

如果他再次刷新页面,则该页面不应再次从服务器获取配置文件,因为先前的GET请求将获得与当前Get请求相同的结果,因此它们之间不会执行POST。 不需要在第二个GET请求上再次从服务器获取结果,这会影响性能。

当GET方法之间没有进行任何修改请求时,它是理想的缓存方法。

HttpInterceptor是在Angular中缓存GET方法的不错选择。 让我们看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Injectable()
class CacheInterceptor implements HttpInterceptor {
private cache: Map<HttpRequestHttpResponse> = new Map()

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>{
if(req.method !== "GET") {
return next.handle(req)
}
if(req.headers.get("reset")) {
this.cache.delete(req)
}
const cachedResponse: HttpResponse = this.cache.get(req)
if(cachedResponse) {
return of(cachedResponse.clone())
}else {
return next.handle(req).pipe(
do(stateEvent => {
if(stateEvent instanceof HttpResponse) {
this.cache.set(req, stateEvent.clone())
}
})
).share()
}
}
}

首先,我们有一个缓存,其中HttpRequest和HttpResponse存储在键值对中。 在拦截方法中,检查GET方法,如果该方法不是GET,则不进行缓存,将请求沿链向下传递。

它通过检查标题中的“reset”键来检查是否要重置缓存。 如果存在,则将删除缓存的响应并发出新请求。

接下来,从缓存中获取请求的缓存响应。 如果缓存中有响应,则返回响应。 如果没有,则将新请求最终通过链传送到服务器。 服务器返回一个响应,该响应通过do运算符获得,该响应针对请求用键值对的形式进行缓存。 然后,响应通过拦截器的链传递到调用它的组件/服务。

日志

我们可以从HttpInterceptor记录HttpRequest和HttpResponse。

我们可以记录从请求到响应所花费的时间,我们记录HttpRequest和HttpResponse统计信息,只要您想要的任何信息,就由您决定要记录什么。

让我们看看如何记录从请求到响应的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Injectable()
class LoggingInterceptor implements HttpInterceptor {

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>{
const started = Date.now()
return next.handle(req).pipe(
finalize(() => {
const elapsed = Date.now() - started;
log(`URL: ${req.url} Method: ${req.method} Time took: ${elapsed} ms`)
}))
}
}
}

这将记录请求的URL,到达服务器并获得响应所用的方法和时间。

错误处理

在处理http请求时,难免遇到错误。我们可以把错误统一处理,或做有针对性地处理。

处理错误需要对 HttpErrorResponse 有点了解,我们可以从 HttpErrorResponse 中获取错误信息,状态码,url以及其他属性。

关于 HttpErrorResponse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export declare class HttpErrorResponse extends HttpResponseBase implements Error {
readonly name = "HttpErrorResponse";
readonly message: string;
readonly error: any | null;
/**
* Errors are never okay, even when the status code is in the 2xx success range.
*/
readonly ok = false;
constructor(init: {
error?: any;
headers?: HttpHeaders;
status?: number;
statusText?: string;
url?: string;
});
}

更多错误信息可以查看进一步查看 HttpResponseBase

我这里针对err.status为404的错误,为用户弹窗提示。

首先通过拦截器调用链,利用tap操作符判断是否有错误,再使用 async&await 处理错误操作,以免阻塞。然后判断 err是否属于 HttpErrorResponse ,如果是就可以对错误做你想要的反馈,记录日志也好、用户提示都可以。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Injectable()
class ErrorInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
tap(
(event: HttpEvent<any>) => { },
async (err: any) => {
if (err instanceof HttpErrorResponse) {
if (err.status === 404) {
// TODO
// 弹窗
const toast = await this.toastController.create({
header: err.statusText,
message: err.message,
duration: 3000,
position: 'top',
});
toast.present();
}
}
}
))
}
}

总结

我们可以用HttpInterceptors做的有用的事情无穷无尽。 在这里,我列出了其中的一些内容,感兴趣可以继续查找在Angular应用程序中非常有用的方法。

Thanks for watching!😉