JSONP利用<script>标签没有跨域限制达到跨域通信的目的,本站脚本创建一个
<script>
元素,地址指向第三方API的网址<script src='https://i2-test-browser-cpc.wanyol.com/oppoNews/getRelativeRecList?num=5&f=jsonp&docId=tup1z5Al8I&source=oppo&userId=&uuid=&feedssession=&callback=JSONP_oppoNewsgetRelativeRecList_26427578'></script>
,并提供一个回调函数来接收数据,函数名可约定,通过地址参数传递,第三方的响应为一个json包装的数据,(称为jsonp, json padding)JSONP_oppoNewsgetRelativeRecList_26427578({"code":0,"data":[], "desc": ""})
后端返回的是调用callback的js代码,浏览器加载这段代码后,会调用callback函数,并传递解析后json对象作为参数执行
基本原理:
-
html
标签的src
属性没有同源限制,浏览器解析<script>
标签时,会自动下载src
属性值指向的资源 - <script>标签指向的资源文件被下载后,其中的内容会被立即执行
- 服务端的程序会解析
src
属性值中的url
传递的参数,根据这些参数针对性返回一个/多个函数调用表达式,这些函数调用表达式的参数就是客户端跨域想要得到的数据 - 服务器生成、返回的文件中,表达式调用的函数是已经在本地定义好的,参数就是希望从跨域服务器拿到数据
- 字面的<script>标签可以,动态添加到
dom
树中的script
也可以,后者更方便绑定事件
简易实现版:
<body>
<button class="btn">start</button>
<script>
function JSONP_oppoNewsgetRelativeRecList_26427578(data) {
console.log(data)
return data
}
document.querySelector('.btn').addEventListener('click', function(){
var script = document.createElement('script')
script.type = 'text/javascript'
script.src = 'https://i2-test-browser-cpc.wanyol.com/oppoNews/getRelativeRecList?num=5&f=jsonp&docId=tup1z5Al8I&source=oppo&userId=&uuid=&feedssession=&callback=JSONP_oppoNewsgetRelativeRecList_26427578'
document.body.appendChild(script)
script.parentNode.removeChild(script)
})
</script>
完善版本
function ajax(options) {
function setData() {
var map = {};
if (typeof data === 'string') {
data.split('&').forEach(function(item) {
var tmp = item.split('=');
map[tmp[0]] = tmp[1];
});
} else if (typeof data === 'object') {
map = data;
}
if (dataType === 'jsonp') {
var timeName;
if (jsonpCache > 0) {
timeName = parseInt(Date.now() / (jsonpCache * 1000 * 60));
} else {
timeName = Date.now() + Math.round(Math.random() * 1000);
}
callback = callback
? ['JSONP', callback, timeName].join('_')
: ['JSONP', timeName].join('_');
map['callback'] = callback;
}
var arr = [];
for (var name in map) {
var value = (map[name] && map[name].toString()) || '';
name = encodeURIComponent(name);
value = encodeURIComponent(value);
arr.push(name + '=' + value);
}
map = arr.join('&').replace('/%20/g', '+');
if (type === 'get' || dataType === 'jsonp') {
url += url.indexOf('?') > -1 ? map : '?' + map;
}
}
// JSONP
function createJsonp() {
window[callback] = function(data) {
clearTimeout(timeoutFlag);
script.parentNode.removeChild(script);
success(data);
};
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
setTime(callback, script);
script.onerror = script.onreadystatechange = function() {
if (
!this.readyState ||
((this.readyState === 'loaded' || this.readyState === 'complete') &&
!window[callback])
) {
delete window[callback];
script.onload = script.onreadystatechange = null;
script.parentNode.removeChild(script);
clearTimeout(timeoutFlag);
error(new Error('ajax_load_error'));
}
};
document.body.appendChild(script);
}
// 设置请求超时
function setTime(callback, script) {
if (timeOut !== undefined) {
timeoutFlag = setTimeout(function() {
if (dataType === 'jsonp') {
delete window[callback];
script.parentNode.removeChild(script);
error(new Error('ajax_load_timeout'));
} else {
timeoutBool = true;
xhr && xhr.abort();
}
console.warn('timeout:: ', url);
}, timeOut);
}
}
function createXHR() {
xhr = new XMLHttpRequest();
xhr.open(type, url, async);
if (type === 'post' && !contentType) {
xhr.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencodedcharset=UTF-8',
);
} else if (contentType) {
xhr.setRequestHeader('Content-Type', contentType);
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (timeOut !== undefined) {
if (timeoutBool) {
return;
}
clearTimeout(timeoutFlag);
}
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
success(xhr.responseText);
} else {
error(xhr.status, xhr.statusText);
}
}
};
xhr.send(type === 'get' ? null : data);
setTime();
}
var url = options.url || '';
var type = (options.type || 'get').toLowerCase();
var data = options.data || null;
var callback = options.callback || null;
var contentType = options.contentType || '';
var dataType = options.dataType || '';
var async = options.async === undefined && true;
var timeOut = options.timeOut;
var before = options.before || function() {};
var error = options.error || function() {};
var success = options.success || function() {};
var jsonpCache = parseInt(options.jsonpCache) || 0;
var timeoutBool = false;
var timeoutFlag = null;
var xhr = null;
setData();
before();
if (dataType === 'jsonp') {
createJsonp();
} else {
createXHR();
}
}
export default ajax;
用法
function createFetch(api, data) {
const url = getBaseUri() + api;
return new Promise(function (resolve, reject) {
ajax({
url: url,
dataType: 'jsonp',
timeOut: 15000,
jsonpCache: 1,
data: data,
callback: api.replace(/\//g, ''),
success: function (res) {
resolve(res)
},
error: function (error) {
reject(error)
}
})
}).then(res => {
let data
let error
switch (res.code) {
case 0:
data = res.data
break
case undefined:
data = res.articles
break
case 1400:
error = createError(1400, '请求参数错误')
break
case 1401:
error = createError(1401, '鉴权失败')
break
case 1403:
error = createError(1403, 'Session过期')
break
case 1500:
error = createError(1500, '内部服务错误')
break
case 1504:
error = createError(1504, '第三方资源请求超时')
break
default:
error = createError(res.code || -1, res.msg || res.message || res.desc || 'unknown mistake')
break
}
return error ? Promise.reject(error) : data
}).catch(error => {
console.log(`%c>>Request ${url} get error ${error.code}(${error.message})`, 'color: red;')
return Promise.reject(new Error(error.message))
})
}
export function fetchRelatedNews(data = {}) {
const params = Object.assign({
num: '5'
}, getBasicParams(), data)
return createFetch('oppoNews/getRelativeRecList', params)
}
关于jsonp的xss漏洞有时间再说