成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

[NG] 考古 - HttpInterceptor 循環(huán)引用錯(cuò)誤

myeveryheart / 4230人閱讀

摘要:官網(wǎng)也給出了范例,以下代碼可以實(shí)現(xiàn)一個(gè)攔截器問題描述但在之前,執(zhí)行上述官方給出的代碼是會報(bào)錯(cuò)的??梢垣@取攔截器服務(wù)的實(shí)例們。

原文首發(fā)于 baishusama.github.io,歡迎圍觀~
前言

恍然間發(fā)現(xiàn)這個(gè)錯(cuò)誤已經(jīng)不復(fù)存在了,于是稍微看了下相關(guān) issue、commit、PR。寫篇筆記祭奠下~

需求描述

一個(gè)使用 HttpInterceptor 的常見場景是實(shí)現(xiàn)基于 token 的驗(yàn)證機(jī)制。

為什么要使用攔截(intercepting)呢?

因?yàn)?,在基?token 的驗(yàn)證機(jī)制中,證明用戶身份的 token 需要被附帶在每一個(gè)(需要驗(yàn)證的請求的)請求頭。如果不使用攔截手段,那么(由 HttpClient 實(shí)例觸發(fā)的)每一個(gè)請求都需要手動修改請求頭(header)。顯然手動修改是繁瑣和難以維護(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, 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)錯(cuò)的。原因是 存在循環(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 是一個(gè) 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 里,每一個(gè)服務(wù)實(shí)例的初始化所需要的依賴都是需要事先準(zhǔn)備好的,但一個(gè)循環(huán)依賴是永遠(yuǎn)也準(zhǔn)備不好的……Angular 因此會檢測循環(huán)依賴的存在,并在循環(huán)依賴被檢測到時(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)部通過 Map 類型的 _seenProviders 來記錄看到過的供應(yīng)商。

在其方法 _getOrCreateLocalProvider 內(nèi)部判斷是否已經(jīng)看過,如果已經(jīng)看過會在 _errors 中記錄一個(gè) ProviderError 錯(cuò)誤。

我用 5.2.2 版本的 Angular 編寫了一個(gè)遵循官方文檔寫法但出現(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 值的,所以會向 _errors 中記錄一個(gè) Cannot instantiate cyclic dependency! 開頭的錯(cuò)誤。當(dāng)執(zhí)行完所有代碼之后,控制臺會出現(xiàn)該錯(cuò)誤:

用戶的修復(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 方法時(shí)才去獲取 auth 服務(wù)實(shí)例,而不是將 auth 作為依賴注入、在調(diào)用構(gòu)造函數(shù)的時(shí)候去獲取。

由此我們繞開了編譯階段的對循環(huán)依賴做的檢查。

官方的修復(fù)

就像 PR 里提到的這樣:

Either HttpClient or the user has to deal specially with the circular dependency.

所以,為了造福大眾,最終官方做出了修改,原理和作為用戶的我們的代碼的思路是一致的——利用懶加載解決循環(huán)依賴問題!

因?yàn)樾迯?fù)的代碼量很少,所以這里整個(gè)摘錄下。

首先,新增 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 方法時(shí)才去獲取 HTTP_INTERCEPTORS 攔截器依賴,而不是將 interceptors 作為依賴注入(在調(diào)用構(gòu)造函數(shù)的時(shí)候去獲?。?。

也就是官方修復(fù)的思路如下:

AuthInterceptor -> AuthService -> HttpClient -x-> AuthInterceptor
即 AuthInterceptor -> AuthService -> HttpClient,其中,在 HttpClient 中懶加載 interceptors

因?yàn)?AuthInterceptorAuthService 的引用和 AuthServiceHttpClient 的引用是用戶定義的,所以官方可以控制的只剩下 HttpClient 到攔截器的依賴引用了。所以,官方選擇從 HttpClient 處切斷依賴。

那么,我們?yōu)槭裁催x擇從 AuthInterceptor 處而不是從 AuthService 處切斷依賴呢?

我覺得原因有二:

一個(gè)是為了讓 AuthService 盡可能保持透明——對 interceptor 引起的問題沒有察覺。因?yàn)楸举|(zhì)上這是 interceptors 不能依賴注入 HttpClient 的問題。

另一個(gè)是 AuthService 往往有很多能觸發(fā) HttpClient 使用的方法,那么在什么時(shí)候去通過 injector 來 get HttpClient 服務(wù)實(shí)例呢?或者說所有方法都加上相關(guān)判斷么?……所以為了避免問題的復(fù)雜化,選擇選項(xiàng)更少(只有一個(gè) intercept 方法)的 AuthInterceptor 顯然更為明智。

后記

還是太年輕,以前翻 github 的時(shí)候沒有及時(shí)訂閱 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ù)煤?,深入了攔截器和 HttpClient 的內(nèi)部機(jī)制,推薦閱讀!

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/97060.html

相關(guān)文章

  • HttpInterceptor 攔截器 - 網(wǎng)絡(luò)請求超時(shí)與重試的簡單實(shí)現(xiàn)

    摘要:對象表示攔截器鏈表中的下一個(gè)攔截器。至此,攔截器只會再重試到最大次數(shù)還是失敗的情況下拋出超時(shí)錯(cuò)誤。完成上述步驟,一個(gè)簡單的網(wǎng)絡(luò)請求超時(shí)與重試的攔截器便實(shí)現(xiàn)了。 ... 攔截器在Angular項(xiàng)目中其實(shí)有著十分重要的地位,攔截器可以統(tǒng)一對 HTTP 請求進(jìn)行攔截處理,我們可以在每個(gè)請求體或者響應(yīng)后對應(yīng)的流添加一系列動作或者處理數(shù)據(jù),再返回給使用者調(diào)用。 每個(gè) API 調(diào)用的時(shí)候都不可避免...

    stonezhu 評論0 收藏0
  • 使用ng2-admin搭建成熟可靠的后臺系統(tǒng) -- ng2-admin(五)

    摘要:創(chuàng)建一個(gè)工具類,負(fù)責(zé)提供以及完成拼接參數(shù)的工作。根據(jù)我們的配置,來創(chuàng)建這個(gè)文件。因?yàn)槭潜韱翁峤?,所以我們新建一個(gè)服務(wù),由它來完成表單提交的最后一步。 使用ng2-admin搭建成熟可靠的后臺系統(tǒng) -- ng2-admin(五) 完善動態(tài)表單組件 升級Angular 4.1 -> 4.3 添加 json-server 模擬數(shù)據(jù) 創(chuàng)建自己的 http 完成一次表單提交 升級Angu...

    MiracleWong 評論0 收藏0
  • 我理解的 core 目錄

    摘要:只需引入一次的什么是項(xiàng)目中只需要引入一次的舉個(gè)例子,全局錯(cuò)誤處理根路由數(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...

    callmewhy 評論0 收藏0
  • 簽發(fā)的用戶認(rèn)證token超時(shí)刷新策略

    摘要:簽發(fā)的用戶認(rèn)證超時(shí)刷新策略這個(gè)模塊分離至項(xiàng)目權(quán)限管理系統(tǒng)與前后端分離實(shí)踐,感覺那樣太長了找不到重點(diǎn),分離出來要好點(diǎn)。這樣在有效期過后的時(shí)間段內(nèi)可以申請刷新。 簽發(fā)的用戶認(rèn)證token超時(shí)刷新策略 這個(gè)模塊分離至項(xiàng)目api權(quán)限管理系統(tǒng)與前后端分離實(shí)踐,感覺那樣太長了找不到重點(diǎn),分離出來要好點(diǎn)。 對于登錄的用戶簽發(fā)其對應(yīng)的jwt,我們在jwt設(shè)置他的固定有效期時(shí)間,在有效期內(nèi)用戶攜帶jw...

    e10101 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<