JSONP 是一种跨域通信手段。原理很简单:利用 script 标签的 src 不受同源策略限制,把前端回调名传给服务器,服务器再返回一段「调用该函数」的脚本。
因为走的是 script 加载,所以 JSONP 只支持 GET。下面按流程、最小实现和更可靠的封装把这件事写清楚。来源:https://segmentfault.com/a/1190000007665361#articleHeader1。
一、概述与流程
-
-
前端把回调函数名当作参数传给服务器,服务器注入参数后再返回,完成「服务端向客户端传数据」。
-
先设定一个 script 标签,callback 指定函数名:
<script src="http://jsonp.js?callback=xxx"></script>
远程服务端调用指定函数并传入参数,把 fn(response) 传回客户端。PHP 侧常见写法如下(原文把 json_encode 写进了字符串里,这里改成真正拼接):
$callback = !empty($_GET['callback']) ? $_GET['callback'] : 'callback';
echo $callback.'('.json_encode($data).')';
客户端收到返回的 JS 脚本后开始解析,执行 fn(response)。
| 步骤 |
谁做 |
结果 |
| 1 |
前端 |
插入 script,src 带 callback 名 |
| 2 |
服务端 |
返回 callback(json) |
| 3 |
浏览器 |
当脚本执行,数据进回调 |
二、简单实现
最小实现就是拼接 URL,再动态往 head 里塞一个 script:
function jsonp(req){
var script = document.createElement('script');
var url = req.url + '?callback=' + req.callback.name;
script.src = url;
document.getElementsByTagName('head')[0].appendChild(script);
}
前端调用示例:
function hello(res){
alert('hello ' + res.data);
}
jsonp({
url : '',
callback : hello
});
Node 服务端把查询参数里的 callback 名和 JSON 拼成一段函数调用:
var http = require('http');
var urllib = require('url');
var port = 8080;
var data = {'data':'world'};
http.createServer(function(req,res){
var params = urllib.parse(req.url,true);
if(params.query.callback){
console.log(params.query.callback);
//jsonp
var str = params.query.callback + '(' + JSON.stringify(data) + ')';
res.end(str);
} else {
res.end();
}
}).listen(port,function(){
console.log('jsonp server is on');
});
这个实现虽短,但有两处不足:回调必须是全局函数(应尽量少暴露全局方法);还缺少参数校验,接口容易被乱调。
三、更可靠的封装
下面这份实现用自增 id 生成一次性全局函数名,请求结束后删节点、删全局函数,并处理 onerror:
(function (global) {
var id = 0,
container = document.getElementsByTagName("head")[0];
function jsonp(options) {
if(!options || !options.url) return;
var scriptNode = document.createElement("script"),
data = options.data || {},
url = options.url,
callback = options.callback,
fnName = "jsonp" + id++;
// 添加回调函数
data["callback"] = fnName;
// 拼接url
var params = [];
for (var key in data) {
params.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
}
url = url.indexOf("?") > 0 ? (url + "&") : (url + "?");
url += params.join("&");
scriptNode.src = url;
// 传递的是一个匿名的回调函数,要执行的话,暴露为一个全局方法
global[fnName] = function (ret) {
callback && callback(ret);
container.removeChild(scriptNode);
delete global[fnName];
}
// 出错处理
scriptNode.onerror = function () {
callback && callback({error:"error"});
container.removeChild(scriptNode);
global[fnName] && delete global[fnName];
}
scriptNode.type = "text/javascript";
container.appendChild(scriptNode)
}
global.jsonp = jsonp;
})(this);
使用示例:
jsonp({
url : "www.example.com",
data : {id : 1},
callback : function (ret){
console.log(ret); }
});
一句话总结:JSONP 靠 script.src 跨域,服务端返回 callback(json);封装时用一次性全局函数名、拼参、用完即删,并补上 onerror。