探索 Angular 中的 HttpClientModule

2025-06-04

探索 Angular 中的 HttpClientModule

在这篇文章中,我们将了解HttpClientModule幕后实际工作原理,并找到使用该模块时可能出现的一些问题的答案。

注意:本文基于Angular 8.2.x

内容


设置

我最喜欢的了解事物实际工作原理的方式是使用调试器,同时在文本编辑器中保存源代码,这样我就可以轻松地探索和做出假设。

在你的机器上安装 Angular

git clone -b 8.2.x --single-branch https://github.com/angular/angular.git
Enter fullscreen mode Exit fullscreen mode

StackBlitz

您可以在此处找到 StackBlitz 演示

我们将在整篇文章中使用它,以便更好地理解实体如何相互连接。


什么是 HttpClientModule?

HttpClientModule是 Angular 提供的一个服务模块,它允许我们执行HTTP 请求并轻松操作这些请求及其响应。它之所以被称为服务模块,是因为它仅实例化服务,并且不导出任何组件、指令或管道。

返回内容


让我们开始探索🚧

进入 StackBlitz 项目后:

  • 打开开发工具

  • 转到token.interceptor.ts(CTRL + P)并在旁边放置一个断点console.warn()

  • 刷新StackBlitz 浏览器

现在,您应该看到类似这样的内容:

通过单击中的匿名函数client.ts,您现在位于HttpClient类中,这是您通常在服务中注入的类。

正如您可能预料的那样,此类包含众所周知的 HTTP 动词的方法。

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

我建议您切换到文本编辑器并开始HttpClient.request稍微探索一下这种方法。

继续,在 处设置一个断点line 492并刷新浏览器。最有趣的部分即将开始!

此时,我们无法进入,this.handler.handle()因为可观察对象刚刚构建,还没有订阅者。因此,我们必须在handle方法内部手动设置断点。

为此,请切换到文本编辑器并向上滚动到constructor
HttpHandler是一个映射到 的DI 令牌HttpInterceptingHandler

以下是所有提供商的列表

@NgModule({
    /* ... */

    providers: [
        HttpClient,
        { provide: HttpHandler, useClass: HttpInterceptingHandler },
        HttpXhrBackend,
        { provide: HttpBackend, useExisting: HttpXhrBackend },
        BrowserXhr,
        { provide: XhrFactory, useExisting: BrowserXhr },
    ],
})
export class HttpClientModule {
}
Enter fullscreen mode Exit fullscreen mode

剩下要做的就是进入HttpInterceptingHandler课堂并在方法内设置断点handle

成功识别其位置后,切换回您的开发工具,添加断点并恢复执行!

BarInterceptor提供于app.module

在这里,我们能够通过在我们的方法中注入HTTP_INTERCEPTOR多提供者令牌)来获取所有的拦截器。

下一步是创建注入器链

首先,我们来快速看一下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);
    }
}
Enter fullscreen mode Exit fullscreen mode

我喜欢将这个链想象成从尾节点开始构建的链接列表

为了更好地了解这一点,我建议您继续执行直到到达line 42,同时注意选项卡中发生的情况Scope

head node现在,链构建完成后,我们可以通过从进入handle函数来遍历从开始的列表line 42

这个链表看起来可能是这样的:

从上图可以看出,每个拦截器next.handle() 都会返回一个可观察对象
这意味着每个拦截器都可以为返回的可观察对象添加自定义行为。这些更改传递链中前面的拦截器。

在继续讨论之前,让我们先把注意力集中在 上this.backend。它是从哪里来的?如果你看一下构造函数,你应该会看到 是由 提供的HttpBackend,它映射到HttpXhrBackend(如果不确定原因,请检查该模块提供了什么)。

让我们探索一下HttpXhrBackend

在这里和那里设置一些断点肯定会带来更好的理解!:)

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();
      };
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

首先映入眼帘的是该方法,它也是拦截器链handle()中最后一个被调用的方法,因为它位于节点。它还负责将请求分发到后端。

  • partialFromXhrHttpHeaderResponse-从当前中提取XMLHttpRequest并记录;此对象只需计算一次,可在多个地方使用。例如,它用于onLoadonError事件

  • onLoad-当响应完全可用时触发的回调函数;它还解析验证响应主体

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,
      }));
  }
}
Enter fullscreen mode Exit fullscreen mode
  • onError-请求过程中发生网络错误时调用的回调函数
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);
};
Enter fullscreen mode Exit fullscreen mode

最后,值得一提的是,当我们订阅 的某个方法(等)时, from 返回的可观察对象HttpXhrBackend.handle()将会分派请求。这意味着返回一个冷可观察对象,可以使用 进行订阅HttpClientgetpostHttpXhrBackend.handle()concatMap

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

从可观察对象返回的回调

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

可观察对象停止发出值时(即发生错误完成通知时),将会被调用。

完成

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!
*/
Enter fullscreen mode Exit fullscreen mode

错误

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
*/
Enter fullscreen mode Exit fullscreen mode

返回内容


如何取消请求?

一个常见的情况是typeahead功能:

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

建议这样做。这是因为 的switchMap魔力,它会取消订阅内部的可观察对象,以处理下一个发出的值。

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
*/
Enter fullscreen mode Exit fullscreen mode

1被发出,当我们等待内部可观察对象完成时,另一个值2立即出现,并将取消switchMap订阅当前内部可观察对象,这反过来将调用可观察对象返回的函数。

以下是从调度请求的可观察对象返回的函数内部发生的情况(位于HttpXhrBackend.handle中):

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

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

    // Finally, abort the in-flight request.
    xhr.abort();
}
Enter fullscreen mode Exit fullscreen mode

因此,我们可以推断,如果发出请求的可观察对象被取消订阅,则将调用上述回调。

返回内容


拦截器如何重试请求?

令牌拦截器可能看起来像这样:

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 } })
}
Enter fullscreen mode Exit fullscreen mode

重试逻辑可以通过 实现switchMap(() => next.handle(req))

如果我们到达 里面的代码catchError,这意味着消费者取消订阅可观察对象(从HttpXhrBackend.handle返回的那个)。
这将允许我们重新订阅该可观察对象,从而导致请求再次被发送,并且跟随此拦截器的拦截器将intercept再次运行它们的函数。

让我们将其缩小到一个更简单的例子:

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!
*/
Enter fullscreen mode Exit fullscreen mode

StackBlitz

在我看来,这就是每个拦截器内部的效果next.handle()(图片见此处)。想象一下,如果const obsI3$ = obsI2$我们没有这样做,那么我们将会得到如下效果:

// 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!!' })),
    /* ... */
  )
Enter fullscreen mode Exit fullscreen mode

obsI3$现在将成为可观察对象,next.handle()这意味着它现在可以添加自己的自定义行为,并且如果出现问题,它可以重新调用源可观察对象。

当使用拦截器时,您可能希望通过使用重试请求switchMap(() => next.handle(req)(就像在第一个代码片段中所做的那样),因为除了每个拦截器返回的可观察对象之外,您还希望运行位于其intercept()函数内部的逻辑。

从这一行switchMap(() => /* obsI2$ */caught)我们可以看到catchError可以有第二个参数,caught即源可观察变量。(更多信息请见此处)。

返回内容


为什么有时需要在拦截器内部克隆请求对象?

将 JWT 令牌添加到请求的过程可能如下所示:

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

return next.handle(request)
Enter fullscreen mode Exit fullscreen mode

最重要的原因在于不可变性。你肯定不想在多个地方修改请求对象。因此,每个拦截器都应该独立配置请求。
克隆的请求最终会被传递给链中的下一个拦截器。

返回内容


为什么建议在 AppModule 或 CoreModule 中只加载一次 HttpClientModule?

惰性加载的模块A会创建自己的子注入器,该注入器会解析providers该模块中的 。这意味着模块内部提供的服务提供者 A,以及模块导入的模块提供的 A服务提供者,其作用域都将限定在 模块 内A

导入HttpClientModuleA导致仅应用内部提供的拦截而排除注入器树中向上层的所有拦截器。这是因为comes 带有自己的提供程序,如上所述,这些提供程序的作用域将限定在 内AHttpClientModuleA

             { 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) { }
  }
Enter fullscreen mode Exit fullscreen mode

如果HttpClientModule在 中导入A,它将查找注入器树直到找到所需的提供程序(在本例中,它位于 中AppModule)。这也意味着 中提供的任何拦截器A都将被排除

返回内容


如何才能彻底绕过拦截器?

TLDR;

确保HttpHandler映射到HttpXhrBackend

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

详细说明

建议首先探索HttpClientModule

无论何时执行类似HttpClient.get()(或任何其他HTTP 动词)的操作,该HttpClient.request()方法最终都会被调用。在此方法中,将到达以下代码行:

const events$: Observable<HttpEvent<any>> =
        of (req).pipe(concatMap((req: HttpRequest<any>) => this.handler.handle(req)));
Enter fullscreen mode Exit fullscreen mode

让我们看看如何this.handler检索:

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

  /* ... */
}
Enter fullscreen mode Exit fullscreen mode

如果我们看HttpClientModule一下 的提供商,

@NgModule({
    /* ... */

    providers: [
        HttpClient,
        { provide: HttpHandler, useClass: HttpInterceptingHandler },
        HttpXhrBackend,
        { provide: HttpBackend, useExisting: HttpXhrBackend },
        BrowserXhr,
        { provide: XhrFactory, useExisting: BrowserXhr },
    ],
})
export class HttpClientModule {
}
Enter fullscreen mode Exit fullscreen mode

我们可以知道HttpHandler映射到HttpInterceptingHandler

@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);
  }
}
Enter fullscreen mode Exit fullscreen mode

HttpInterceptingHandler构建拦截器链,最终让我们将所有注册的拦截器应用于请求。

我们还可以看到HttpInterceptingHandler 实现 HttpHandler

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

HttpHandler由实现HttpBackend

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

HttpBackend由 实现HttpXhrBackend,最终将请求发送到服务器(有关更多信息,请点击此处)。

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

  handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
    /* ... */
  }
}
Enter fullscreen mode Exit fullscreen mode

如您所见,和都HttpInterceptingHandler必须HttpXhrBackend实现方法handle()
因此,解决这个问题的方法是将HttpHandlermap 设置为HttpXhrBackend

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

返回内容


setHeaders有什么区别headers

setHeaders

req = req.clone({
  setHeaders: { foo: 'bar' },
})
Enter fullscreen mode Exit fullscreen mode

使用setHeaders,我们可以将提供的标题附加到现有的标题中。

headers

req = req.clone({
  setHeaders: { foo: 'bar' },
})
Enter fullscreen mode Exit fullscreen mode

使用headers(的实例HttpHeaders),我们可以覆盖现有的标题

以下是摘录自以下来源:

// 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);
}
Enter fullscreen mode Exit fullscreen mode

setParams注意: & params;也同样如此

返回内容


背后有何魔力HttpHeaders

HttpHeaders是一个允许我们操作(执行CRUD 操作)请求标头的类。

看一下这个例子:

const headers = new HttpHeaders({
  foo: 'foo',
  bar: 'bar',
});

const newHeaders = headers
  .append('name', 'andrei')
  .set('city', 'tgv')
  .delete('foo')
  .has('abcd');
Enter fullscreen mode Exit fullscreen mode

它的神奇之处在于,它只会在真正需要的时候才初始化标头(键值对)。也就是说,当你想查询它们的当前状态(HttpHeaders.forEach()等等HttpHeaders.get()……)时。

构造函数如下所示:

constructor(headers?: string|{[name: string]: string | string[]}) {
  if (!headers) {
    this.headers = new Map<string, string[]>();
  } else if (typeof headers === 'string') {
    this.lazyInit = () => { /* ... */ }
  } else {
    this.lazyInit = () => { /* ... */ }
  }
}
Enter fullscreen mode Exit fullscreen mode

如我们所见,该lazyInit函数在 的构造函数中初始化HttpHeaders

因此,为了执行诸如HttpHeaders.appendHttpHeaders.set或 之类的操作HttpHeaders.delete,这些操作最终会改变构造函数提供的初始状态,将会创建一个克隆来存储新的操作(-> -> -> )。 这些存储的操作最终初始状态合并。createsetupdateappenddeletedelete

HttpHeaders.clone看起来是这样的:

// action
interface Update {
  name: string;
  value?: string|string[];
  op: 'a'|'s'|'d';
}

private clone(update: Update): HttpHeaders {
  const clone = new HttpHeaders();
  // Preserve the initialization across multiple clones
  clone.lazyInit =
      (!!this.lazyInit && this.lazyInit instanceof HttpHeaders) ? this.lazyInit : this;
  // Accumulate actions 
  clone.lazyUpdate = (this.lazyUpdate || []).concat([update]);
  return clone;
}
Enter fullscreen mode Exit fullscreen mode

让我们通过使用最初的例子来理解这个逻辑:

const headers = new HttpHeaders({
  foo: 'foo',
  bar: 'bar',
});
/* 
-->
h1.lazyInit = () => {
  // Initialize headers
}
*/

const newHeaders = headers
  .append('name', 'andrei')
  /* 
  -->
  // Creating a clone
  h2.lazyInit = h1 // Preserving the first `instance` across multiple clones
  h2.lazyUpdate = { "name": "name", "value": "andrei", "op": "a" }
  */
  .set('city', 'tgv')
  /* 
  -->
  // Creating a clone
  // h2.lazyInit = h1
  h3.lazyInit = h2.lazyInit // Preserving the first `instance` across multiple clones
  h3.lazyUpdate = [
    { "name": "name", "value": "andrei", "op": "a" }, // append
    { "name": "city", "value": "tgv", "op": "s" } // set
  ]
  */
  .delete('foo')
  /* 
  -->
  // Creating a clone
  // h3.lazyInit = h2.lazyInit
  h4.lazyInit = h3.lazyInit // Preserving the first `instance` across multiple clones
  h4.lazyUpdate = [
    { "name": "name", "value": "andrei", "op": "a" },
    { "name": "city", "value": "tgv", "op": "s" },
    { "name": "foo", "op": "d" }
  ]
  */
  .has('abcd');
  /* 
  -->
  Here is where the initialization takes place
  */
Enter fullscreen mode Exit fullscreen mode

合并过程如下

private init(): void {
  if (!!this.lazyInit) {
    if (this.lazyInit instanceof HttpHeaders) {
      this.copyFrom(this.lazyInit);
    } else {
      this.lazyInit();
    }
    this.lazyInit = null;
    if (!!this.lazyUpdate) {
      this.lazyUpdate.forEach(update => this.applyUpdate(update));
      this.lazyUpdate = null;
    }
  }
}

private copyFrom(other: HttpHeaders) {
  other.init();
  Array.from(other.headers.keys()).forEach(key => {
    this.headers.set(key, other.headers.get(key) !);
    this.normalizedNames.set(key, other.normalizedNames.get(key) !);
  });
}
Enter fullscreen mode Exit fullscreen mode

HttpHeaders.init()查询标头状态时调用该方法(通过使用诸如HttpHeaders.get()、之类的方法HttpHeaders.has()

在 中HttpHeaders.copyFrom()other将是 的第一个实例HttpHeaders,其中包含初始化逻辑:lazyInit。调用other.init()最终会到达 的这个部分HttpHeaders.init()this.lazyInit();。在这里将初始状态创建到第一个实例中。

接下来我们还剩下两件事要做:

1)将第一个实例的状态复制到当前实例(最后一个克隆)中;这是通过以下几行实现的HttpHeaders.copyFrom()

Array.from(other.headers.keys()).forEach(key => {
  this.headers.set(key, other.headers.get(key) !);
  this.normalizedNames.set(key, other.normalizedNames.get(key) !);
});
Enter fullscreen mode Exit fullscreen mode

2)将收集到的动作应用到复制的状态上:

// HttpHeaders.init()
if (!!this.lazyUpdate) {
  this.lazyUpdate.forEach(update => this.applyUpdate(update));
  this.lazyUpdate = null;
}
Enter fullscreen mode Exit fullscreen mode

返回内容


怎么样HttpClientJsonpModule

JSONP是解决众所周知的CORS问题的一种方法。它通过将资源视为script文件来实现这一点。

当我们请求带有标签的资源时,我们script可以传递一个定义的回调,资源最终将在其中包装 json 响应。回调加载时被调用script

该模块提供了一种使用JSONP 的方法,无需过多担心上述细节。

让我们快速探索一下,看看它为什么这么棒!

@NgModule({
  providers: [
    JsonpClientBackend,
    {provide: JsonpCallbackContext, useFactory: jsonpCallbackContext},
    {provide: HTTP_INTERCEPTORS, useClass: JsonpInterceptor, multi: true},
  ],
})
export class HttpClientJsonpModule {
}
Enter fullscreen mode Exit fullscreen mode

JsonpCallbackContext映射到jsonpCallbackContext哪个对象,将返回窗口对象空对象(用于测试环境)。返回的对象用于存储最终将被脚本调用的回调函数

它还提供了一个拦截器,即JsonpInterceptor。这个拦截器的作用是确保当请求方法为时,我们的请求永远不会到达HttpBackendJSONP (它将包含完全不同的逻辑) 。

@Injectable()
export class JsonpInterceptor {
  constructor(private jsonp: JsonpClientBackend) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (req.method === 'JSONP') {
      return this.jsonp.handle(req as HttpRequest<never>);
    }

    // Fall through for normal HTTP requests.
    return next.handle(req); // Next interceptor in the chain
  }
}
Enter fullscreen mode Exit fullscreen mode

JsonpClientBackend就是神奇的地方。它会自动生成一个回调函数,稍后会被脚本调用。具体方法是用新生成的回调函数名称替换JSONP_CALLBACKurl 中的参数

export class JsonpClientBackend implements HttpBackend {
  private nextCallback(): string { return `ng_jsonp_callback_${nextRequestId++}`; }

  /* ... */

  handle (/* ... */) {
    return new Observable<HttpEvent<any>>((observer: Observer<HttpEvent<any>>) => {
      /* ... */
      const callback = this.nextCallback();
      const url = req.urlWithParams.replace(/=JSONP_CALLBACK(&|$)/, `=${callback}$1`);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

然后,它将回调函数存储在jsonpCallbackContext使用生成的回调名称返回的对象中

this.callbackMap[callback] = (data?: any) => {
  delete this.callbackMap[callback];

  if (cancelled) {
    return;
  }

  body = data;
  finished = true;
};
Enter fullscreen mode Exit fullscreen mode

需要再次注意的是,上述回调函数应该在脚本下载完成之前调用。这样,如果我们决定自己提供回调函数名称,就可以确定提供的回调函数是否被调用。脚本加载完成
后,回调函数会执行此操作:

// Inside `JsonpClientBackend.handle`
const onLoad = (event: Event) => {
    // Maybe due to `switchMap`
    if (cancelled) {
      return;
    }

    cleanup();

    // Was the callback called with the response?
    if (!finished) {
      // If not, send the error response to the stream

      return;
    }

    // If yes, sent the response to the stream - everything was successful
}
Enter fullscreen mode Exit fullscreen mode

返回内容


结论

我希望探索这个模块是一次有趣的冒险!

感谢阅读!

照片由 Anton Repponen 在 Unsplash 上拍摄

特别感谢@wassimchegham和@pietrucha

文章来源:https://dev.to/angular/exploring-the-httpclientmodule-in-angular-1ogm
PREV
在 Angular 中使用结构指令处理可观察对象
NEXT
Angular 独立组件的组件优先状态管理