[NG] 考古 - HttpInterceptor 循環引用錯誤

原文首發於 baishusama.github.io,歡迎圍觀~html

前言

恍然間發現這個錯誤已經不復存在了,因而稍微看了下相關 issue、commit、PR。寫篇筆記祭奠下~git

需求描述

一個使用 HttpInterceptor 的常見場景是實現基於 token 的驗證機制。github

爲何要使用攔截(intercepting)呢?typescript

由於,在基於 token 的驗證機制中,證實用戶身份的 token 須要被附帶在每個(須要驗證的請求的)請求頭。若是不使用攔截手段,那麼(由 HttpClient 實例觸發的)每個請求都須要手動修改請求頭(header)。顯然手動修改是繁瑣和難以維護的。因此,咱們選擇作攔截。segmentfault

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 以前,執行上述官方給出的代碼是會報錯的。緣由是 存在循環引用問題bash

依賴關係1

咱們看一下上述代碼:AuthInterceptor 因爲須要使用 AuthService 服務提供的獲取 token 的方法,依賴注入了 AuthServiceapp

AuthInterceptor -> AuthService  // AuthInterceptor 攔截器須要 AuthService 服務來獲取 token
複製代碼

依賴關係2

而通常狀況下咱們的 AuthService 須要作登陸登出等操做,特別是須要和後端交互以獲取 token,因此須要依賴注入 HttpClient,存在依賴關係:ide

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 類的實例。

P.S. 關於使用哪種 token 更好的問題,能夠【TODO:】看一下這篇文章譯文)。

也就是說,HttpClient 依賴於全部 HttpInterceptors,包括 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 中添加斷點調試獲得的結果:

  • 圖1、截圖時 _seenProviders 中已經記錄的各個供應商:
    _seenProviders
  • 圖2、截圖時 token 變量的值:
    token

在上述截圖中,根據圖二的 token 變量是能在 _seenProviders 中獲取到非 null 值的,因此會向 _errors 中記錄一個 Cannot instantiate cyclic dependency! 開頭的錯誤。當執行完全部代碼以後,控制檯會出現該錯誤:

interceptor 循環引用報錯

用戶的修復

那麼在 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
複製代碼

由於 AuthInterceptorAuthService 的引用和 AuthServiceHttpClient 的引用是用戶定義的,因此官方能夠控制的只剩下 HttpClient 到攔截器的依賴引用了。因此,官方選擇從 HttpClient 處切斷依賴。

那麼,咱們爲何選擇從 AuthInterceptor 處而不是從 AuthService 處切斷依賴呢?

我以爲緣由有二:

  1. 一個是爲了讓 AuthService 儘量保持透明——對 interceptor 引發的問題沒有察覺。由於本質上這是 interceptors 不能依賴注入 HttpClient 的問題。
  2. 另外一個是 AuthService 每每有不少能觸發 HttpClient 使用的方法,那麼在何時去經過 injector 來 get HttpClient 服務實例呢?或者說全部方法都加上相關判斷麼?……因此爲了不問題的複雜化,選擇選項更少(只有一個 intercept 方法)的 AuthInterceptor 顯然更爲明智。

後記

仍是太年輕,之前翻 github 的時候沒有及時訂閱 issue,致使一些問題修復了都毫無察覺……

從今天起,好好訂閱 issue,好好整理筆記,共勉~

P.S. 很久沒寫文章了,這篇文章簡直在划水……因此我確定不少地方沒講清楚(特別是代碼都沒有細講),各位看官哪裏沒看明白的請務必指出,我會根據須要慢慢補充。望輕拍磚(逃

參考

相關文章
相關標籤/搜索