探索 Angular 中的 HttpClientModule
在这篇文章中,我们将了解HttpClientModule
幕后实际工作原理,并找到使用该模块时可能出现的一些问题的答案。
注意:本文基于Angular 8.2.x。
内容
- 设置
- 什么是 HttpClientModule?
- 让我们开始探索🚧
- 如何取消请求?
- 拦截器如何重试请求?
- 为什么有时需要在拦截器内部克隆请求对象?
- 为什么建议在 AppModule 或 CoreModule 中只加载一次 HttpClientModule?
- 如何才能彻底绕过拦截器?
- setHeaders 和 headers 有什么区别?
- HttpHeaders 背后有什么魔力?
- HttpClientJsonpModule 怎么样?
- 结论
设置
我最喜欢的了解事物实际工作原理的方式是使用调试器,同时在文本编辑器中保存源代码,这样我就可以轻松地探索和做出假设。
在你的机器上安装 Angular
git clone -b 8.2.x --single-branch https://github.com/angular/angular.git
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));
}
}
我建议您切换到文本编辑器并开始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 {
}
剩下要做的就是进入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);
}
}
我喜欢将这个链想象成从尾节点开始构建的链接列表。
为了更好地了解这一点,我建议您继续执行直到到达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();
};
});
}
}
首先映入眼帘的是该方法,它也是拦截器链handle()
中最后一个被调用的方法,因为它位于尾节点。它还负责将请求分发到后端。
-
partialFromXhr
HttpHeaderResponse
-从当前中提取XMLHttpRequest
并记录;此对象只需计算一次,可在多个地方使用。例如,它用于onLoad
和onError
事件 -
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,
}));
}
}
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);
};
最后,值得一提的是,当我们订阅 的某个方法(等)时, from 返回的可观察对象HttpXhrBackend.handle()
将会分派请求。这意味着返回一个冷可观察对象,可以使用 进行订阅:HttpClient
get
post
HttpXhrBackend.handle()
concatMap
this.httpClient.get(url).subscribe() -> of(req).pipe(concatMap(req => this.handler.handle))
从可观察对象返回的回调
return () => {
xhr.removeEventListener('error', onError);
xhr.removeEventListener('load', onLoad);
xhr.abort();
};
当可观察对象停止发出值时(即发生错误或完成通知时),将会被调用。
完成
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!
*/
错误
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
*/
返回内容
如何取消请求?
一个常见的情况是typeahead功能:
this.keyPressed
.pipe(
debounceTime(300),
switchMap(v => this.http.get(url + '?key=' + v))
)
建议这样做。这是因为 的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
*/
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();
}
因此,我们可以推断,如果发出请求的可观察对象被取消订阅,则将调用上述回调。
返回内容
拦截器如何重试请求?
令牌拦截器可能看起来像这样:
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 } })
}
重试逻辑可以通过 实现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!
*/
在我看来,这就是每个拦截器内部的效果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!!' })),
/* ... */
)
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)
最重要的原因在于不可变性。你肯定不想在多个地方修改请求对象。因此,每个拦截器都应该独立配置请求。
克隆的请求最终会被传递给链中的下一个拦截器。
返回内容
为什么建议在 AppModule 或 CoreModule 中只加载一次 HttpClientModule?
惰性加载的模块A
会创建自己的子注入器,该注入器会解析providers
该模块中的 。这意味着模块内部提供的服务提供者 A
,以及模块导入的模块提供的 A
服务提供者,其作用域都将限定在 模块 内A
。
导入HttpClientModule
将A
导致仅应用内部提供的拦截器,而排除注入器树中向上层的所有拦截器。这是因为comes 带有自己的提供程序,如上所述,这些提供程序的作用域将限定在 内。A
HttpClientModule
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) { }
}
如果HttpClientModule
未在 中导入A
,它将查找注入器树,直到找到所需的提供程序(在本例中,它位于 中AppModule
)。这也意味着 中提供的任何拦截器A
都将被排除。
返回内容
如何才能彻底绕过拦截器?
TLDR;
确保HttpHandler
映射到HttpXhrBackend
:
@NgModule({
imports: [
/* ... */
HttpClientModule,
/* ... */
],
declarations: [ /* ... */ ],
providers: [
/* ... */
{
provide: HttpHandler,
useExisting: HttpXhrBackend,
},
/* ... */
]
})
export class AppModule { }
详细说明
建议首先探索HttpClientModule。
无论何时执行类似HttpClient.get()
(或任何其他HTTP 动词)的操作,该HttpClient.request()
方法最终都会被调用。在此方法中,将到达以下代码行:
const events$: Observable<HttpEvent<any>> =
of (req).pipe(concatMap((req: HttpRequest<any>) => this.handler.handle(req)));
让我们看看如何this.handler
检索:
@Injectable()
export class HttpClient {
constructor(private handler: HttpHandler) {}
/* ... */
}
如果我们看HttpClientModule
一下 的提供商,
@NgModule({
/* ... */
providers: [
HttpClient,
{ provide: HttpHandler, useClass: HttpInterceptingHandler },
HttpXhrBackend,
{ provide: HttpBackend, useExisting: HttpXhrBackend },
BrowserXhr,
{ provide: XhrFactory, useExisting: BrowserXhr },
],
})
export class HttpClientModule {
}
我们可以知道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);
}
}
HttpInterceptingHandler
构建拦截器链,最终让我们将所有注册的拦截器应用于请求。
我们还可以看到HttpInterceptingHandler
实现 HttpHandler
:
export abstract class HttpHandler {
abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}
HttpHandler
由实现HttpBackend
export abstract class HttpBackend implements HttpHandler {
abstract handle(req: HttpRequest<any>): Observable<HttpEvent<any>>;
}
HttpBackend
由 实现HttpXhrBackend
,最终将请求发送到服务器(有关更多信息,请点击此处)。
@Injectable()
export class HttpXhrBackend implements HttpBackend {
constructor(private xhrFactory: XhrFactory) {}
handle(req: HttpRequest<any>): Observable<HttpEvent<any>> {
/* ... */
}
}
如您所见,和都HttpInterceptingHandler
必须HttpXhrBackend
实现该方法handle()
。
因此,解决这个问题的方法是将HttpHandler
map 设置为HttpXhrBackend
。
@NgModule({
imports: [
/* ... */
HttpClientModule,
/* ... */
],
declarations: [ /* ... */ ],
providers: [
/* ... */
{
provide: HttpHandler,
useExisting: HttpXhrBackend,
},
/* ... */
]
})
export class AppModule { }
返回内容
setHeaders
和有什么区别headers
?
setHeaders
req = req.clone({
setHeaders: { foo: 'bar' },
})
使用setHeaders
,我们可以将提供的标题附加到现有的标题中。
headers
req = req.clone({
setHeaders: { foo: 'bar' },
})
使用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);
}
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');
它的神奇之处在于,它只会在真正需要的时候才初始化标头(键值对)。也就是说,当你想查询它们的当前状态(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 = () => { /* ... */ }
}
}
如我们所见,该lazyInit
函数在 的构造函数中初始化HttpHeaders
。
因此,为了执行诸如HttpHeaders.append
、HttpHeaders.set
或 之类的操作HttpHeaders.delete
,这些操作最终会改变构造函数提供的初始状态,将会创建一个克隆来存储新的操作(-> 、-> 、-> )。 这些存储的操作最终将与初始状态合并。create
set
update
append
delete
delete
它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;
}
让我们通过使用最初的例子来理解这个逻辑:
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
*/
合并过程如下:
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) !);
});
}
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) !);
});
2)将收集到的动作应用到复制的状态上:
// HttpHeaders.init()
if (!!this.lazyUpdate) {
this.lazyUpdate.forEach(update => this.applyUpdate(update));
this.lazyUpdate = null;
}
返回内容
怎么样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 {
}
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
}
}
JsonpClientBackend就是神奇的地方。它会自动生成一个回调函数,稍后会被脚本调用。具体方法是用新生成的回调函数名称替换JSONP_CALLBACK
url 中的参数值。
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`);
});
}
}
然后,它将回调函数存储在jsonpCallbackContext
使用生成的回调名称返回的对象中。
this.callbackMap[callback] = (data?: any) => {
delete this.callbackMap[callback];
if (cancelled) {
return;
}
body = data;
finished = true;
};
需要再次注意的是,上述回调函数应该在脚本下载完成之前调用。这样,如果我们决定自己提供回调函数名称,就可以确定提供的回调函数是否被调用。脚本加载完成
后,回调函数会执行此操作:
// 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
}
返回内容
结论
我希望探索这个模块是一次有趣的冒险!
感谢阅读!
照片由 Anton Repponen 在 Unsplash 上拍摄。
特别感谢@wassimchegham和@pietrucha。
文章来源:https://dev.to/angular/exploring-the-httpclientmodule-in-angular-1ogm