JSONP
网上有蛮多的 JSONP 的教程,但看多了容易乱,故此记录一下。
误解 1 - JSON
JSONP == JSON with Padding
JSONP 是个坏名字,往往让人觉得必须得是 JSON,但并不一定。只是由于 JSONP 主要用于跨域取数据,说的精确点,是某数据提供者对第三方开放的取数据接口。数据可能很复杂,但必须有良好的格式(不能丢了自己的面皮啊),所以 99.99% 的情况下都会选择 JSON 这个格式。
可能因为如此才叫的 JSONP 吧。P
为 Padding,表示 JSON 字符串两边需要填充一些东西。填充什么呢,就是能把数据包成一个 JS 方法调用的语句:___(
和 );
。
误解 2 - AJAX
JSONP == 跨域 AJAX - NONONO。
JSONP 跟 AJAX 半点扯不上关系,完全没有用到 XMLHttpRequest
或其 ActiveX
版本。它所用到的纯粹是 script
标签,利用的是它跨域加载的特性。script
标签加载动态 JS 的形式跟 AJAX 很像,但跟 AJAX 相比,有以下区别:
- AJAX 可以指定异步或同步;JSONP 只能异步 - 因为
script
的加载是异步且不可控的 - AJAX 可以传 HTTP header 信息;JSONP 不行 - 无法在
script
标签中加入头信息 - AJAX 可以指定 method 为 POST 或 GET;JSONP 只能 GET -
script
标签只能以URL
的形式传参数 - AJAX 可以 abort;JSONP 严格来说无法 abort - 即使从 DOM 中把
script
删掉都不行
原理 - 基础
JSONP 之所以被广泛用于解决跨域获取数据,是因为它是在很简单。你不需要对服务端做任何的配置,只需要前后端有个约定即可。
它利用 script
标签的跨域加载特性。原理就是:让服务端生成一段很简单纯粹的 JS 脚本,一般是这样的 ___(...);
,其中 ___
是前端代码告知后端的 JS 方法名,...
是后端真正给的数据。前端代码通过临时创建 ___
方法,这个方法一般做的事情就是返回它所获得的参数。这样,在解决了跨域的基础上,前端代码就能获得服务端给出的数据了。
JSONP 利用了 script
标签的跨域加载特性,加载的内容为 JavaScript 代码,script
插到 DOM 中,其内容会被浏览器自动解释执行。
原理 - 协议
JSONP 制定了这样一个协议:数据提供者(server)认为带某参数名(比如 jsonp/callback/jsonp_callback
之类)为 JSONP 请求,并用其值加上数据拼装成一段 JS,类似 func_name(data);
;数据索求者(client)在请求数据的时候在 url 上加上指定的参数,比如 server 指定的 JSONP 标志参数为 jsonp
,则请求的 url 须加上 jsonp=js_func_name
;这里 js_func_name
必须是能在 client 端的 window 对象下按路径能找到的 JS 方法。
于是 client 端得到的是 js_func_name(...data...)
这样的 JS 代码,所以这就是为什么前面说 JSONP 并不一定跟 JSON 有关,这里的 data
完全可以是别的格式。
总结 - 该协议就是提供者和请求者商量 JSONP 的名字和值,提供者决定怎么传(名字),请求者决定传什么(值)。
动手实例
数据提供者(server)
可以自己写一个,或者网上找一个,但是网上很多都需要申请 key。在 一个例子 里找到一个可用的:http://api.twitter.com/1/statuses/user_timeline/codinghorror.json?callback=?。
但是退特你知道,被 GCD 河蟹了的,所以你很可能需要自己写一个。我偷了一下懒,用 fiddler 的「AutoRespondor」功能模拟了一个:
而这个 .jsonp
的内容很简单:
fuck({
a: 1
});
这里约定 JSONP 的标志性参数名为 jsonp
(当然,因为这里没有任何的后台代码,client 的任何请求都会成为一个 JSONP 请求…姑且认为限定为 jsonp
)。而 jsonp
的值也给限死成 fuck
了(其实应该由 client 端决定)。
数据请求者
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "http://fake-xdomain.com/x.jsonp?jsonp=fuck";
document.head.appendChild(script);
window.fuck = function() {
console.warn(arguments);
};
var cmp = function() {
document.head.removeChild(script);
window.fuck = null;
delete window.fuck;
};
script.onload = function() {
console.warn("success");
cmp();
};
script.onerror = function() {
console.warn("error");
cmp();
};