借助攔截機(jī)制,你可以聲明一些攔截器,它們可以檢查并轉(zhuǎn)換從應(yīng)用中發(fā)給服務(wù)器的 HTTP 請求。這些攔截器還可以在返回應(yīng)用的途中檢查和轉(zhuǎn)換來自服務(wù)器的響應(yīng)。多個攔截器構(gòu)成了請求/響應(yīng)處理器的雙向鏈表。
攔截器可以用一種常規(guī)的、標(biāo)準(zhǔn)的方式對每一次 HTTP 的請求/響應(yīng)任務(wù)執(zhí)行從認(rèn)證到記日志等很多種隱式任務(wù)。
如果沒有攔截機(jī)制,那么開發(fā)人員將不得不對每次 HttpClient
調(diào)用顯式實(shí)現(xiàn)這些任務(wù)。
要實(shí)現(xiàn)攔截器,就要實(shí)現(xiàn)一個實(shí)現(xiàn)了 HttpInterceptor
接口中的 intercept()
方法的類。
這里是一個什么也不做的空白攔截器,它只會不做任何修改的傳遞這個請求。
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
方法會把請求轉(zhuǎn)換成一個最終返回 HTTP 響應(yīng)體的 Observable
。 在這個場景中,每個攔截器都完全能自己處理這個請求。
大多數(shù)攔截器攔截都會在傳入時檢查請求,然后把(可能被修改過的)請求轉(zhuǎn)發(fā)給 next
對象的 handle()
方法,而 next
對象實(shí)現(xiàn)了 HttpHandler
接口。
export abstract class HttpHandler {
abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}
像 intercept()
一樣,handle()
方法也會把 HTTP 請求轉(zhuǎn)換成 HttpEvents
組成的 Observable
,它最終包含的是來自服務(wù)器的響應(yīng)。 intercept()
函數(shù)可以檢查這個可觀察對象,并在把它返回給調(diào)用者之前修改它。
這個無操作的攔截器,會直接使用原始的請求調(diào)用 next.handle()
,并返回它返回的可觀察對象,而不做任何后續(xù)處理。
next
對象表示攔截器鏈表中的下一個攔截器。 這個鏈表中的最后一個 next
對象就是 HttpClient
的后端處理器(backend handler
),它會把請求發(fā)給服務(wù)器,并接收服務(wù)器的響應(yīng)。
大多數(shù)的攔截器都會調(diào)用 next.handle()
,以便這個請求流能走到下一個攔截器,并最終傳給后端處理器。 攔截器也可以不調(diào)用 next.handle()
,使這個鏈路短路,并返回一個帶有人工構(gòu)造出來的服務(wù)器響應(yīng)的 自己的 Observable
。
這是一種常見的中間件模式,在像 "Express.js" 這樣的框架中也會找到它。
這個 NoopInterceptor
就是一個由 Angular 依賴注入 (DI
)系統(tǒng)管理的服務(wù)。 像其它服務(wù)一樣,你也必須先提供這個攔截器類,應(yīng)用才能使用它。
由于攔截器是 HttpClient
服務(wù)的(可選)依賴,所以你必須在提供 HttpClient
的同一個(或其各級父注入器)注入器中提供這些攔截器。 那些在 DI
創(chuàng)建完 HttpClient
之后再提供的攔截器將會被忽略。
由于在 AppModule
中導(dǎo)入了 HttpClientModule
,導(dǎo)致本應(yīng)用在其根注入器中提供了 HttpClient
。所以你也同樣要在 AppModule
中提供這些攔截器。
在從 @angular/common/http
中導(dǎo)入了 HTTP_INTERCEPTORS
注入令牌之后,編寫如下的 NoopInterceptor
提供者注冊語句:
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
注意 multi: true
選項(xiàng)。 這個必須的選項(xiàng)會告訴 Angular HTTP_INTERCEPTORS
是一個多重提供者的令牌,表示它會注入一個多值的數(shù)組,而不是單一的值。
你也可以直接把這個提供者添加到 AppModule
中的提供者數(shù)組中,不過那樣會非常啰嗦。況且,你將來還會用這種方式創(chuàng)建更多的攔截器并提供它們。 你還要特別注意提供這些攔截器的順序。
認(rèn)真考慮創(chuàng)建一個封裝桶(barrel
)文件,用于把所有攔截器都收集起來,一起提供給 httpInterceptorProviders
數(shù)組,可以先從這個 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)建新的攔截器時,就同樣把它們添加到 httpInterceptorProviders
數(shù)組中,而不用再修改 AppModule
。
Angular 會按照你提供它們的順序應(yīng)用這些攔截器。 如果你提供攔截器的順序是先 A,再 B,再 C,那么請求階段的執(zhí)行順序就是 A->B->C,而響應(yīng)階段的執(zhí)行順序則是 C->B->A。
以后你就再也不能修改這些順序或移除某些攔截器了。 如果你需要動態(tài)啟用或禁用某個攔截器,那就要在那個攔截器中自行實(shí)現(xiàn)這個功能。
大多數(shù) HttpClient
方法都會返回 HttpResponse<any>
型的可觀察對象。HttpResponse
類本身就是一個事件,它的類型是 HttpEventType.Response
。但是,單個 HTTP
請求可以生成其它類型的多個事件,包括報(bào)告上傳和下載進(jìn)度的事件。HttpInterceptor.intercept()
和 HttpHandler.handle()
會返回 HttpEvent<any>
型的可觀察對象。
很多攔截器只關(guān)心發(fā)出的請求,而對 next.handle()
返回的事件流不會做任何修改。 但是,有些攔截器需要檢查并修改 next.handle()
的響應(yīng)。上述做法就可以在流中看到所有這些事件。
雖然攔截器有能力改變請求和響應(yīng),但 HttpRequest
和 HttpResponse
實(shí)例的屬性卻是只讀(readonly
)的, 因此讓它們基本上是不可變的。
有充足的理由把它們做成不可變對象:應(yīng)用可能會重試發(fā)送很多次請求之后才能成功,這就意味著這個攔截器鏈表可能會多次重復(fù)處理同一個請求。 如果攔截器可以修改原始的請求對象,那么重試階段的操作就會從修改過的請求開始,而不是原始請求。 而這種不可變性,可以確保這些攔截器在每次重試時看到的都是同樣的原始請求。
你的攔截器應(yīng)該在沒有任何修改的情況下返回每一個事件,除非它有令人信服的理由去做。
TypeScript 會阻止你設(shè)置 HttpRequest
的只讀屬性。
// Typescript disallows the following assignment because req.url is readonly
req.url = req.url.replace('http://', 'https://');
如果你必須修改一個請求,先把它克隆一份,修改這個克隆體后再把它傳給 next.handle()
。你可以在一步中克隆并修改此請求,例子如下。
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);
這個 clone()
方法的哈希型參數(shù)允許你在復(fù)制出克隆體的同時改變該請求的某些特定屬性。
readonly
這種賦值保護(hù),無法防范深修改(修改子對象的屬性),也不能防范你修改請求體對象中的屬性。
req.body.name = req.body.name.trim(); // bad idea!
如果必須修改請求體,請執(zhí)行以下步驟。
clone()
方法克隆這個請求對象。 // 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è)置為 null
。
注:
- 如果你把克隆后的請求體設(shè)為
undefined
,那么 Angular 會認(rèn)為你想讓請求體保持原樣。
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)用通常會使用攔截器來設(shè)置外發(fā)請求的默認(rèn)請求頭。
該范例應(yīng)用具有一個 AuthService
,它會生成一個認(rèn)證令牌。 在這里,AuthInterceptor
會注入該服務(wù)以獲取令牌,并對每一個外發(fā)的請求添加一個帶有該令牌的認(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);
}
}
這種在克隆請求的同時設(shè)置新請求頭的操作太常見了,因此它還有一個快捷方式 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截器可以同時處理請求和響應(yīng),所以它們也可以對整個 HTTP 操作執(zhí)行計(jì)時和記錄日志等任務(wù)。
考慮下面這個 LoggingInterceptor
,它捕獲請求的發(fā)起時間、響應(yīng)的接收時間,并使用注入的 MessageService
來發(fā)送總共花費(fèi)的時間。
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
操作符會捕獲請求成功了還是失敗了。 RxJS 的 finalize
操作符無論在響應(yīng)成功還是失敗時都會調(diào)用(這是必須的),然后把結(jié)果匯報(bào)給 MessageService
。
在這個可觀察對象的流中,無論是 tap
還是 finalize
接觸過的值,都會照常發(fā)送給調(diào)用者。
攔截器還可以自行處理這些請求,而不用轉(zhuǎn)發(fā)給 next.handle()
。
比如,你可能會想緩存某些請求和響應(yīng),以便提升性能。 你可以把這種緩存操作委托給某個攔截器,而不破壞你現(xiàn)有的各個數(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ù)用于決定該請求是否允許緩存。 在這個例子中,只有發(fā)到 npm
包搜索 API
的 GET
請求才是可以緩存的。of()
函數(shù)返回一個已緩存的響應(yīng)體的可觀察對象,然后繞過 next
處理器(以及所有其它下游攔截器)。sendRequest()
。這個函數(shù)會創(chuàng)建一個沒有請求頭的請求克隆體,這是因?yàn)?npm API
禁止它們。然后,該函數(shù)把請求的克隆體轉(zhuǎn)發(fā)給 next.handle()
,它會最終調(diào)用服務(wù)器并返回來自服務(wù)器的響應(yīng)對象。/**
* 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)對象,該操作符的回調(diào)函數(shù)會把該響應(yīng)對象添加到緩存中。
然后,原始的響應(yīng)會通過這些攔截器鏈,原封不動的回到服務(wù)器的調(diào)用者那里。
數(shù)據(jù)服務(wù),比如 PackageSearchService
,并不知道它們收到的某些 HttpClient
請求實(shí)際上是從緩存的請求中返回來的。
HttpClient.get()
方法通常會返回一個可觀察對象,它會發(fā)出一個值(數(shù)據(jù)或錯誤)。攔截器可以把它改成一個可以發(fā)出多個值的可觀察對象。
修改后的 CachingInterceptor
版本可以返回一個立即發(fā)出所緩存響應(yī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)是由一個自定義的x-refresh
請求頭觸發(fā)的。
PackageSearchComponent
中的一個檢查框會切換withRefresh
標(biāo)識, 它是PackageSearchService.search()
的參數(shù)之一。search()
方法創(chuàng)建了自定義的x-refresh
頭,并在調(diào)用HttpClient.get()
前把它添加到請求里。
修改后的 CachingInterceptor
會發(fā)起一個服務(wù)器請求,而不管有沒有緩存的值。 就像 前面 的 sendRequest()
方法一樣進(jìn)行訂閱。 在訂閱 results$
可觀察對象時,就會發(fā)起這個請求。
results$
。result$
的管道中,使用重組后的可觀察對象進(jìn)行處理,并發(fā)出兩次。 先立即發(fā)出一次緩存的響應(yīng)體,然后發(fā)出來自服務(wù)器的響應(yīng)。 訂閱者將會看到一個包含這兩個響應(yīng)的序列。
更多建議: