JSONP实现原理
jsonp,其实就是单纯为了实现跨域请求而创造的一个欺骗(trick)。
虽然,因为同源策略的影响,不能通过XMLHttpRequest请求不同域上的数据(Cross -origin reads)。但是,在页面上引入不同域上的js脚本文件却是可以的(Cross -origin embedding)。因此,在js文件载入完毕之后,触发回调,可以将需要的data作为参数传入
注意,实现方式(需前后端配合)
优点
兼容性好(兼容低版本IE)
缺点
JSONP只支持GET请求,XMLHttpRequest相对于JSONP有着更好的错误处理机制。
实现
1.服务端采用的是Node,服务端处理请求方法如下
router.get('/', function(req, res, next) {
console.log('收到客户端的请求:', req.query);
// 传回到客户端的数据
let data = JSON.stringify({
'status':200,
'result':{
'name':'柳成荫',
'site':'123456'
}
});
// 获取方法名称 - 这是客户端传过来的方法名参数
// 因为这个方法名必须是客户端有的,必须要客户端告诉服务端是哪个方法
let methodName = req.query.callback;
let methodStr = methodName + '(' + data + ')';
// 快速结束没有任何数据的响应,客户端会执行这个方法,从而获取到服务端返回的数据
res.end(methodStr)
});
2.封装JSONP
(function (w) {
/**
* jsonp的实现
* @param {Object}option
*/
function jsonp(option) {
// 把success函数挂载在全局的getDate函数上
w.getData = option.success;
// 处理url,拼接参数 - 回调方法是getData
option.url = option.url + '?callback=getData';
// 创建script标签,并插入body
let scriptEle = document.createElement('script');
scriptEle.src = option.url;
document.body.appendChild(scriptEle);
}
// 全局挂载一个jsonp函数
w.jsonp = jsonp;
})(window);
/**
* 把对象转换成拼接字符串
* 把形如
data:{
"sex":"男",
"name":"九月"
}
转换成sex=男&name=九月
* @param paramObj 对象参数
* @param words
* @returns {string} 字符串
*/
function getStrWithObject(paramObj,words){
let resArr = [];
// 1.转换对象
for(let key in paramObj){
let str = key + '=' + paramObj[key];
resArr.push(str);
}
resArr.push(words);
// 3.数组转换成字符串
return resArr.join("&");
}
看似代码没有任何问题。但是,我们测试一下,多次调用jsonp方法。
jsonp({
url:'http://localhost:3000/',
data:{
"sex":"男",
"name":"九月"
},
success:function (data) {
console.log(data);
alert(1);
}
});
jsonp({
url:'http://localhost:3000/',
data:{
"sex":"男",
"name":"九月"
},
success:function (data) {
console.log(data);
alert(2);
}
});
结果会怎么样?的确,还是会执行2次,但是每次执行的都是第二个jsonp方法。这是为什么?
问题出现在这里,多次调用,会发生函数覆盖。
解决方案就是让每一次调用函数名不一致,在JS里有很多方法,比如通过随机数、时间戳之类的。这里采用的是随机数,修改如下。
// 0.产生不同的函数名 - 解决了调用多次请求,造成覆盖的问题
// 如果方法名相同,调用多次,都会执行最后一个,出现覆盖现象
let callBackName = 'lcy' + Math.random().toString().substr(2)
+ Math.random().toString().substr(2);
// 把success函数挂载在全局的getDate函数上
w[callBackName] = option.success;
// 处理url,拼接参数
option.url = option.url + '?' + getStrWithObject(option.data,'callback='+callBackName);
好,现在看似完美解决。然而,我们发现,每次调用jsonp方法,都会在body里插入一个script标签。我们并不希望在调用jsonp之后,添加这样一个标签,我们需要在调用完之后,将其移除。
怎么做呢?我们在将success函数挂载在全局时,我们在success外层再套个函数。在这个函数里调用success方法之后,即已经请求到数据之后,我们就去把这个script标签给它移除就行了。修改如下
// 1.函数挂载在全局
w[callBackName] = function(data){
option.success(data);
// 删除script标签
document.body.removeChild(scriptEle);
};
整个过程就是这样,以下是完整代码。
(function (w) {
/**
* jsonp的实现
* @param {Object}option
*/
function jsonp(option) {
// 0.产生不同的函数名 - 解决了调用多次请求,造成覆盖的问题
// 如果方法名相同,调用多次,都会执行最后一个,出现覆盖现象
let callBackName = 'lcy' + Math.random().toString().substr(2) + Math.random().toString().substr(2);
// 1.函数挂载在全局
w[callBackName] = function(data){
option.success(data);
// 删除script标签
document.body.removeChild(scriptEle);
};
// 2.处理url
option.url = option.url + '?' + getStrWithObject(option.data,'callback='+callBackName);
// 3.创建script标签插入body
let scriptEle = document.createElement('script');
scriptEle.src = option.url;
document.body.appendChild(scriptEle);
}
w.jsonp = jsonp;
})(window);
/**
* 把对象转换成拼接字符串
* @param paramObj 对象参数
* @returns {string} 字符串
*/
function getStrWithObject(paramObj,words){
let resArr = [];
// 1.转换对象
for(let key in paramObj){
let str = key + '=' + paramObj[key];
resArr.push(str);
}
resArr.push(words);
// 3.数组转换成字符串
return resArr.join("&");
}