Angular 中攔截器的真相和 HttpClient 內部機制

原文:Insider’s guide into interceptors and HttpClient mechanics in Angular
做者:Max Koretskyi
原技術博文由 Max Koretskyi 撰寫發佈,他目前於 ag-Grid 擔任開發大使(Developer Advocate)
譯者按:開發大使負責確保其所在的公司認真聽取社區的聲音並向社區傳達他們的行動及目標,其做爲社區和公司之間的紐帶存在。
譯者:Ice Panpan;校對者:dreamdevil00git

您可能知道 Angular 在4.3版本中新引入了強大的 HttpClient。它的一個主要功能是請求攔截(request interception)—— 聲明位於應用程序和後端之間的攔截器的能力。攔截器的文檔寫的很好,展現瞭如何編寫並註冊一個攔截器。在這篇文章中,我將深刻研究 HttpClient 服務的內部機制,特別是攔截器。我相信這些知識對於深刻使用該功能是必要的。閱讀完本文後,您將可以輕鬆瞭解像緩存之類工具的工做流程,並可以輕鬆自如地實現複雜的請求/響應操做方案。github

首先,咱們將使用文檔中描述的方法來註冊兩個攔截器,以便爲請求添加自定義的請求頭。而後咱們將實現自定義的中間件鏈,而不是使用 Angular 定義的機制。最後,咱們將瞭解 HttpClient 的請求方法如何構建 HttpEvents 類型的 observable 流並知足不可變(immutability)性的需求json

與個人大部分文章同樣,咱們將經過操做實例來學習更多內容。bootstrap

應用示例

首先,讓咱們實現兩個簡單的攔截器,每一個攔截器使用文檔中描述的方法向傳出的請求添加請求頭。對於每一個攔截器,咱們聲明一個實現了 intercept 方法的類。在此方法中,咱們經過添加 Custom-Header-1Custom-Header-2 的請求頭信息來修改請求:後端

@Injectable()
export class I1 implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const modified = req.clone({setHeaders: {'Custom-Header-1': '1'}});
        return next.handle(modified);
    }
}

@Injectable()
export class I2 implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const modified = req.clone({setHeaders: {'Custom-Header-2': '2'}});
        return next.handle(modified);
    }
}
複製代碼

正如您所看到的,每一個攔截器都將下一個處理程序做爲第二個參數。咱們須要經過調用該函數來將控制權傳遞給中間件鏈中的下一個攔截器。咱們會很快發現調用 next.handle 時發生了什麼以及爲何有的時候你不須要調用該函數。此外,若是你一直想知道爲何須要對請求調用 clone() 方法,你很快就會獲得答案。api

攔截器實現以後,咱們須要使用 HTTP_INTERCEPTORS 令牌註冊它們:瀏覽器

@NgModule({
    imports: [BrowserModule, HttpClientModule],
    declarations: [AppComponent],
    providers: [
        {
            provide: HTTP_INTERCEPTORS,
            useClass: I1,
            multi: true
        },
        {
            provide: HTTP_INTERCEPTORS,
            useClass: I2,
            multi: true
        }
    ],
    bootstrap: [AppComponent]
})
export class AppModule {}
複製代碼

緊接着執行一個簡單的請求來檢查咱們自定義的請求頭是否添加成功:緩存

@Component({
    selector: 'my-app',
    template: ` <div><h3>Response</h3>{{response|async|json}}</div> <button (click)="request()">Make request</button>`
    ,
})
export class AppComponent {
    response: Observable<any>;
    constructor(private http: HttpClient) {}

    request() {
        const url = 'https://jsonplaceholder.typicode.com/posts/1';
        this.response = this.http.get(url, {observe: 'response'});
    }
}
複製代碼

若是咱們已經正確完成了全部操做,當咱們檢查 Network 選項卡時,咱們能夠看到咱們自定義的請求頭髮送到服務器:服務器

這很容易吧。你能夠在 stackblitz 上找到這個基礎示例。如今是時候研究更有趣的東西了。網絡

實現自定義的中間件鏈

咱們的任務是在不使用 HttpClient 提供的方法的狀況下手動將攔截器集成處處理請求的邏輯中。同時,咱們將構建一個處理程序鏈,就像 Angular 內部完成的同樣。

處理請求

在現代瀏覽器中,AJAX 功能是使用 XmlHttpRequestFetch API 實現的。此外,還有常用的會致使與變動檢測相關的意外結果JSONP 技術。Angular 須要一個使用上述方法之一的服務來向服務器發出請求。這種服務在 HttpClient 文檔上被稱爲 後端(backend),例如:

In an interceptor, next always represents the next interceptor in the chain, if any, or the final backend if there are no more interceptors

在攔截器中,next 始終表示鏈中的下一個攔截器(若是有的話),若是沒有更多攔截器的話則表示最終後端

在 Angular 提供的 HttpClient 模塊中,這種服務有兩種實現方法——使用 XmlHttpRequest API 實現的HttpXhrBackend 和使用 JSONP 技術實現的 JsonpClientBackendHttpClient 中默認使用 HttpXhrBackend

Angular 定義了一個名爲 HTTP(request)handler 的抽象概念,負責處理請求。處理請求的中間件鏈由 HTTP handlers 組成,這些處理程序將請求傳遞給鏈中的下一個處理程序,直到其中一個處理程序返回一個 observable 流。處理程序的接口由抽象類 HttpHandler 定義:

export abstract class HttpHandler {
    abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}
複製代碼

因爲 backend 服務(如 HttpXhrBackend )能夠經過 發出網絡請求來處理請求,因此它是 HTTP handler 的一個例子。經過和 backend 服務通訊來處理請求是最多見的處理形式,但卻不是惟一的處理方式。另外一種常見的請求處理示例是從本地緩存中爲請求提供服務,而不是發送請求給服務器。所以,任何能夠處理請求的服務都應該實現 handle 方法,該方法根據函數簽名返回一個 HTTP events 類型的 observable,如 HttpProgressEventHttpHeaderResponseHttpResponse。所以,若是咱們想提供一些自定義請求處理邏輯,咱們須要建立一個實現了 HttpHandler 接口的服務。

使用 backend 做爲 HTTP handler

HttpClient 服務在 DI 容器中的 HttpHandler 令牌下注入了一個全局的 HTTP handler 。而後經過調用它的 handle 方法來發出請求:

export class HttpClient {
    constructor(private handler: HttpHandler) {}
    
    request(...): Observable<any> {
        ...
        const events$: Observable<HttpEvent<any>> = 
            of(req).pipe(concatMap((req: HttpRequest<any>) => this.handler.handle(req)));
        ...
    }
}
複製代碼

默認狀況下,全局 HTTP handler 是 HttpXhrBackend backend。它被註冊在注入器中的 HttpBackend 令牌下。

@NgModule({
    providers: [
        HttpXhrBackend,
        { provide: HttpBackend, useExisting: HttpXhrBackend } 
    ]
})
export class HttpClientModule {}
複製代碼

正如你可能猜到的那樣 HttpXhrBackend 實現了 HttpHandler 接口:

export abstract class HttpHandler {
    abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}

export abstract class HttpBackend implements HttpHandler {
    abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}

export class HttpXhrBackend implements HttpBackend {
    handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {}
}
複製代碼

因爲默認的 XHR backend 是在 HttpBackend 令牌下注冊的,咱們能夠本身注入並替換 HttpClient 用於發出請求的用法。咱們替換掉下面這個使用 HttpClient 的版本:

export class AppComponent {
    response: Observable<any>;
    constructor(private http: HttpClient) {}

    request() {
        const url = 'https://jsonplaceholder.typicode.com/posts/1';
        this.response = this.http.get(url, {observe: 'body'});
    }
}
複製代碼

讓咱們直接使用默認的 XHR backend,以下所示:

export class AppComponent {
    response: Observable<any>;
    constructor(private backend: HttpXhrBackend) {}

    request() {
        const req = new HttpRequest('GET', 'https://jsonplaceholder.typicode.com/posts/1');
        this.response = this.backend.handle(req);
    }
}
複製代碼

這是示例。在示例中須要注意一些事項。首先,咱們須要手動構建 HttpRequest。其次,因爲 backend 處理程序返回 HTTP events 流,你將在屏幕上看到不一樣的對象一閃而過,最終將呈現整個 http 響應對象。

添加攔截器

咱們已經設法直接使用 backend,但因爲咱們沒有運行攔截器,因此請求頭還沒有添加到請求中。一個攔截器包含處理請求的邏輯,但它要與 HttpClient 一塊兒使用,須要將其封裝到實現了 HttpHandler 接口的服務中。咱們能夠經過執行一個攔截器並將鏈中的下一個處理程序的引用傳遞給此攔截器的方式來實現此服務。這樣攔截器就能夠觸發下一個處理程序,後者一般是 backend。爲此,每一個自定義的處理程序將保存鏈中下一個處理程序的引用,並將其與請求一塊兒傳遞給下一個攔截器。下面就是咱們想要的東西:

在 Angular 中已經存在這種封裝處理程序的方法了並被稱爲 HttpInterceptorHandler。讓咱們用它來封裝咱們的一個攔截器吧。可是不幸的是,Angular 沒有將其導出爲公共 API,所以咱們只能從源代碼中複製基本實現:

export class HttpInterceptorHandler implements HttpHandler {
    constructor(private next: HttpHandler, private interceptor: HttpInterceptor) {}

    handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
        // execute an interceptor and pass the reference to the next handler
        return this.interceptor.intercept(req, this.next);
    }
}
複製代碼

並像這樣使用它來封裝咱們的第一個攔截器:

export class AppComponent {
    response: Observable<any>;
    constructor(private backend: HttpXhrBackend) {}

    request() {
        const req = new HttpRequest('GET', 'https://jsonplaceholder.typicode.com/posts/1');
        const handler = new HttpInterceptorHandler(this.backend, new I1());
        this.response = handler.handle(req);
    }
}
複製代碼

如今,一旦咱們發出請求,咱們就能夠看到 Custom-Header-1 已添加到請求中。這是示例。經過上面的實現,咱們將一個攔截器和引用了下一個處理程序的 XHR backend 封裝進了 HttpInterceptorHandler。如今,這就是這是一條處理程序鏈。

讓咱們經過封裝第二個攔截器來將另外一個處理程序添加到鏈中:

export class AppComponent {
    response: Observable<any>;
    constructor(private backend: HttpXhrBackend) {}

    request() {
        const req = new HttpRequest('GET', 'https://jsonplaceholder.typicode.com/posts/1');
        const i1Handler = new HttpInterceptorHandler(this.backend, new I1());
        const i2Handler = new HttpInterceptorHandler(i1Handler, new I2());
        this.response = i2Handler.handle(req);
    }
}
複製代碼

在這能夠看到演示,如今一切正常,就像咱們在最開始的示例中使用 HttpClient 的那樣。咱們剛剛所作的就是構建了處理程序的中間件鏈,其中每一個處理程序執行一個攔截器並將下一個處理程序的引用傳遞給它。這是鏈的圖表:

當咱們在攔截器中執行 next.handle(modified) 語句時,咱們將控制權傳遞給鏈中的下一個處理程序:

export class I1 implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const modified = req.clone({setHeaders: {'Custom-Header-1': '1'}});
        // passing control to the handler in the chain
        return next.handle(modified);
    }
}
複製代碼

最終,控制權將被傳遞到最後一個 backend 處理程序,該處理程序將對服務器執行請求。

自動封裝攔截器

咱們能夠經過使用 HTTP_INTERCEPTORS 令牌注入全部的攔截器,而後使用 reduceRight 將它們連接起來的方式自動構建攔截器鏈,而不是逐個地手動將攔截器連接起來構成攔截器鏈。咱們這樣作:

export class AppComponent {
    response: Observable<any>;
    constructor(
        private backend: HttpBackend, 
        @Inject(HTTP_INTERCEPTORS) private interceptors: HttpInterceptor[]) {}

    request() {
        const req = new HttpRequest('GET', 'https://jsonplaceholder.typicode.com/posts/1');
        const i2Handler = this.interceptors.reduceRight(
            (next, interceptor) => new HttpInterceptorHandler(next, interceptor), this.backend);
        this.response = i2Handler.handle(req);
    }
}
複製代碼

咱們須要在這裏使用 reduceRight 來從最後註冊的攔截器開始構建一個鏈。使用上面的代碼,咱們會得到與手動構建的處理程序鏈相同的鏈。經過 reduceRight 返回的值是對鏈中第一個處理程序的引用。

實際上,上述我寫的代碼在 Angular 中是使用 interceptingHandler 函數來實現的。原話是這麼說的:

Constructs an HttpHandler that applies a bunch of HttpInterceptors to a request before passing it to the given HttpBackend. Meant to be used as a factory function within HttpClientModule.

構造一個 HttpHandler,在將請求傳遞給給定的 HttpBackend 以前,將一系列 HttpInterceptor 應用於請求。 能夠在 HttpClientModule 中用做工廠函數。

(下面順便貼一下源碼:)

export function interceptingHandler( backend: HttpBackend, interceptors: HttpInterceptor[] | null = []): HttpHandler {
  if (!interceptors) {
    return backend;
  }
  return interceptors.reduceRight(
      (next, interceptor) => new HttpInterceptorHandler(next, interceptor), backend);
}
複製代碼

如今咱們知道是如何構造一條處理函數鏈的了。在 HTTP handler 中須要注意的最後一點是, interceptingHandler 默認爲 HttpHandler

@NgModule({
  providers: [
    {
      provide: HttpHandler,
      useFactory: interceptingHandler,
      deps: [HttpBackend, [@Optional(), @Inject(HTTP_INTERCEPTORS)]],
    }
  ]
})
export class HttpClientModule {}
複製代碼

所以,執行此函數的結果是鏈中第一個處理程序的引用被注入 HttpClient 服務並被使用。

構建處理鏈的 observable 流

好的,如今咱們知道咱們有一堆處理程序,每一個處理程序執行一個關聯的攔截器並調用鏈中的下一個處理程序。調用此鏈返回的值是一個 HttpEvents 類型的 observable 流。這個流一般(但不老是)由最後一個處理程序生成,這跟 backend 的具體實現有關。其餘的處理程序一般只返回該流。下面是大多數攔截器最後的語句:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    ...
    return next.handle(authReq);
}
複製代碼

因此咱們能夠這樣來展現邏輯:

可是由於任何攔截器均可以返回一個 HttpEvents 類型的 observable 流,因此你有不少定製機會。例如,你能夠實現本身的 backend 並將其註冊爲攔截器。或者實現一個緩存機制,若是找到了緩存就當即返回, 而不用交給下個處理程序處理:

此外,因爲每一個攔截器均可以訪問下一個攔截器(經過調用 next.handler())返回的 observable 流,因此咱們能夠經過 RxJs 操做符添加自定義的邏輯來修改返回的流。

構建 HttpClient 的 observable 流

若是您仔細閱讀了前面的部分,那麼您如今可能想知道處理鏈建立的 HTTP events 流是否與調用 HttpClient 方法,如 get 或者 post 所返回的流徹底相同。咦...不是!實現的過程更有意思。

HttpClient 經過使用 RxJS 的建立操做符 of 來將請求對象變爲 observable 流,並在調用 HttpClient 的 HTTP request 方法時返回它。處理程序鏈做爲此流的一部分被同步處理,而且使用 concatMap 操做符壓平鏈返回的 observable實現的關鍵點就在 request 方法,由於全部的 API 方法像 getpostdelete只是包裝了 request 方法:

const events$: Observable<HttpEvent<any>> = of(req).pipe(
    concatMap((req: HttpRequest<any>) => this.handler.handle(req))
);
複製代碼

在上面的代碼片斷中,我用 pipe 替換了舊技術 call。若是您仍然對 concatMap 如何工做感到困惑,你能夠閱讀學習將 RxJs 序列與超級直觀的交互式圖表相結合。有趣的是,處理程序鏈在以 of 開頭的 observable 流中執行是有緣由的,這裏有一個解釋:

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).

經過 Observable.of() 初始請求,並在 concatMap() 中運行處理程序(包括全部攔截器)。這樣,處理程序就在一個 Observable 鏈中運行,這會使得攔截器會在每一個訂閱上從新運行(這樣重試的時候也會從新運行處理程序,包括攔截器)。

處理 ‘observe’ 請求選項

經過 HttpClient 建立的初始 observable 流,發出了全部的 HTTP events,如 HttpProgressEventHttpHeaderResponseHttpResponse。可是從文檔中咱們知道咱們能夠經過設置 observe 選項來指定咱們感興趣的事件:

request() {
    const url = 'https://jsonplaceholder.typicode.com/posts/1';
    this.response = this.http.get(url, {observe: 'body'});
}
複製代碼

使用 {observe: 'body'} 後,從 get 方法返回的 observable 流只會發出響應中 body 部分的內容。 observe 的其餘選項還有 eventsresponse 而且 response 是默認選項。在探索處理程序鏈的實現的一開始,我就指出過調用處理程序鏈返回的流會發出全部 HTTP events。根據 observe 的參數過濾這些 events 是 HttpClient 的責任。

這意味着我在上一節中演示 HttpClient 返回流的實現須要稍微調整一下。咱們須要作的是過濾這些 events 並根據 observe 參數值將它們映射到不一樣的值。接下來簡單實現下:

const events$: Observable<HttpEvent<any>> = of(req).pipe(...)

if (options.observe === 'events') {
    return events$;
}

const res$: Observable<HttpResponse<any>> =
    events$.pipe(filter((event: HttpEvent<any>) => event instanceof HttpResponse));

if (options.observe === 'response') {
    return res$;
}

if (options.observe === 'body') {
    return res$.pipe(map((res: HttpResponse<any>) => res.body));
}
複製代碼

在這裏,您能夠找到源碼。

不可變性的須要

文檔上關於不變性的一個有趣的段落是這樣的:

Interceptors exist to examine and mutate outgoing requests and incoming responses. However, it may be surprising to learn that the HttpRequest and HttpResponse classes are largely immutable. This is for a reason: because the app may retry requests, the interceptor chain may process an individual request multiple times. If requests were mutable, a retried request would be different than the original request. Immutability ensures the interceptors see the same request for each try.

雖然攔截器有能力改變請求和響應,但 HttpRequest 和 HttpResponse 實例的屬性倒是隻讀(readonly)的,所以,它們在很大意義上說是不可變對象。有充足的理由把它們作成不可變對象:應用可能會重試發送不少次請求以後才能成功,這就意味着這個攔截器鏈表可能會屢次重複處理同一個請求。 若是攔截器能夠修改原始的請求對象,那麼重試階段的操做就會從修改過的請求開始,而不是原始請求。 而這種不可變性,能夠確保這些攔截器在每次重試時看到的都是一樣的原始請求。

讓我詳細說明一下。當您調用 HttpClient 的任何 HTTP 請求方法時,就會建立請求對象。正如我在前面部分中解釋的那樣,此請求用於生成一個 events$ 的 observable 序列,而且在訂閱時,它會在處理程序鏈中被傳遞。可是 events$ 流可能會被重試,這意味着在序列以外建立的原始請求對象可能再次觸發序列屢次。但攔截器應始終以原始請求開始。若是請求是可變的,而且能夠在攔截器運行期間進行修改,則此條件不適用於下一次攔截器運行。因爲同一請求對象的引用將屢次用於開始 observable 序列,請求及其全部組成部分,如 HttpHeadersHttpParams 應該是不可變的。

相關文章
相關標籤/搜索