0

Khám phá HttpClientModule trong Angular

Với hầu hết các Angular App thì việc giao tiếp với các service bên ngoài thông qua các API sử dụng phương thức HTTP là không thể tránh khỏi. Trong bài viết này chúng ta sẽ tìm hiểu HttpClientModule hoạt động ra sao và trả lời một số câu hỏi thương phát sinh trong quá trình sử dụng module này. Bắt đầu thôi

1. Cài đặt thế nào?

Để tìm hiểu cách sử dụng cũng như tìm hiểu mọi thứ hoạt động thế nào tốt nhất có lẽ là thực hành trong một giả định nào đó, cùng với các công cụ để debug và một editor để chỉnh sửa code.

Installing Angular on your machine

git clone -b 8.2.x --single-branch https://github.com/angular/angular.git

StackBlitz Bạn cũng có thể theo dõi chúng trên StackBlitz tại đây. Chúng tôi sẽ sử dụng nó trong suốt bài viết để hiểu rõ hơn về cách các thực thể đang kết nối với nhau.

2. What is HttpClientModule?

HttpClientModule là môt service module được cung cấp bởi chính Angular cho phép chúng ta thực hiện các HTTP request, dẽ dàng thao tác với các request cũng như các respone của chúng. Lưu ý: Nó không được gọi là một service module bảo vì nó chỉ instantiates services và không export bất kì components, directives or pipes nào.

3. Bắt đầu nào

Nếu bạn đang ở trong StackBlitz project hãy:

  • open the dev tools
  • tới token.interceptor.ts (CTRL + P) và đặt một breakpoint bên cạnh console.warn ()
  • refresh the StackBlitz browser

Nó sẽ trông như thế này

Bằng cách click vào anonymous function từ client.ts, giờ đây bạn đang ở trong class HTTPClient, đây là function bạn thường inject trong các service của mình. Class này bao gồm các methods hỗ trợ gọi cho các phương thức HTTP thông dụng. Ví dụ:

export class HttpClient {
    constructor (private handler: HttpHandler) { }

    /* ... Method overloads ... */
    request(first: string | HttpRequest<any>, url?: string, options: {/* ... */}): Observable<any> {
        /* ... */
    }

    /* ... Method overloads ... */
    delete(url: string, options: {/* ... */}): Observable<any> {
        return this.request<any>('DELETE', url, options as any);
    }

    /* ... Method overloads ... */
    get(url: string, options: {/* ... */}): Observable<any> {
        return this.request<any>('GET', url, options as any);
    }

    /* ... Method overloads ... */
    post(url: string, body: any | null, options: {/* ... */}): Observable<any> {
        return this.request<any>('POST', url, addBody(options, body));
    }

    /* ... Method overloads ... */
    put(url: string, body: any | null, options: {/* ... */}): Observable<any> {
        return this.request<any>('PUT', url, addBody(options, body));
    }
}

Hãy chuyển qua editor để theo dõi rõ hơn nhé Tiếp tục, đặt một breakpoint trên dòng 492 và refresh trình duyệt. Phần thú vị nhất sắp bắt đầu!

Tại thời điểm này, chúng tôi không thể step vào this.handler.handle () vì observable chỉ đang được xây dựng và chưa có subcriber. Vì vậy, chúng ta phải tự đặt breakpoint bên trong handle method.

Để làm như thế, hãy chuyển sang editor của bạn và tìm hàm khởi tạo contructor HttpHandler là một DI token ánh xạ đến HttpInterceptingHandler.

Dưới đây là danh sách tất cả các provider:

@NgModule({
    /* ... *
    
    providers: [
        HttpClient,
        { provide: HttpHandler, useClass: HttpInterceptingHandler },
        HttpXhrBackend,
        { provide: HttpBackend, useExisting: HttpXhrBackend },
        BrowserXhr,
        { provide: XhrFactory, useExisting: BrowserXhr },
    ],
})
export class HttpClientModule {
}

Những gì còn lại phải làm là đi vào class HttpInterceptingHandler và thiết lập một breakpoint bên trong handle method.

Sau khi xác định thành công vị trí của nó, chuyển trở lại công cụ dev của bạn, thêm breakpoint của bạn và tiếp tục execution!

BarInterceptor đã được provide vào app.module

Tại đây chúng ta chó thể lấy tất cả các interceptors bằng cách injecting the HTTP_INTERCEPTOR(a multi-provider token) vào trong method.

Bước tiếp theo bao gồm tạo injectors chain. Nhưng trước tiên, chúng ta hãy xem nhanh về httpInterceptorHandler:

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

    handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
        return this.interceptor.intercept(req, this.next);
    }
}

Bên trên tôi có đề cập đến khái niệm chain vậy cùng tìm hiểu xem nó là cái gì nhé

Tôi thì thường nghĩ nó như là một linked list được xây dựng và bắt đầu nó từ tail node.

Để có cái nhìn tổng quan hơn về vấn đề này, tôi khuyên bạn nên tiếp tục thực hiện cho đến khi bạn đạt đến dòng 42, trong khi chú ý đến những gì đang diễn ra trong tab Scrope. Cho ai không xem kĩ hình trên, dòng 42 là dòng này nè return this.chain.handle(req);. Bây giờ, sau khi chain đã được xây dựng, chúng ta có thể đi qua list bắt đầu từ head node bằng cách bước vào hàm xử lý từ dòng 42. Đây là cách biểu diễn linked list nè:

Qua hình ảnh phía trên chúng ta có thể thấy răng mỗi next.handle() return một observable Điều này có nghĩ là sao? Là mọi interceptor có thể thêm những custom behaviorr để return observable. Những thay đổi sẽ được lan truyền trong các precedent interceptors trong chain.

Trước khi tiếp tục, hãy tập trung sự chú ý của chúng ta vào this.backend. Nó đến từ đâu? Nếu bạn nhìn vào contructor, bạn sẽ thấy nó được cung cấp bởi HttpBackend. ánh xạ nào tới httpXhrBackend? (nếu không chắc chắn tại sao, hãy kiểm tra xem mô-đun này cung cấp những gì).

4. Hãy cùng khám phá httpXhrBackend

Đặt một số điểm dừng ở đây và chắc chắn sẽ dẫn đến sự khám phá sẽ tốt hơn! 😃

export class HttpXhrBackend implements HttpBackend {
  constructor(private xhrFactory: XhrFactory) {}

  handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
    // Everything happens on Observable subscription.
    return new Observable((observer: Observer<HttpEvent<any>>) => {
      const xhr = this.xhrFactory.build();
      
        /* ... Setting up the headers ... */
        /* ... Setting up the response type & serializing the body ... */

      // partialFromXhr extracts the HttpHeaderResponse from the current XMLHttpRequest
      // state, and memoizes it into headerResponse.
      const partialFromXhr = (): HttpHeaderResponse => { /* ... */ };

      // First up is the load event, which represents a response being fully available.
      const onLoad = () => { /* ... */ };

      const onError = (error: ProgressEvent) => { /* ... */ };

      xhr.addEventListener('load', onLoad);
      xhr.addEventListener('error', onError);

      // Fire the request, and notify the event stream that it was fired.
      xhr.send(reqBody !);
      observer.next({type: HttpEventType.Sent});

      // This is the return from the Observable function, which is the
      // request cancellation handler.
      return () => {
        xhr.removeEventListener('error', onError);
        xhr.removeEventListener('load', onLoad);
        xhr.abort();
      };
    });
  }
}

Điều đầu tiên nhảy vào mắt là phương thức handle (), đây cũng là phương thức cuối cùng được gọi trong interceptor chain bởi vì nó nằm ở tail node. Nó cũng chịu trách nhiệm gửi dispatching đến backend.

partialFromXhr extract httpHeaderResponse từ XMLHttpRequest hiện tại và ghi nhớ nó; đối tượng này chỉ cần được tính toán một lần có thể được sử dụng ở nhiều nơi. Ví dụ: nó được sử dụng trong các sự kiện onLoad và onError.

  • onLoad - callback function triggered khi response fully available; nó cũng parsesvalidates body của response
const onLoad = () => {
  // Read response state from the memoized partial data.
  let { headers, status, statusText, url } = partialFromXhr();

  // The body will be read out if present.
  let body: any | null = null;

  let ok = status >= 200 && status < 300;

  /* ... Parse body and check its validity ... */

  if (ok) {
      // A successful response is delivered on the event stream.
      observer.next(new HttpResponse({
          body,
          headers,
          status,
          statusText,
          url: url || undefined,
      }));
      // The full body has been received and delivered, no further events
      // are possible. This request is complete.
      observer.complete();
  } else {
      // An unsuccessful request is delivered on the error channel.
      observer.error(new HttpErrorResponse({
          // The error in this case is the response body (error from the server).
          error: body,
          headers,
          status,
          statusText,
          url: url || undefined,
      }));
  }
}
  • onError callback function gọi khi xảy ra lỗi mạng trong khi request
const onError = (error: ProgressEvent) => {
  const {url} = partialFromXhr();
  const res = new HttpErrorResponse({
    error,
    status: xhr.status || 0,
    statusText: xhr.statusText || 'Unknown Error',
    url: url || undefined,
  });
  observer.error(res);
};

Cuối cùng, điều quan trọng là observable được return từ HttpXhrBackend.handle() sẽ dispatch request khi chúng ta subcribe một trong các methor của HttpClient(get, post, v.v). Điều này có nghĩa là HttpXhrBackend.handle () return cold observable có thể được subcribe bằng cách sử dụng concatMap:

this.httpClient.get(url).subscribe() -> of(req).pipe(concatMap(req => this.handler.handle))

Callback sẽ return từ observable

return () => {
  xhr.removeEventListener('error', onError);
  xhr.removeEventListener('load', onLoad);
  xhr.abort();
};

sẽ được invok khi observable stops emitting values. Đó là, khi một error hoặc một complete thông báo xảy ra.

  • onComplete
const obsBE$ = new Observable(obs => {
  timer(1000)
    .subscribe(() => {
      obs.next({ response: { data: ['foo', 'bar'] } });

      // Stop receiving values!
      obs.complete();
    })

    return () => {
      console.warn("I've had enough values!");
    }
});

obsBE$.subscribe(console.log)
/* 
-->
response
I've had enough values!
*/
  • onError
const be$ = new Observable(o => {
  o.next('foo');

  return () => {
    console.warn('NO MORE VALUES!');
  }
});

be$
 .pipe(
    flatMap(v => throwError('foo')),
 )
  .subscribe(null, console.error)
/* 
-->
foo
NO MORE VALUES
*/

5.Vậy làm cách nào để hủy một request

Một trường hợp điển hình đó là khi typing để tìm kiếm một cái gì đó

this.keyPressed
    .pipe(
        debounceTime(300),
        switchMap(v => this.http.get(url + '?key=' + v))
    )

Cách được khuyến khích sẽ thế này. Lý do đó là sự magic của 'switchMap', nó lẽ hủy subcribe inner observable để handle emmited value tiếp theo.

const src = new Observable(obs => {
  obs.next('src 1');
  obs.next('src 2');
  
  setTimeout(() => {
    obs.next('src 3');
    obs.complete(); 
  }, 1000);

  return () => {
    console.log('called on unsubscription')
  };
});

of(1, 2)
  .pipe(
    switchMap(() => src)
  )
  .subscribe(console.log)

/* 
src 1
src 2
called on unsubscription ---> unsubscribed from because the next value(`2`) kicked in
src 1
src 2
src 3
called on unsubscription ---> completion
*/
  • 1 được emitt và trong khi chúng ta đang chờ đợi inner observable complete
  • 2 đến ngay lập tức và sẽ làm cho switchMap unsubscribe từ observable hiện tại mà lần lượt sẽ gọi hàm trả về từ observable.

Đây là những gì đang diễn ra bên trong hàm được trả về từ observable dispatches request (được tìm thấy trong HttpXhrBackend.handle):

return () => {
    /* Skipped some lines for brevity... */

    xhr.removeEventListener('error', onError);
    xhr.removeEventListener('load', onLoad);
    
    // Finally, abort the in-flight request.
    xhr.abort();
}

Do đó, chúng ta có thể suy luận rằng nếu observable yêu cầu unsubcribed, callback ở trên sẽ được gọi.

6. Làm thế nào interceptors có thể retry requests?

Một token interceptor có thể trông như thế này:

intercept (req: HttpRequest<any>, next: HttpHandler) {
  /* ... Attach token and all that good stuff ... */

  return next.handle()
    .pipe(
      catchError(err => {
        if (err instanceof HttpErrorResponse && err.status === 401) {
          return this.handle401Error(req, next)
        }

        // Simply propagate the error to other interceptors or to the consumer
        return throwError(err);
      })
    )
}

private handle401Error (req: HttpRequest<any>, next: HttpHandler) {
  return this.authService.refreshToken()
    .pipe(
      tap(token => this.authService.setToken(token)),
      map(token => this.attachToken(req, token))
      switchMap(req => next.handle(req))
    )
}

private attachToken(req: HttpRequest<any>, token: string): HttpRequest<any> {
  return req.clone({ setHeaders: { 'x-access-token': token } })
}

Logic để retry có thể được thực hiện với switchMap(() => next.handle(req)). Nếu chúng tôi reach code bên trong CatchError, điều đó có nghĩa là consumer sẽ unsubcribe khỏi observable (được trả về từ HttpXhrBackend.handle). Điều này sẽ cho phép chúng tôi re-subscribe observable, điều này sẽ khiến request được gửi lại cũng như các interceptor theo sau interceptor này để chạy lại intercept function của chúng.

Hãy thu hẹp nó xuống một ví dụ đơn giản hơn:

const obsBE$ = new Observable(obs => {
  timer(1000)
    .subscribe(() => {
      // console.log('%c [OBSERVABLE]', 'color: red;');

      obs.next({ response: { data: ['foo', 'bar'] } });

      // Stop receiving values!
      obs.complete();
    })

    return () => {
      console.warn("I've had enough values!");
    }
});

// Composing interceptors the chain
const obsI1$ = obsBE$
  .pipe(
    tap(() => console.log('%c [i1]', 'color: blue;')),
    map(r => ({ ...r, i1: 'intercepted by i1!' }))
  );

let retryCnt = 0;
const obsI2$ = obsI1$
  .pipe(
    tap(() => console.log('%c [i2]', 'color: green;')),
    map(r => { 
      if (++retryCnt <=3) {
        throw new Error('err!') 
      }

      return r;
    }),
    catchError((err, caught) => {
      return getRefreshToken()
        .pipe(
          switchMap(() => /* obsI2$ */caught),
        )
    })
  );

const obsI3$ = obsI2$
  .pipe(
    tap(() => console.log('%c [i3]', 'color: orange;')),
    map(r => ({ ...r, i3: 'intercepted by i3!' }))
  );

function getRefreshToken () {
  return timer(1500)
    .pipe(q
      map(() => ({ token: 'TOKEN HERE' })),
    );
}

function get () {
  return obsI3$
}

get()
  .subscribe(console.log)

/* 
-->
[i1]
[i2]
I've had enough values!
[i1]
[i2]
I've had enough values!
[i1]
[i2]
I've had enough values!
[i1]
[i2]
[i3]
{
  "response": {
    "data": [
      "foo",
      "bar"
    ]
  },
  "i1": "intercepted by i1!",
  "i3": "intercepted by i3!"
}
I've had enough values!
*/

Theo quan điểm của tôi, đây là hiệu ứng của next.handle () bên trong interceptor. Hãy tưởng tượng rằng thay vì const obsI3$ = obsI2$ chúng ta sẽ có một cái gì đó như thế này:

// Interceptor Nr.2
const next = {
  handle(req) {
    /* ... Some logic here ... */

    return of({ response: '' })
  }
}

const obsI3$ = next.handle(req)
  .pipe(
    map(r => ({ ...r, i3: 'this is interceptor 3!!' })),
    /* ... */
  )

obsI3$ bây giờ sẽ là giá trị observable trả về bởi next.handle () có nghĩa là bây giờ nó có thể thêm custom behavior của riêng mình và nếu có sự cố, nó có thể reinvoke observable resouce.

Khi sử dụng interceptor, ban sẽ muốn retry request bằng cách sử dụng switchMap(() => next.handle(req) (như được sử dụng trong đoạn code đầu tiên) bên cạnh các observable được trả lại của interceptor, bạn cũng muốn chạy logic của chúng nằm bên trong intercept() function của chúng.

Từ dòng switchMap(() => /* obsI2$ */caught) chúng ta có thể thấy rằng catchError có thể là một argument thứ hai, caught, đó là resource abservable

7. Tại sao cần phải clone request object bên trong một interceptor

Quá trình thêm JWT token vào request của bạn có thể như sau

if (token) {
  request = request.clone({
    setHeaders: { [this.AuthHeader]: token },
  });
}

return next.handle(request)

Lý do quan trọng nhất sẽ là immutability. Bạn sẽ không muốn thay đổi request object từ nhiều nơi. Vì vậy, mọi interceptot nên configure request một cách độc lập. Cloned request cuối cùng sẽ được chuyển cho interceptor tiếp theo trong chain.

8. Tại sao chỉ nên load httpClientModule một lần trong AppModule hoặc CoreModule?

Một lazy-loaded module A sẽ tạo ra child injector của nó để resolve các provider từ module đó. Điều này có nghĩa là cácproviders provided inside A và những provided by modules imported by A sẽ nằm trong phạm vi của module A. Import HttpClientModule vào A sẽ dẫn đến việc chỉ áp dụng các inpterceptor được provide bên trong A, không bao gồm bất cứ thứ gì có trong injector tree. Điều này là do HttpClientModule đi kèm với các provider riêng của mình, như đã đề cập ở trên, sẽ nằm trong phạm vi A.

             { provide: HttpHandler, useClass: ... }
  AppModule {    /
    imports: [  /
      HttpClientModule
    ]
  }
                  { provide: HttpHandler, useClass: HttpInterceptingHandler } <- where interceptors are gathered
  FeatureModule { /  <- lazy-loaded                  |
    imports: [   /                                   |
      HttpClientModule <------------------           |
    ]                                     |          |
                                          |          |
    declarations: [FeatureComponent]       <------------------------
    providers: [                                     |              |
                                                    /               |
      { provide: HTTP_INTERCEPTORS, useClass: FeatInterceptor_1 },  |
      { provide: HTTP_INTERCEPTORS, useClass: FeatInterceptor_2 }   |
    ]                                      ------------------------>
  }                                       |
                                          | httpClient.get()
  FeatureComponent {                      |
    constructor (private httpClient: HttpClient) { }
  }

Nếu HttpClientModule không được import vào A, nó sẽ tra cứu injector tree cho đến khi tìm thấy các provider cần thiết (trong trường hợp này, nó sẽ ở trong AppModule). Điều này cũng có nghĩa là bất kỳ interceptor được provide nào được cung cấp trong A sẽ bị loại trừ.

9. Làm thế nào interceptors được bỏ qua hoàn toàn?

Hãy chắc chắn rằng HttpHandler ánh xạ tới HttpXhrBackend

@NgModule({
  imports: [
    /* ... */
    HttpClientModule,
    /* ... */
  ],
  declarations: [ /* ... */ ],
  providers: [
    /* ... */
    {
      provide: HttpHandler,
      useExisting: HttpXhrBackend,
    },
    /* ... */
  ]
})
export class AppModule { }

10. Sự khác biệt giữa setHeaders và headers là gì?

setHeaders

req = req.clone({
  setHeaders: { foo: 'bar' },
})

Với setHeaders, chúng ta có thể append các header được provide vào các header hiện có.

headers

req = req.clone({
  setHeaders: { foo: 'bar' },
})

Với headers (một phiên bản của HTTPHeaders), chúng ta có thể ghi đè lên các header hiện có.

// Headers and params may be appended to if `setHeaders` or
// `setParams` are used.
let headers = update.headers || this.headers;
let params = update.params || this.params;

// Check whether the caller has asked to add headers.
if (update.setHeaders !== undefined) {
  // Set every requested header.
  headers =
      Object.keys(update.setHeaders)
          .reduce((headers, name) => headers.set(name, update.setHeaders ![name]), headers);
}

Lưu ý: Điều tương tự cũng xảy ra với setParams & params

Kết

Trên đây là một số tìm hiểu của mình về HttpClientModule, tuy còn một số điểm hay ho và thú vị nữa những mình xin phép chia sẻ ở những nội dung sau, cảm ơn bạn đã theo dõi, hy vọng những thứ trên giúp ích cho bạn trong các dự án.

Tham khảo https://angular.io/api/common/http/HttpClientModule https://indepth.dev/exploring-the-httpclientmodule-in-angular/#how-can-interceptors-retry-requests


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí