前言
最近因为工作的缘故,几乎把市面上所有Jsonp库都下载了一遍,却发现没有百分百让我满意的,最后自己手动改写了Jsonp,才符合了要求,也因此有了这篇文章。本文示例详实,代码简单,想弄明白Jsonp, 这一篇文章就够了。
什么是Jsonp
因为AJAX收到浏览器同源策略的限制,导致在跨域上有心无力,经常需要后台同学的帮助。而在浏览器中,所有带有src的标签都是不受同源策略限制的,如image, script。Jsonp上就是利用了script标签的这个特点,来实现跨域的。
其中,Jsonp和AJAX的原理完全不同,只不过Jquery带了个很不好的头,把两个东西封装在一起了,所以经常让新的同学混淆了。
Jsonp的原理:script src
AJAX的原理:xhr
举一个最简单的Jsonp的例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Jsonp简单示例</title>
</head>
<body>
</body>
<script type="text/javascript">
// Jsonp回调
function jsonCallback(data) {
console.log(data);
}
//添加<script>标签的方法
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type", "text/javascript");
script.src = src;
document.body.appendChild(script);
}
// 向后台发起请求(链接是胡乱写的)
addScriptTag('http://www.qq.com/getJsonp?callback=jsonCallback')
</script>
</html>
http://www.qq.com/getJsonp?callback=jsonCallback
这个链接返回的内容应该是
jsonpCallback({
msg: success
})
这样,相当于后台调用了前台提前写好的callback函数,将要返回的数据当做callback函数的参数传入,这样前端就拿到后台传回来的数据了。
改写Jsonp
但是坦白来说,单纯的拿到数据并不能让我们满意。一个合适的请求函数,必然包含对成功、失败、超时的处理,就像我们上面写的那个简单示例,一旦出现异常,就不能让我们满意了。
在这一点上不得不说Jquery做的很好,Jquery的Jsonp函数包含了对各种情况的处理,还伪造了一个http状态码的返回。
Jsonp和AJAX不同,是拿不到状态码的,但是Jquery对于所有的错误都赋予了一个404的状态码,也是机智
对比其他的组件库(axios-jsonp, axios-jsonp-pro, jsonp, fetch=jsonp-es6), 要不就是完全没有对超时的处理,要不然就是把错误和超时混成一谭,更有甚者,有些都不能自定义callback函数的名字。这简直太过分了。
那我为什么不选择Jquery呢?因为太大了,webpack引入JQuery后瞬间大了80K, 而且单独将Jsonp打包出来也有70K的样子,而我的源码只有20K,这是我不能接受的。
jsonp这个组件的问题是没有对错误的处理,理解了Jsonp的原理,我们能很容易的添加上这块的逻辑,以下是添加后的源码:
/**
* Module exports.
*/
module.exports = jsonp;
/**
* Callback index.
*/
var count = 0;
/**
* Noop function.
*/
function noop(){}
/**
* JSONP handler
*
* Options:
* - param {String} qs parameter (`callback`)
* - prefix {String} qs parameter (`__jp`)
* - name {String} qs parameter (`prefix` + incr)
* - timeout {Number} how long after a timeout error is emitted (`60000`)
*
* @param {String} url
* @param {Object|Function} optional options / callback
* @param {Function} optional callback
*/
function jsonp(url, opts, fn){
if ('function' == typeof opts) {
fn = opts;
opts = {};
}
if (!opts) opts = {};
var prefix = opts.prefix || '__jp';
// use the callback name that was passed if one was provided.
// otherwise generate a unique name by incrementing our counter.
var id = opts.name || (prefix + (count++));
var param = opts.param || 'callback';
var timeout = null != opts.timeout ? opts.timeout : 60000;
var enc = encodeURIComponent;
var target = document.getElementsByTagName('script')[0] || document.head;
var script;
var timer;
if (timeout) {
timer = setTimeout(function(){
cleanup();
if (fn) fn(new Error('Timeout'));
}, timeout);
}
function cleanup(){
if (script.parentNode) script.parentNode.removeChild(script);
window[id] = noop;
if (timer) clearTimeout(timer);
}
function cancel(){
if (window[id]) {
cleanup();
}
}
window[id] = function(data){
cleanup();
if (fn) fn(null, data);
};
// add qs component
url += (~url.indexOf('?') ? '&' : '?') + param + '=' + enc(id);
url = url.replace('?&', '?');
// create script
script = document.createElement('script');
script.src = url;
// 添加对错误的处理
script.onerror = function (evt) {
if (fn) fn(new Error('Error'));
if (timer) clearTimeout(timer)
}
target.parentNode.insertBefore(script, target);
return cancel;
}
因为大部分是人家的代码,我也就不班门弄斧了,有需要的可以直接npm install jsonp, 然后比对node_modules/jsonp/index.js进行修改;有需要对Jsonp有更详细的处理的,也可以在我的基础上继续添加。
总结
Jsonp的本质就是创建一个回调函数,然后在远程服务上调用这个函数并且将JSON数据形式作为参数传递,完成回调。比起另外两种后台无感知的跨域方案:image src、fetch no-cor,Jsonp可以对错误和超时进行处理,也能对后台返回的数据进行分析;而对于AJAX,Jsonp免去了后台添加跨域头的烦恼,后台的改动较小,一次写好,终生受用(跨域头还要不断维护白名单)。这三种方案都有各自的使用场景,要在不同的场景进行恰当的选用,以上。