JSONP(JSON with Padding)是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。基于XMLHttpRequest的数据请求会受到同源策略限制,而 JSONP 以 <script> 标签的形式实现会被浏览器判定为静态资源的请求加载,从而跳过同源策略的限制。
1、Angular JSONP 的使用方法
在 Angular 项目中,使用 JSONP 实现跨域数据访问,我们需要引入 HttpClientModule 和 HttpClientJsonpModule 模块,一般都是在根模块 AppModule 中进行导入。
import { HttpClientJsonpModule, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule, HttpClientJsonpModule, AppRoutingModule],
bootstrap: [AppComponent]
})
export class AppModule { }
模块导入后,就可以在组件中通过 HttpClient 发起请求了:
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-main',
templateUrl: './main.component.html',
styleUrls: ['./main.component.css']
})
export class MainComponent implements OnInit {
constructor(private http: HttpClient) { }
ngOnInit(): void {
this.jsonp();
}
jsonp() {
let term = `王力宏`;
let url = `https://itunes.apple.com/search?term=${term}&media=music&limit=10`;
this.http.jsonp<any>(url, "callback").subscribe(data => {
console.log(data.results.map((d: any) => d.trackName));
});
}
}
这里演示了通过 JSONP 请求 itunes 以关键字 term 查询歌曲信息的数据,结果只打印输入歌曲的名字。调试运行打印如图:
2、jsonp方法简介
查看jsonp方法的声明:
/**
* Constructs a `JSONP` request for the given URL and name of the callback parameter.
*
* @param url The resource URL.
* @param callbackParam The callback function name.
*
* You must install a suitable interceptor, such as one provided by `HttpClientJsonpModule`.
* If no such interceptor is reached,
* then the `JSONP` request can be rejected by the configured backend.
*
* @return An `Observable` of the response object, with response body in the requested type.
*/
jsonp<T>(url: string, callbackParam: string): Observable<T>;
注释说明第一个参数 url 指请求的地址,第二个参数 callbackParam 指的是服务端指定的用于传递回调方法名称的参数名称。以上诉代码为例 callbackParam 的值就是 "callback" ,不同的服务端可以约定自己的传递回调方法名的参数名。
查看方法的源代码:
jsonp(url, callbackParam) {
return this.request('JSONP', url, {
params: new HttpParams().append(callbackParam, 'JSONP_CALLBACK'),
observe: 'body',
responseType: 'json',
});
}
可以发现,jsonp 方法只是把指定回调方法的参数作为额外参数添加到了请求的参数字典中,以上述示例的请求来讲相当于 url 由
https://itunes.apple.com/search?term=王力宏&media=music&limit=10 变成了
https://itunes.apple.com/search?term=王力宏&media=music&limit=10&callback=JSONP_CALLBACK
然后再调用 HttpCliet.request() 方法进行请求,HttpCliet.request() 的调用流程,在【Angular中的HTTP请求】- HttpClient 详解 中已作说明,这里不再详细介绍。
3、jsonp的独立流程
还是有必要看一下 HttpCliet.request() 中 HttpHandler 的处理流程:
请求在经过拦截器的处理后最后交由 httpBackend 处理。而在示例的项目中,AppModule 中并没有添加拦截器的 provider。
结合之前关于拦截器的介绍,我们知道 HttpClientModule 自带一个默认的拦截器 HttpXsrfInterceptor,这个拦截器是向符合条件的请求添加XSRF令牌的,对JSONP 请求方法没有影响。
查看代码可以发现 HttpClientJsonpModule 也自带了一个拦截器 JsonpInterceptor :
查看 JsonpInterceptor 的源代码:
class JsonpInterceptor {
constructor(jsonp) {
this.jsonp = jsonp;
}
/**
* Identifies and handles a given JSONP request.
* @param req The outgoing request object to handle.
* @param next The next interceptor in the chain, or the backend
* if no interceptors remain in the chain.
* @returns An observable of the event stream.
*/
intercept(req, next) {
if (req.method === 'JSONP') {
return this.jsonp.handle(req);
}
// Fall through for normal HTTP requests.
return next.handle(req);
}
}
JsonpInterceptor.decorators = [
{ type: Injectable }
];
JsonpInterceptor.ctorParameters = () => [
{ type: JsonpClientBackend }
];
可以发现,对于 JSONP 请求,拦截器直接将请求交给 JsonpClientBackend (this.jsonp) 处理了,而且不再调用 next.hanle() 方法传递给后面的拦截器。
所以在Angular项目中对于全部的 JSONP 请求,能够起作用的就只有两个默认的拦截器,其他手动添加的拦截器都不会起作用。
4、JsonpClientBackend 详解
查看 JsonpClientBackend 的声明:
export declare class JsonpClientBackend implements HttpBackend {
private callbackMap;
private document;
/**
* A resolved promise that can be used to schedule microtasks in the event handlers.
*/
private readonly resolvedPromise;
constructor(callbackMap: ɵangular_packages_common_http_http_b, document: any);
/**
* Get the name of the next callback method, by incrementing the global `nextRequestId`.
*/
private nextCallback;
/**
* Processes a JSONP request and returns an event stream of the results.
* @param req The request object.
* @returns An observable of the response events.
*
*/
handle(req: HttpRequest<never>): Observable<HttpEvent<any>>;
}
查看代码可以知道构造方法中 ɵangular_packages_common_http_http_b 其实就是 JsonpCallbackContext。
export { ..., NoopInterceptor as ɵangular_packages_common_http_http_a, JsonpCallbackContext as ɵangular_packages_common_http_http_b, ... };
从 HttpClientJsonpModule 的providers中可以发现 JsonpCallbackContext 实际使用的是 jsonpCallbackContext。
{ provide: JsonpCallbackContext, useFactory: jsonpCallbackContext },
再查看其源代码,发现 jsonpCallbackContext 就是 window 对象:
function jsonpCallbackContext() {
if (typeof window === 'object') {
return window;
}
return {};
}
再看JsonpClientBackend.handle() 方法源代码:
handle(req) {
if (req.method !== 'JSONP') {
throw new Error(JSONP_ERR_WRONG_METHOD);
}
else if (req.responseType !== 'json') {
throw new Error(JSONP_ERR_WRONG_RESPONSE_TYPE);
}
return new Observable((observer) => {
const callback = this.nextCallback(); //生成唯一的回调方法名称
// 首次调用生成的回调方法名称为 ng_jsonp_callback_0
const url = req.urlWithParams.replace(/=JSONP_CALLBACK(&|$)/, `=${callback}$1`);
//这一句替换请求url中的回调方法名称,以上面的例子来说
// 替换前:https://itunes.apple.com/search?term=王力宏&media=music&limit=10&callback=JSONP_CALLBACK
// 替换后:https://itunes.apple.com/search?term=王力宏&media=music&limit=10&callback=ng_jsonp_callback_0
const node = this.document.createElement('script'); //创建script标签
node.src = url; //指定url
let body = null;
......
// 指定回调方法
// 按上述回到方法名称 相当于 window["ng_jsonp_callback_0"] = (data) => { ... };
this.callbackMap[callback] = (data) => {
delete this.callbackMap[callback]; //回调方法调用后删除方法 delete window["ng_jsonp_callback_0"]
...
body = data; //将回调方法接收到的数据作为body通过HTTPResponse返回
finished = true;
};
const cleanup = () => { ... };
const onLoad = (event) => {
......
this.resolvedPromise.then(() => {
......
if (!finished) {
observer.error(new HttpErrorResponse({
url,
status: 0,
statusText: 'JSONP Error',
error: new Error(JSONP_ERR_NO_CALLBACK),
}));
return;
}
observer.next(new HttpResponse({
body,
status: 200 /* Ok */,
statusText: 'OK',
url,
}));
observer.complete();
});
};
const onError = (error) => { ...... };
node.addEventListener('load', onLoad);
node.addEventListener('error', onError);
this.document.body.appendChild(node); //将 script 标签添加到页面上
observer.next({ type: HttpEventType.Sent });
// Cancellation handler. 指定取消订阅时的处理逻辑
return () => {
// Track the cancellation so event listeners won't do anything even if already scheduled.
cancelled = true;
// Remove the event listeners so they won't run if the events later fire.
node.removeEventListener('load', onLoad);
node.removeEventListener('error', onError);
// And finally, clean up the page.
cleanup();
};
});
}
其中生成唯一回调方法名称的方法:
nextCallback() {
return `ng_jsonp_callback_${nextRequestId++}`; // nextRequestId 从0自增
}
可以看到JsonpClientBackend.handle()实现jsonp请求的方式与我们使用JavaScript实现的方式一致,不同的是我们一般使用jsonp请求每次请求时回调方法都是固定的,已经写好在页面上的,而 JsonpClientBackend 是动态的,每次通过 nextCallback() 方法获取新的回调方法名称,通过 window["xxxxxx"]=(data)=>{} 动态地添加方法,而且回调方法调用一次就把自己删掉了。更重要的是将整个过程按照RxJS响应式编程的规范进行了封装,返回可观察对象 Observable。
5、结语
以上就是Angular中使用JSONP的介绍,可以看到使用JSONP的话自己写的拦截器就不能用了。而且对于JSONP格式的请求不同的后端语言都需要做额外的编码才能够支持。JSONP在众多的跨域解决方案中使用的比例不多。更多的还是通过 CORS 或者代理实现跨域请求。