原文首發於 baishusama.github.io,歡迎圍觀~
恍然間發現這個錯誤已經不復存在了,因而稍微看了下相關 issue、commit、PR。寫篇筆記祭奠下~html
一個使用 HttpInterceptor
的常見場景是實現基於 token 的驗證機制。git
爲何要使用攔截(intercepting)呢?github
由於,在基於 token 的驗證機制中,證實用戶身份的 token 須要被附帶在每個(須要驗證的請求的)請求頭。若是不使用攔截手段,那麼(由 HttpClient
實例觸發的)每個請求都須要手動修改請求頭(header)。顯然手動修改是繁瑣和難以維護的。因此,咱們選擇作攔截。typescript
Angular 官網也給出了範例,如下代碼能夠實現一個 AuthInterceptor
攔截器:segmentfault
import { Injectable } from '@angular/core'; import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { AuthService } from '../auth.service'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private auth: AuthService) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const authToken = this.auth.getAuthorizationToken(); const authReq = req.clone({ headers: req.headers.set('Authorization', authToken) }); return next.handle(authReq); } }
但在 5.2.3 以前,執行上述官方給出的代碼是會報錯的。緣由是 存在循環引用問題!後端
咱們看一下上述代碼:AuthInterceptor
因爲須要使用 AuthService
服務提供的獲取 token 的方法,依賴注入了 AuthService
:app
AuthInterceptor -> AuthService // AuthInterceptor 攔截器須要 AuthService 服務來獲取 token
而通常狀況下咱們的 AuthService
須要作登陸登出等操做,特別是須要和後端交互以獲取 token,因此須要依賴注入 HttpClient
,存在依賴關係:ide
AuthService -> HttpClient // AuthService 服務須要 HttpClient 服務來和後端交互
從下述源碼能夠看出,HttpClient
服務依賴注入了 HttpHandler
:函數
// v5.2.x export class HttpClient { constructor(private handler: HttpHandler) {} request(...): Observable<any> { let req: HttpRequest<any>; ... // Start with an Observable.of() the initial request, and run the handler (which // includes all interceptors) inside a concatMap(). This way, the handler runs // inside an Observable chain, which causes interceptors to be re-run on every // subscription (this also makes retries re-run the handler, including interceptors). const events$: Observable<HttpEvent<any>> = concatMap.call(of (req), (req: HttpRequest<any>) => this.handler.handle(req)); ... }
而 HttpHandler
的依賴中包含可選的 new Inject(HTTP_INTERCEPTORS)
:ui
// v5.2.2 @NgModule({ imports: [...], providers: [ HttpClient, // HttpHandler is the backend + interceptors and is constructed // using the interceptingHandler factory function. { provide: HttpHandler, useFactory: interceptingHandler, deps: [HttpBackend, [new Optional(), new Inject(HTTP_INTERCEPTORS)]], }, HttpXhrBackend, {provide: HttpBackend, useExisting: HttpXhrBackend}, ... ], }) export class HttpClientModule { }
其中,HTTP_INTERCEPTORS
是一個 InjectionToken
實例,用於標識全部攔截器服務。new Inject(HTTP_INTERCEPTORS)
能夠獲取攔截器服務的實例們。
這裏的「token」是 Angular 的 DI 系統中用於標識以來對象的東西。token 能夠是字符串或者Type
/InjectionToken
/OpaqueToken
類的實例。
也就是說,HttpClient
依賴於全部 HttpInterceptor
s,包括 AuthInterceptor
:
HttpClient -> AuthInterceptor // HttpClient 服務須要 AuthInterceptor 在內的全部攔截器服務來處理請求
綜上,咱們有循環依賴:
AuthInterceptor -> AuthService -> HttpClient -> AuthInterceptor -> ...
而在 Angular 裏,每個服務實例的初始化所須要的依賴都是須要事先準備好的,但一個循環依賴是永遠也準備很差的……Angular 所以會檢測循環依賴的存在,並在循環依賴被檢測到時報錯,部分源碼以下:
// v5.2.x export class NgModuleProviderAnalyzer { private _transformedProviders = new Map<any, ProviderAst>(); private _seenProviders = new Map<any, boolean>(); private _allProviders: Map<any, ProviderAst>; private _errors: ProviderError[] = []; ... private _getOrCreateLocalProvider(token: CompileTokenMetadata, eager: boolean): ProviderAst|null { const resolvedProvider = this._allProviders.get(tokenReference(token)); if (!resolvedProvider) { return null; } let transformedProviderAst = this._transformedProviders.get(tokenReference(token)); if (transformedProviderAst) { return transformedProviderAst; } if (this._seenProviders.get(tokenReference(token)) != null) { this._errors.push( new ProviderError(`Cannot instantiate cyclic dependency! ${tokenName(token)}`, resolvedProvider.sourceSpan)); return null; } this._seenProviders.set(tokenReference(token), true); ... } }
讓咱們稍微看一下代碼:
NgModuleProviderAnalyzer
內部經過 Map
類型的 _seenProviders
來記錄看到過的供應商。_getOrCreateLocalProvider
內部判斷是否已經看過,若是已經看過會在 _errors
中記錄一個 ProviderError
錯誤。我用 5.2.2 版本的 Angular 編寫了一個遵循官方文檔寫法但出現「循環引用錯誤」的示例項目。下面是我 ng serve
運行該應用後,在 compiler.js
中添加斷點調試獲得的結果:
_seenProviders
中已經記錄的各個供應商:token
變量的值:在上述截圖中,根據圖二的 token
變量是能在 _seenProviders
中獲取到非 null
值的,因此會向 _errors
中記錄一個 Cannot instantiate cyclic dependency!
開頭的錯誤。當執行完全部代碼以後,控制檯會出現該錯誤:
那麼在 5.2.2 及之前,做爲 Angular 開發者,要如何解決上述問題呢?
咱們能夠經過注入 Injector
手動懶加載 AuthService
而不是直接注入其到 constructor
,來使依賴關係變爲以下:
AuthInterceptor --x-> AuthService -> HttpClient -> AuthInterceptor --x-> 即 AuthService -> HttpClient -> AuthInterceptor,其中,在 AuthInterceptor 中懶加載 AuthService
即將官方的示例代碼修改成以下:
import { Injectable, Injector } from '@angular/core'; import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { AuthService } from '../auth.service'; @Injectable() export class AuthInterceptor implements HttpInterceptor { private auth: AuthService; constructor(private injector: Injector) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { this.auth = this.injector.get(AuthService); const authToken = this.auth.getAuthorizationToken(); const authReq = req.clone({ headers: req.headers.set('Authorization', authToken) }); return next.handle(authReq); } }
能夠看到和官方的代碼相比,咱們改成依賴注入 Injector
,並經過其實例對象 this.injector
在調用 intercept
方法時纔去獲取 auth
服務實例,而不是將 auth
做爲依賴注入、在調用構造函數的時候去獲取。
由此咱們繞開了編譯階段的對循環依賴作的檢查。
就像 PR 裏提到的這樣:
Either HttpClient or the user has to deal specially with the circular dependency.
因此,爲了造福大衆,最終官方作出了修改,原理和做爲用戶的咱們的代碼的思路是一致的——利用懶加載解決循環依賴問題!
由於修復的代碼量不多,因此這裏整個摘錄下。
首先,新增 HttpInterceptingHandler
類(代碼一):
// v5.2.3 /** * An `HttpHandler` that applies a bunch of `HttpInterceptor`s * to a request before passing it to the given `HttpBackend`. * * The interceptors are loaded lazily from the injector, to allow * interceptors to themselves inject classes depending indirectly * on `HttpInterceptingHandler` itself. */ @Injectable() export class HttpInterceptingHandler implements HttpHandler { private chain: HttpHandler|null = null; constructor(private backend: HttpBackend, private injector: Injector) {} handle(req: HttpRequest<any>): Observable<HttpEvent<any>> { if (this.chain === null) { const interceptors = this.injector.get(HTTP_INTERCEPTORS, []); this.chain = interceptors.reduceRight( (next, interceptor) => new HttpInterceptorHandler(next, interceptor), this.backend); } return this.chain.handle(req); } }
HttpHandler
依賴的建立方式由原來的使用 useFactory: interceptingHandler
函數(代碼二):
// v5.2.2 @NgModule({ imports: [...], providers: [ HttpClient, // HttpHandler is the backend + interceptors and is constructed // using the interceptingHandler factory function. { provide: HttpHandler, useFactory: interceptingHandler, deps: [HttpBackend, [new Optional(), new Inject(HTTP_INTERCEPTORS)]], }, HttpXhrBackend, {provide: HttpBackend, useExisting: HttpXhrBackend}, ... ], }) export class HttpClientModule { }
改成使用 useClass: HttpInterceptingHandler
類(代碼三):
// v5.2.3 @NgModule({ imports: [...], providers: [ HttpClient, {provide: HttpHandler, useClass: HttpInterceptingHandler}, HttpXhrBackend, {provide: HttpBackend, useExisting: HttpXhrBackend}, ... ], }) export class HttpClientModule { }
不難發現,在「代碼一」中咱們看到了熟悉的寫法:依賴注入 Injector
,並經過其實例對象 this.injector
在調用 handle
方法時纔去獲取 HTTP_INTERCEPTORS
攔截器依賴,而不是將 interceptors
做爲依賴注入(在調用構造函數的時候去獲取)。
也就是官方修復的思路以下:
AuthInterceptor -> AuthService -> HttpClient -x-> AuthInterceptor 即 AuthInterceptor -> AuthService -> HttpClient,其中,在 HttpClient 中懶加載 interceptors
由於 AuthInterceptor
對 AuthService
的引用和 AuthService
對 HttpClient
的引用是用戶定義的,因此官方能夠控制的只剩下 HttpClient
到攔截器的依賴引用了。因此,官方選擇從 HttpClient
處切斷依賴。
那麼,咱們爲何選擇從
AuthInterceptor
處而不是從AuthService
處切斷依賴呢?我以爲緣由有二:
- 一個是爲了讓
AuthService
儘量保持透明——對 interceptor 引發的問題沒有察覺。由於本質上這是 interceptors 不能依賴注入HttpClient
的問題。- 另外一個是
AuthService
每每有不少能觸發HttpClient
使用的方法,那麼在何時去經過injector
來 getHttpClient
服務實例呢?或者說全部方法都加上相關判斷麼?……因此爲了不問題的複雜化,選擇選項更少(只有一個intercept
方法)的AuthInterceptor
顯然更爲明智。
仍是太年輕,之前翻 github 的時候沒有及時訂閱 issue,致使一些問題修復了都毫無察覺……
從今天起,好好訂閱 issue,好好整理筆記,共勉~
P.S. 很久沒寫文章了,這篇文章簡直在划水……因此我確定不少地方沒講清楚(特別是代碼都沒有細講),各位看官哪裏沒看明白的請務必指出,我會根據須要慢慢補充。望輕拍磚(逃
HttpClient
的內部機制,推薦閱讀!