原文首發於 baishusama.github.io,歡迎圍觀~
前言
恍然間發現這個錯誤已經不復存在了,於是稍微看了下相關 issue、commit、PR。寫篇筆記祭奠下~
需求描述
一個使用 HttpInterceptor
的常見場景是實現基於 token 的驗證機制。
為什麼要使用攔截(intercepting)呢?
因為,在基於 token 的驗證機制中,證明使用者身份的 token 需要被附帶在每一個(需要驗證的請求的)請求頭。如果不使用攔截手段,那麼(由 HttpClient
例項觸發的)每一個請求都需要手動修改請求頭(header)。顯然手動修改是繁瑣和難以維護的。所以,我們選擇做攔截。
Angular 官網也給出了範例,以下程式碼可以實現一個 AuthInterceptor
攔截器:
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 之前,執行上述官方給出的程式碼是會報錯的。原因是 存在迴圈引用問題!
依賴關係1
我們看一下上述程式碼:AuthInterceptor
由於需要使用 AuthService
服務提供的獲取 token 的方法,依賴注入了 AuthService
:
AuthInterceptor -> AuthService // AuthInterceptor 攔截器需要 AuthService 服務來獲取 token
複製程式碼
依賴關係2
而一般情況下我們的 AuthService
需要做登入登出等操作,特別是需要和後端互動以獲取 token,所以需要依賴注入 HttpClient
,存在依賴關係:
AuthService -> HttpClient // AuthService 服務需要 HttpClient 服務來和後端互動
複製程式碼
依賴關係3
從下述原始碼可以看出,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)
:
// 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. 好久沒寫文章了,這篇文章簡直在划水……所以我肯定很多地方沒講清楚(特別是程式碼都沒有細講),各位看官哪裡沒看明白的請務必指出,我會根據需要慢慢補充。望輕拍磚(逃
參考
- Angular CHANGELOG.md
- fix(common): allow HttpInterceptors to inject HttpClient
- Insider’s guide into interceptors and HttpClient mechanics in Angular:這篇寫得相當得好,深入了攔截器和
HttpClient
的內部機制,推薦閱讀!