网上有蛮多的 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 相比,有以下区别:

  1. AJAX 可以指定异步或同步;JSONP 只能异步 - 因为 script 的加载是异步且不可控的
  2. AJAX 可以传 HTTP header 信息;JSONP 不行 - 无法在 script 标签中加入头信息
  3. AJAX 可以指定 method 为 POST 或 GET;JSONP 只能 GET - script 标签只能以 URL 的形式传参数
  4. 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();
};