什么是HTTP_INTERCEPTORS?
const HTTP_INTERCEPTORS: InjectionToken<HttpInterceptor[]>;
什么是HttpInterceptor?
攔截HttpRequest並處理它們
interface HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
}
說明
大多數攔截器都會在外發的請求由next.handle(transformedReq)發給攔截器鏈中的下一個攔截器之前,對該請求進行轉換。
攔截器還可以通過為next.handle()返回的流添加額外的RxJS操作符,來對響應事件流進行轉換。
極少數情況下,攔截器也可以自己完全處理一個請求,並且組合出新的事件流來而不必調用next.handle()。
這也是允許的,不過要時刻記住,這將會完全跳過所有后繼攔截器。
另一種同樣罕見但是有用的攔截器,會為單個請求在事件流上給出多個響應對象。
方法
intercept()
標識和處理給定的HTTP請求
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<>any>
req HttpRequest 要處理的傳出請求對象
next HttpHandler 鏈中的下一個攔截器,如果鏈中沒有攔截器,則成為后端。
返回值
Observable<HttpEvent<any>> 事件流的一個可觀察對象
使用說明
要想在整個應用中使用HttpInterceptors的同一個實例,就只能在AppModule模塊中導入HttpClientModule,
並且把攔截器都添加到應用到根注入器中。如果你在不同的模塊中多次導入HttpClientModule,則每次導入都會創建HttpClientModule的一個新復本,
它將會覆蓋根模塊上提供的那些攔截器。
攔截請求和響應
參考:https://angular.cn/guide/http#intercepting-requests-and-responses
借助攔截機制,你可以聲明一些攔截器,它們可以檢查並轉換從應用中發給服務器的HTTP請求。這些攔截器還可以在返回應用的途中檢查和轉換來自服務器的響應。
多個攔截器構成了請求/響應處理器的雙向鏈表。
攔截器可以用一種常規的、標准的方式對每一次HTTP的請求/響應任務執行從認證到記日志等很多種隱式任務。
如果沒有攔截機制,那么開發人員將不得不對每次HttpClient調用顯示實現這些任務。
編寫攔截器
要實現攔截器,就要實現一個實現了HttpInterceptor接口中的intercept()方法的類。
這里是一個什么也不做的空白攔截器,它只會不做任何修改的傳遞這個請求。
import { Injectable } from '@angular/core'; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable() export class NoopInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(req); } }
intercept方法會把請求轉換成一個最終返回HTTP響應體的Observable。
在這個場景中,每個攔截器都完全能自己處理這個請求。
大多數攔截器攔截都會在傳入時檢查請求,然后把(可能被修改過的)請求轉發給next對象的handle()方法,而next對象實現了HttpHandle接口。
export abstract class HttpHandler { abstract hande(req: HttpRequest<any>): Observable<HttpEvent<any>>; }
像intercept一樣,handle()方法也會把HTTP請求轉換成HttpEvents組成的Observable,它最終包含的是來自服務器的響應。
intercept()函數可以檢查這個可觀察對象,並在把它返回給調用者之前修改它。
這個無操作的攔截器,會直接使用原始的請求調用next.handle(),並返回它返回的可觀察對象,而不做任何后續處理。
next對象
next對象表示攔截器鏈表中的下一個攔截器。這個鏈表中的最后一個next對象就是HttpClient的后端處理器(backend handler),
它會把請求發給服務器,並接收服務器的響應。
大多數的攔截器都會調用next.handle(),以便這個請求流能走到下一個攔截器,並最終傳給后端處理器。攔截器也可以不調用next.handle(),
使這個鏈路短路,並返回一個帶有人工構造出來的服務器響應的自己的Observable
這是一種常見的中間件模式,在像Express.js這樣的框架中也會找到它。
提供這個攔截器
這個NoopInterceptor就是一個由Angular依賴注入DI系統管理的服務。像其他服務一樣,你也必須先提供這個攔截器類,應用才能使用它。
由於攔截器是HttpClient服務的(可選)依賴,所以你必須在提供HttpClient的同一個(或其各級父注入器)注入器中提供這些攔截器。
那些在DI創建完HttpClient之后再提供的攔截器將會被忽略。
由於在AppModule中導入了HttpClientModule,導致本應用在其根注入器中提供了HttpClient,所以你也同樣要在AppModule中提供這些攔截器。
在從@angular/common/http中導入了Http_INTERCEPTORS注入令牌之后,編寫如下NoopInterceptor提供者注冊語句:
{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true }
注意multi: true選擇。
這個必須的選項會告訴Angular HTTP_INTERCEPTORS是一個多重提供者的令牌,表示它會注入一個多值的數組,而不是單一的值。
你也可以直接把這個提供者添加到AppModule中的提供者數組中,不過那樣會非常啰嗦。況且,你將來還會用這種方式創建更多的攔截器並提供它們。
你還要特別注意提供這些攔截器的順序。
認真考慮創建一個封裝桶barrel文件,用於把所有攔截器都收集起來,一起提供給httpInterceptorProviders數組,可以先從這個NoopInterceptor開始
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} ];
然后導入它,並把它加到AppModule的providers數組中,就像這樣:
... import { httpInterceptorProviders } from './core/http-interceptors'; ... ... providers: [ httpInterceptorProviders, { provide: NZ_I18N, useValue: zh_CN }], ...
當你再創建新的攔截器時,就同樣把它們添加到httpInterceptorProviders數組中,而不用再修改AppModule
攔截器的順序
Angular會按照你提供它們的順序應用這些攔截器。
如果你提供攔截器的順序是先A,再B,再C,那么請求階段的執行順序就是A->B->C
而響應階段的執行順序則是C->B->A
以后你就再也不能修改這個順序或移除某些攔截器了。如果你需要動態啟用或禁用某個攔截器,那就要在那個攔截器中自行實現這個功能。
處理攔截器事件
大多數HttpClient方法都會返回HttpResponse<any>型的可觀察對象。
HttpResponse類本身就是一個事件,它的類型是HttpEventType.Response
但是,單個HTTP請求可以生成其他類型的多個事件,包括報告上傳和下載進度的事件。
HttpInterceptor.intercept()和HttpHandler.handle()會返回HttpEvent<any>型的可觀察對象。
很多攔截器只關心發出的請求,而對next.handle()返回的事件流不會做任何修改。但是,有些攔截器需要檢查並修改next.handle()響應。
上述做法就可以在流中看到所有這些事件。
雖然攔截器有能力改變請求和響應,但HttpRequest和HttpResponse實例的屬性卻是只讀(readonly)的,因此讓它們基本上是不可變的。
有充足的理由把它們做成不可變對象:應用可能會重試發送很多次請求之后才能成功,這就意味着這個攔截器鏈表可能會多次重復處理同一個請求。
如果攔截器可以修改原始的請求對象,那么重試階段的操作就會從修改過的請求開始,而不是原始請求。而這種不可變性,可以確保這些攔截器在每次重試時看到的都是同樣的原始請求。
你的攔截器應該在沒有任何修改的情況下返回每一個事件,除非它有令人信服的理由去做。
TypeScript會阻止你設置HttpRequest的只讀屬性
/** 錯誤: Cannot assign to 'url' because it is a read-only property. */ req.url = req.url.replace('http://', 'https://');
如果你必須修改一個請求,先把它克隆一份,修改這個克隆體后再把它傳給next.handle()
你可以在一步中克隆並修改此請求,例子如下:
const secureReq = req.clone({ url: req.url.replace('http://', 'https://') }); return next.handle(secureReq);
這個clone()方法的哈希型參數允許你在復制出克隆體的同時改變請求的某些特定屬性。
修改請求體
readonly這種賦值保護,無法防范深修改(修改子對象的屬性),也不能防范你修改請求體對象中的屬性。
req.body.name = req.body.name.trim(); // bad idea!
如果必須修改請求體,請執行以下步驟
1. 復制請求體並在副本中進行修改
2. 使用clone()方法克隆這個請求對象
3. 用修改過的副本替換被克隆的請求體
app/http-interceptors/trim-name-interceptor.ts(except)
const body = req.body; const newBody = { ...body, name: body.name.trim() }; const newReq = req.clone({ body: newBody }); return next.handle(newReq);
克隆時清除請求體
有時,你需要清除請求體而不是替換它。為此,請將克隆后的請求體設置為null
提示:如果你把克隆后的請求體設為undefined,那么Angular會認為你想讓請求體保持原樣。
const newReq = req.clone({body: null});
設置默認請求頭
(未完)