借助攔截機(jī)制,你可以聲明一些攔截器,它們可以檢查并轉(zhuǎn)換從應(yīng)用中發(fā)給服務(wù)器的 HTTP 請(qǐng)求。這些攔截器還可以在返回應(yīng)用的途中檢查和轉(zhuǎn)換來自服務(wù)器的響應(yīng)。多個(gè)攔截器構(gòu)成了請(qǐng)求/響應(yīng)處理器的雙向鏈表。
攔截器可以用一種常規(guī)的、標(biāo)準(zhǔn)的方式對(duì)每一次 HTTP 的請(qǐng)求/響應(yīng)任務(wù)執(zhí)行從認(rèn)證到記日志等很多種隱式任務(wù)。
如果沒有攔截機(jī)制,那么開發(fā)人員將不得不對(duì)每次 HttpClient
調(diào)用顯式實(shí)現(xiàn)這些任務(wù)。
要實(shí)現(xiàn)攔截器,就要實(shí)現(xiàn)一個(gè)實(shí)現(xiàn)了 HttpInterceptor
接口中的 intercept()
方法的類。
這里是一個(gè)什么也不做的空白攔截器,它只會(huì)不做任何修改的傳遞這個(gè)請(qǐng)求。
Path:"app/http-interceptors/noop-interceptor.ts" 。
import { Injectable } from '@angular/core';
import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';
/** Pass untouched request through to the next request handler. */
@Injectable()
export class NoopInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler):
Observable<HttpEvent<any>> {
return next.handle(req);
}
}
intercept
方法會(huì)把請(qǐng)求轉(zhuǎn)換成一個(gè)最終返回 HTTP 響應(yīng)體的 Observable
。 在這個(gè)場景中,每個(gè)攔截器都完全能自己處理這個(gè)請(qǐng)求。
大多數(shù)攔截器攔截都會(huì)在傳入時(shí)檢查請(qǐng)求,然后把(可能被修改過的)請(qǐng)求轉(zhuǎn)發(fā)給 next
對(duì)象的 handle()
方法,而 next
對(duì)象實(shí)現(xiàn)了 HttpHandler
接口。
export abstract class HttpHandler {
abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}
像 intercept()
一樣,handle()
方法也會(huì)把 HTTP 請(qǐng)求轉(zhuǎn)換成 HttpEvents
組成的 Observable
,它最終包含的是來自服務(wù)器的響應(yīng)。 intercept()
函數(shù)可以檢查這個(gè)可觀察對(duì)象,并在把它返回給調(diào)用者之前修改它。
這個(gè)無操作的攔截器,會(huì)直接使用原始的請(qǐng)求調(diào)用 next.handle()
,并返回它返回的可觀察對(duì)象,而不做任何后續(xù)處理。
next
對(duì)象表示攔截器鏈表中的下一個(gè)攔截器。 這個(gè)鏈表中的最后一個(gè) next
對(duì)象就是 HttpClient
的后端處理器(backend handler
),它會(huì)把請(qǐng)求發(fā)給服務(wù)器,并接收服務(wù)器的響應(yīng)。
大多數(shù)的攔截器都會(huì)調(diào)用 next.handle()
,以便這個(gè)請(qǐng)求流能走到下一個(gè)攔截器,并最終傳給后端處理器。 攔截器也可以不調(diào)用 next.handle()
,使這個(gè)鏈路短路,并返回一個(gè)帶有人工構(gòu)造出來的服務(wù)器響應(yīng)的 自己的 Observable
。
這是一種常見的中間件模式,在像 "Express.js" 這樣的框架中也會(huì)找到它。
這個(gè) NoopInterceptor
就是一個(gè)由 Angular 依賴注入 (DI
)系統(tǒng)管理的服務(wù)。 像其它服務(wù)一樣,你也必須先提供這個(gè)攔截器類,應(yīng)用才能使用它。
由于攔截器是 HttpClient
服務(wù)的(可選)依賴,所以你必須在提供 HttpClient
的同一個(gè)(或其各級(jí)父注入器)注入器中提供這些攔截器。 那些在 DI
創(chuàng)建完 HttpClient
之后再提供的攔截器將會(huì)被忽略。
由于在 AppModule
中導(dǎo)入了 HttpClientModule
,導(dǎo)致本應(yīng)用在其根注入器中提供了 HttpClient
。所以你也同樣要在 AppModule
中提供這些攔截器。
在從 @angular/common/http
中導(dǎo)入了 HTTP_INTERCEPTORS
注入令牌之后,編寫如下的 NoopInterceptor
提供者注冊(cè)語句:
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
注意 multi: true
選項(xiàng)。 這個(gè)必須的選項(xiàng)會(huì)告訴 Angular HTTP_INTERCEPTORS
是一個(gè)多重提供者的令牌,表示它會(huì)注入一個(gè)多值的數(shù)組,而不是單一的值。
你也可以直接把這個(gè)提供者添加到 AppModule
中的提供者數(shù)組中,不過那樣會(huì)非常啰嗦。況且,你將來還會(huì)用這種方式創(chuàng)建更多的攔截器并提供它們。 你還要特別注意提供這些攔截器的順序。
認(rèn)真考慮創(chuàng)建一個(gè)封裝桶(barrel
)文件,用于把所有攔截器都收集起來,一起提供給 httpInterceptorProviders
數(shù)組,可以先從這個(gè) NoopInterceptor
開始。
Path:"app/http-interceptors/index.ts" 。
/* "Barrel" of Http Interceptors */
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NoopInterceptor } from './noop-interceptor';
/** Http interceptor providers in outside-in order */
export const httpInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
];
然后導(dǎo)入它,并把它加到 AppModule
的 providers
數(shù)組中,就像這樣:
Path:"app/app.module.ts (interceptor providers)" 。
providers: [
httpInterceptorProviders
],
當(dāng)你再創(chuàng)建新的攔截器時(shí),就同樣把它們添加到 httpInterceptorProviders
數(shù)組中,而不用再修改 AppModule
。
Angular 會(huì)按照你提供它們的順序應(yīng)用這些攔截器。 如果你提供攔截器的順序是先 A,再 B,再 C,那么請(qǐng)求階段的執(zhí)行順序就是 A->B->C,而響應(yīng)階段的執(zhí)行順序則是 C->B->A。
以后你就再也不能修改這些順序或移除某些攔截器了。 如果你需要?jiǎng)討B(tài)啟用或禁用某個(gè)攔截器,那就要在那個(gè)攔截器中自行實(shí)現(xiàn)這個(gè)功能。
大多數(shù) HttpClient
方法都會(huì)返回 HttpResponse<any>
型的可觀察對(duì)象。HttpResponse
類本身就是一個(gè)事件,它的類型是 HttpEventType.Response
。但是,單個(gè) HTTP
請(qǐng)求可以生成其它類型的多個(gè)事件,包括報(bào)告上傳和下載進(jìn)度的事件。HttpInterceptor.intercept()
和 HttpHandler.handle()
會(huì)返回 HttpEvent<any>
型的可觀察對(duì)象。
很多攔截器只關(guān)心發(fā)出的請(qǐng)求,而對(duì) next.handle()
返回的事件流不會(huì)做任何修改。 但是,有些攔截器需要檢查并修改 next.handle()
的響應(yīng)。上述做法就可以在流中看到所有這些事件。
雖然攔截器有能力改變請(qǐng)求和響應(yīng),但 HttpRequest
和 HttpResponse
實(shí)例的屬性卻是只讀(readonly
)的, 因此讓它們基本上是不可變的。
有充足的理由把它們做成不可變對(duì)象:應(yīng)用可能會(huì)重試發(fā)送很多次請(qǐng)求之后才能成功,這就意味著這個(gè)攔截器鏈表可能會(huì)多次重復(fù)處理同一個(gè)請(qǐng)求。 如果攔截器可以修改原始的請(qǐng)求對(duì)象,那么重試階段的操作就會(huì)從修改過的請(qǐng)求開始,而不是原始請(qǐng)求。 而這種不可變性,可以確保這些攔截器在每次重試時(shí)看到的都是同樣的原始請(qǐng)求。
你的攔截器應(yīng)該在沒有任何修改的情況下返回每一個(gè)事件,除非它有令人信服的理由去做。
TypeScript 會(huì)阻止你設(shè)置 HttpRequest
的只讀屬性。
// Typescript disallows the following assignment because req.url is readonly
req.url = req.url.replace('http://', 'https://');
如果你必須修改一個(gè)請(qǐng)求,先把它克隆一份,修改這個(gè)克隆體后再把它傳給 next.handle()
。你可以在一步中克隆并修改此請(qǐng)求,例子如下。
Path:"app/http-interceptors/ensure-https-interceptor.ts (excerpt)" 。
// clone request and replace 'http://' with 'https://' at the same time
const secureReq = req.clone({
url: req.url.replace('http://', 'https://')
});
// send the cloned, "secure" request to the next handler.
return next.handle(secureReq);
這個(gè) clone()
方法的哈希型參數(shù)允許你在復(fù)制出克隆體的同時(shí)改變?cè)撜?qǐng)求的某些特定屬性。
readonly
這種賦值保護(hù),無法防范深修改(修改子對(duì)象的屬性),也不能防范你修改請(qǐng)求體對(duì)象中的屬性。
req.body.name = req.body.name.trim(); // bad idea!
如果必須修改請(qǐng)求體,請(qǐng)執(zhí)行以下步驟。
clone()
方法克隆這個(gè)請(qǐng)求對(duì)象。 // copy the body and trim whitespace from the name property
const newBody = { ...body, name: body.name.trim() };
// clone request and set its body
const newReq = req.clone({ body: newBody });
// send the cloned request to the next handler.
return next.handle(newReq);
有時(shí),你需要清除請(qǐng)求體而不是替換它。為此,請(qǐng)將克隆后的請(qǐng)求體設(shè)置為 null
。
注:
- 如果你把克隆后的請(qǐng)求體設(shè)為
undefined
,那么 Angular 會(huì)認(rèn)為你想讓請(qǐng)求體保持原樣。
newReq = req.clone({ ... }); // body not mentioned => preserve original body
newReq = req.clone({ body: undefined }); // preserve original body
newReq = req.clone({ body: null }); // clear the body
應(yīng)用通常會(huì)使用攔截器來設(shè)置外發(fā)請(qǐng)求的默認(rèn)請(qǐng)求頭。
該范例應(yīng)用具有一個(gè) AuthService
,它會(huì)生成一個(gè)認(rèn)證令牌。 在這里,AuthInterceptor
會(huì)注入該服務(wù)以獲取令牌,并對(duì)每一個(gè)外發(fā)的請(qǐng)求添加一個(gè)帶有該令牌的認(rèn)證頭:
Path:"app/http-interceptors/auth-interceptor.ts" 。
import { AuthService } from '../auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private auth: AuthService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
// Get the auth token from the service.
const authToken = this.auth.getAuthorizationToken();
// Clone the request and replace the original headers with
// cloned headers, updated with the authorization.
const authReq = req.clone({
headers: req.headers.set('Authorization', authToken)
});
// send cloned request with header to the next handler.
return next.handle(authReq);
}
}
這種在克隆請(qǐng)求的同時(shí)設(shè)置新請(qǐng)求頭的操作太常見了,因此它還有一個(gè)快捷方式 setHeaders
:
// Clone the request and set the new header in one step.
const authReq = req.clone({ setHeaders: { Authorization: authToken } });
這種可以修改頭的攔截器可以用于很多不同的操作,比如:
If-Modified-Since
因?yàn)閿r截器可以同時(shí)處理請(qǐng)求和響應(yīng),所以它們也可以對(duì)整個(gè) HTTP 操作執(zhí)行計(jì)時(shí)和記錄日志等任務(wù)。
考慮下面這個(gè) LoggingInterceptor
,它捕獲請(qǐng)求的發(fā)起時(shí)間、響應(yīng)的接收時(shí)間,并使用注入的 MessageService
來發(fā)送總共花費(fèi)的時(shí)間。
Path:"app/http-interceptors/logging-interceptor.ts)" 。
import { finalize, tap } from 'rxjs/operators';
import { MessageService } from '../message.service';
@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
constructor(private messenger: MessageService) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
const started = Date.now();
let ok: string;
// extend server response observable with logging
return next.handle(req)
.pipe(
tap(
// Succeeds when there is a response; ignore other events
event => ok = event instanceof HttpResponse ? 'succeeded' : '',
// Operation failed; error is an HttpErrorResponse
error => ok = 'failed'
),
// Log when response observable either completes or errors
finalize(() => {
const elapsed = Date.now() - started;
const msg = `${req.method} "${req.urlWithParams}"
${ok} in ${elapsed} ms.`;
this.messenger.add(msg);
})
);
}
}
RxJS 的 tap
操作符會(huì)捕獲請(qǐng)求成功了還是失敗了。 RxJS 的 finalize
操作符無論在響應(yīng)成功還是失敗時(shí)都會(huì)調(diào)用(這是必須的),然后把結(jié)果匯報(bào)給 MessageService
。
在這個(gè)可觀察對(duì)象的流中,無論是 tap
還是 finalize
接觸過的值,都會(huì)照常發(fā)送給調(diào)用者。
攔截器還可以自行處理這些請(qǐng)求,而不用轉(zhuǎn)發(fā)給 next.handle()
。
比如,你可能會(huì)想緩存某些請(qǐng)求和響應(yīng),以便提升性能。 你可以把這種緩存操作委托給某個(gè)攔截器,而不破壞你現(xiàn)有的各個(gè)數(shù)據(jù)服務(wù)。
下例中的 CachingInterceptor
演示了這種方法。
Path:"app/http-interceptors/caching-interceptor.ts)" 。
@Injectable()
export class CachingInterceptor implements HttpInterceptor {
constructor(private cache: RequestCache) {}
intercept(req: HttpRequest<any>, next: HttpHandler) {
// continue if not cacheable.
if (!isCacheable(req)) { return next.handle(req); }
const cachedResponse = this.cache.get(req);
return cachedResponse ?
of(cachedResponse) : sendRequest(req, next, this.cache);
}
}
isCacheable()
函數(shù)用于決定該請(qǐng)求是否允許緩存。 在這個(gè)例子中,只有發(fā)到 npm
包搜索 API
的 GET
請(qǐng)求才是可以緩存的。of()
函數(shù)返回一個(gè)已緩存的響應(yīng)體的可觀察對(duì)象,然后繞過 next
處理器(以及所有其它下游攔截器)。sendRequest()
。這個(gè)函數(shù)會(huì)創(chuàng)建一個(gè)沒有請(qǐng)求頭的請(qǐng)求克隆體,這是因?yàn)?npm API
禁止它們。然后,該函數(shù)把請(qǐng)求的克隆體轉(zhuǎn)發(fā)給 next.handle()
,它會(huì)最終調(diào)用服務(wù)器并返回來自服務(wù)器的響應(yīng)對(duì)象。/**
* Get server response observable by sending request to `next()`.
* Will add the response to the cache on the way out.
*/
function sendRequest(
req: HttpRequest<any>,
next: HttpHandler,
cache: RequestCache): Observable<HttpEvent<any>> {
// No headers allowed in npm search request
const noHeaderReq = req.clone({ headers: new HttpHeaders() });
return next.handle(noHeaderReq).pipe(
tap(event => {
// There may be other events besides the response.
if (event instanceof HttpResponse) {
cache.put(req, event); // Update the cache.
}
})
);
}
注意 sendRequest()
是如何在返回應(yīng)用程序的過程中攔截響應(yīng)的。該方法通過 tap()
操作符來管理響應(yīng)對(duì)象,該操作符的回調(diào)函數(shù)會(huì)把該響應(yīng)對(duì)象添加到緩存中。
然后,原始的響應(yīng)會(huì)通過這些攔截器鏈,原封不動(dòng)的回到服務(wù)器的調(diào)用者那里。
數(shù)據(jù)服務(wù),比如 PackageSearchService
,并不知道它們收到的某些 HttpClient
請(qǐng)求實(shí)際上是從緩存的請(qǐng)求中返回來的。
HttpClient.get()
方法通常會(huì)返回一個(gè)可觀察對(duì)象,它會(huì)發(fā)出一個(gè)值(數(shù)據(jù)或錯(cuò)誤)。攔截器可以把它改成一個(gè)可以發(fā)出多個(gè)值的可觀察對(duì)象。
修改后的 CachingInterceptor
版本可以返回一個(gè)立即發(fā)出所緩存響應(yīng)的可觀察對(duì)象,然后把請(qǐng)求發(fā)送到 NPM
的 Web API
,然后把修改過的搜索結(jié)果重新發(fā)出一次。
// cache-then-refresh
if (req.headers.get('x-refresh')) {
const results$ = sendRequest(req, next, this.cache);
return cachedResponse ?
results$.pipe( startWith(cachedResponse) ) :
results$;
}
// cache-or-fetch
return cachedResponse ?
of(cachedResponse) : sendRequest(req, next, this.cache);
cache-then-refresh
選項(xiàng)是由一個(gè)自定義的x-refresh
請(qǐng)求頭觸發(fā)的。
PackageSearchComponent
中的一個(gè)檢查框會(huì)切換withRefresh
標(biāo)識(shí), 它是PackageSearchService.search()
的參數(shù)之一。search()
方法創(chuàng)建了自定義的x-refresh
頭,并在調(diào)用HttpClient.get()
前把它添加到請(qǐng)求里。
修改后的 CachingInterceptor
會(huì)發(fā)起一個(gè)服務(wù)器請(qǐng)求,而不管有沒有緩存的值。 就像 前面 的 sendRequest()
方法一樣進(jìn)行訂閱。 在訂閱 results$
可觀察對(duì)象時(shí),就會(huì)發(fā)起這個(gè)請(qǐng)求。
results$
。result$
的管道中,使用重組后的可觀察對(duì)象進(jìn)行處理,并發(fā)出兩次。 先立即發(fā)出一次緩存的響應(yīng)體,然后發(fā)出來自服務(wù)器的響應(yīng)。 訂閱者將會(huì)看到一個(gè)包含這兩個(gè)響應(yīng)的序列。
更多建議: