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