摘要:官網(wǎng)也給出了范例,以下代碼可以實(shí)現(xiàn)一個攔截器問題描述但在之前,執(zhí)行上述官方給出的代碼是會報(bào)錯的。可以獲取攔截器服務(wù)的實(shí)例們。
原文首發(fā)于 baishusama.github.io,歡迎圍觀~前言
恍然間發(fā)現(xiàn)這個錯誤已經(jīng)不復(fù)存在了,于是稍微看了下相關(guān) issue、commit、PR。寫篇筆記祭奠下~
需求描述一個使用 HttpInterceptor 的常見場景是實(shí)現(xiàn)基于 token 的驗(yàn)證機(jī)制。
為什么要使用攔截(intercepting)呢?
因?yàn)椋诨?token 的驗(yàn)證機(jī)制中,證明用戶身份的 token 需要被附帶在每一個(需要驗(yàn)證的請求的)請求頭。如果不使用攔截手段,那么(由 HttpClient 實(shí)例觸發(fā)的)每一個請求都需要手動修改請求頭(header)。顯然手動修改是繁瑣和難以維護(hù)的。所以,我們選擇做攔截。
Angular 官網(wǎng)也給出了范例,以下代碼可以實(shí)現(xiàn)一個 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問題描述, next: HttpHandler): Observable > { const authToken = this.auth.getAuthorizationToken(); const authReq = req.clone({ headers: req.headers.set("Authorization", authToken) }); return next.handle(authReq); } }
但在 5.2.3 之前,執(zhí)行上述官方給出的代碼是會報(bào)錯的。原因是 存在循環(huán)引用問題!
依賴關(guān)系1我們看一下上述代碼:AuthInterceptor 由于需要使用 AuthService 服務(wù)提供的獲取 token 的方法,依賴注入了 AuthService:
AuthInterceptor -> AuthService // AuthInterceptor 攔截器需要 AuthService 服務(wù)來獲取 token依賴關(guān)系2
而一般情況下我們的 AuthService 需要做登錄登出等操作,特別是需要和后端交互以獲取 token,所以需要依賴注入 HttpClient,存在依賴關(guān)系:
AuthService -> HttpClient // AuthService 服務(wù)需要 HttpClient 服務(wù)來和后端交互依賴關(guān)系3
從下述源碼可以看出,HttpClient 服務(wù)依賴注入了 HttpHandler:
// v5.2.x export class HttpClient { constructor(private handler: HttpHandler) {} request(...): Observable{ let req: HttpRequest ; ... // 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 > = concatMap.call(of (req), (req: HttpRequest ) => 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 實(shí)例,用于標(biāo)識所有攔截器服務(wù)。new Inject(HTTP_INTERCEPTORS) 可以獲取攔截器服務(wù)的實(shí)例們。
這里的“token”是 Angular 的 DI 系統(tǒng)中用于標(biāo)識以來對象的東西。token 可以是字符串或者 Type/InjectionToken/OpaqueToken 類的實(shí)例。P.S. 關(guān)于使用哪一種 token 更好的問題,可以【TODO:】看一下這篇文章(譯文)。
也就是說,HttpClient 依賴于所有 HttpInterceptors,包括 AuthInterceptor:
HttpClient -> AuthInterceptor // HttpClient 服務(wù)需要 AuthInterceptor 在內(nèi)的所有攔截器服務(wù)來處理請求循環(huán)依賴
綜上,我們有循環(huán)依賴:
AuthInterceptor -> AuthService -> HttpClient -> AuthInterceptor -> ...
而在 Angular 里,每一個服務(wù)實(shí)例的初始化所需要的依賴都是需要事先準(zhǔn)備好的,但一個循環(huán)依賴是永遠(yuǎn)也準(zhǔn)備不好的……Angular 因此會檢測循環(huán)依賴的存在,并在循環(huán)依賴被檢測到時報(bào)錯,部分源碼如下:
// v5.2.x export class NgModuleProviderAnalyzer { private _transformedProviders = new Map(); private _seenProviders = new Map (); private _allProviders: Map ; 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 內(nèi)部通過 Map 類型的 _seenProviders 來記錄看到過的供應(yīng)商。
在其方法 _getOrCreateLocalProvider 內(nèi)部判斷是否已經(jīng)看過,如果已經(jīng)看過會在 _errors 中記錄一個 ProviderError 錯誤。
我用 5.2.2 版本的 Angular 編寫了一個遵循官方文檔寫法但出現(xiàn)“循環(huán)引用錯誤”的示例項(xiàng)目。下面是我 ng serve 運(yùn)行該應(yīng)用后,在 compiler.js 中添加斷點(diǎn)調(diào)試得到的結(jié)果:
圖一、截圖時 _seenProviders 中已經(jīng)記錄的各個供應(yīng)商:
圖二、截圖時 token 變量的值:
在上述截圖中,根據(jù)圖二的 token 變量是能在 _seenProviders 中獲取到非 null 值的,所以會向 _errors 中記錄一個 Cannot instantiate cyclic dependency! 開頭的錯誤。當(dāng)執(zhí)行完所有代碼之后,控制臺會出現(xiàn)該錯誤:
用戶的修復(fù)那么在 5.2.2 及以前,作為 Angular 開發(fā)者,要如何解決上述問題呢?
我們可以通過注入 Injector 手動懶加載 AuthService 而不是直接注入其到 constructor,來使依賴關(guān)系變?yōu)槿缦拢?/p>
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, next: HttpHandler): Observable > { 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,并通過其實(shí)例對象 this.injector 在調(diào)用 intercept 方法時才去獲取 auth 服務(wù)實(shí)例,而不是將 auth 作為依賴注入、在調(diào)用構(gòu)造函數(shù)的時候去獲取。
由此我們繞開了編譯階段的對循環(huán)依賴做的檢查。
官方的修復(fù)就像 PR 里提到的這樣:
Either HttpClient or the user has to deal specially with the circular dependency.
所以,為了造福大眾,最終官方做出了修改,原理和作為用戶的我們的代碼的思路是一致的——利用懶加載解決循環(huán)依賴問題!
因?yàn)樾迯?fù)的代碼量很少,所以這里整個摘錄下。
首先,新增 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): Observable > { 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 依賴的創(chuàng)建方式由原來的使用 useFactory: interceptingHandler 函數(shù)(代碼二):
// 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 { }
不難發(fā)現(xiàn),在“代碼一”中我們看到了熟悉的寫法:依賴注入 Injector,并通過其實(shí)例對象 this.injector 在調(diào)用 handle 方法時才去獲取 HTTP_INTERCEPTORS 攔截器依賴,而不是將 interceptors 作為依賴注入(在調(diào)用構(gòu)造函數(shù)的時候去獲取)。
也就是官方修復(fù)的思路如下:
AuthInterceptor -> AuthService -> HttpClient -x-> AuthInterceptor 即 AuthInterceptor -> AuthService -> HttpClient,其中,在 HttpClient 中懶加載 interceptors
因?yàn)?AuthInterceptor 對 AuthService 的引用和 AuthService 對 HttpClient 的引用是用戶定義的,所以官方可以控制的只剩下 HttpClient 到攔截器的依賴引用了。所以,官方選擇從 HttpClient 處切斷依賴。
后記那么,我們?yōu)槭裁催x擇從 AuthInterceptor 處而不是從 AuthService 處切斷依賴呢?
我覺得原因有二:
一個是為了讓 AuthService 盡可能保持透明——對 interceptor 引起的問題沒有察覺。因?yàn)楸举|(zhì)上這是 interceptors 不能依賴注入 HttpClient 的問題。
另一個是 AuthService 往往有很多能觸發(fā) HttpClient 使用的方法,那么在什么時候去通過 injector 來 get HttpClient 服務(wù)實(shí)例呢?或者說所有方法都加上相關(guān)判斷么?……所以為了避免問題的復(fù)雜化,選擇選項(xiàng)更少(只有一個 intercept 方法)的 AuthInterceptor 顯然更為明智。
還是太年輕,以前翻 github 的時候沒有及時訂閱 issue,導(dǎo)致一些問題修復(fù)了都毫無察覺……
從今天起,好好訂閱 issue,好好整理筆記,共勉~
P.S. 好久沒寫文章了,這篇文章簡直在劃水……所以我肯定很多地方?jīng)]講清楚(特別是代碼都沒有細(xì)講),各位看官哪里沒看明白的請務(wù)必指出,我會根據(jù)需要慢慢補(bǔ)充。望輕拍磚(逃參考
Angular CHANGELOG.md
fix(common): allow HttpInterceptors to inject HttpClient
Insider’s guide into interceptors and HttpClient mechanics in Angular:這篇寫得相當(dāng)?shù)煤茫钊肓藬r截器和 HttpClient 的內(nèi)部機(jī)制,推薦閱讀!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/97060.html
摘要:對象表示攔截器鏈表中的下一個攔截器。至此,攔截器只會再重試到最大次數(shù)還是失敗的情況下拋出超時錯誤。完成上述步驟,一個簡單的網(wǎng)絡(luò)請求超時與重試的攔截器便實(shí)現(xiàn)了。 ... 攔截器在Angular項(xiàng)目中其實(shí)有著十分重要的地位,攔截器可以統(tǒng)一對 HTTP 請求進(jìn)行攔截處理,我們可以在每個請求體或者響應(yīng)后對應(yīng)的流添加一系列動作或者處理數(shù)據(jù),再返回給使用者調(diào)用。 每個 API 調(diào)用的時候都不可避免...
摘要:創(chuàng)建一個工具類,負(fù)責(zé)提供以及完成拼接參數(shù)的工作。根據(jù)我們的配置,來創(chuàng)建這個文件。因?yàn)槭潜韱翁峤唬晕覀冃陆ㄒ粋€服務(wù),由它來完成表單提交的最后一步。 使用ng2-admin搭建成熟可靠的后臺系統(tǒng) -- ng2-admin(五) 完善動態(tài)表單組件 升級Angular 4.1 -> 4.3 添加 json-server 模擬數(shù)據(jù) 創(chuàng)建自己的 http 完成一次表單提交 升級Angu...
摘要:只需引入一次的什么是項(xiàng)目中只需要引入一次的舉個例子,全局錯誤處理根路由數(shù)據(jù)預(yù)加載請求攔截器等。更漂亮的是為我們提供了攔截器接口,我們只管開發(fā)攔截器邏輯功能,調(diào)用及使用全部控制權(quán)都在框架內(nèi)。 ... 過了一遍 Angular 文檔 的小伙伴大致都會記得最佳實(shí)踐中提到過的有關(guān)CoreModule的一些解釋和說明,其實(shí)關(guān)于名字的命名不是強(qiáng)制性的,只要團(tuán)隊(duì)中一致 pass,你把它命名為XXXM...
摘要:簽發(fā)的用戶認(rèn)證超時刷新策略這個模塊分離至項(xiàng)目權(quán)限管理系統(tǒng)與前后端分離實(shí)踐,感覺那樣太長了找不到重點(diǎn),分離出來要好點(diǎn)。這樣在有效期過后的時間段內(nèi)可以申請刷新。 簽發(fā)的用戶認(rèn)證token超時刷新策略 這個模塊分離至項(xiàng)目api權(quán)限管理系統(tǒng)與前后端分離實(shí)踐,感覺那樣太長了找不到重點(diǎn),分離出來要好點(diǎn)。 對于登錄的用戶簽發(fā)其對應(yīng)的jwt,我們在jwt設(shè)置他的固定有效期時間,在有效期內(nèi)用戶攜帶jw...
閱讀 3847·2021-09-06 15:00
閱讀 2180·2019-08-30 15:53
閱讀 3287·2019-08-23 16:44
閱讀 951·2019-08-23 15:19
閱讀 1398·2019-08-23 12:27
閱讀 4198·2019-08-23 11:30
閱讀 590·2019-08-23 10:33
閱讀 376·2019-08-22 16:05