什么是JSONP
JSONP(JSON with Padding)是资料格式JSON的一种“使用模式”,可以让网页从别的网域获取资料。
由于浏览器同源策略,一般来说位于server1.a.com的网页无法与 server2.a.com的服务器沟通,而HTML的 <script>元素是一个例外。利用 <script>元素的这个开放策略,网页可以得到从其他来源动态产生的JSON资料,而这种使用模式就是所谓的 JSONP。用JSONP抓到的数据并不是JSON,而是任意的JavaScript,用 JavaScript解释器执行而不是用JSON解析器解析。
JSONP实现原理
jsonp 概念我们了解了,那么如何实现呢?
我们要获取一段JSON数据没有跨域问题,我们可以通过xhr GET 方式,假设请求地址是当前域下 /apis/data?id=123
,当前域名是example.com;
返回数据是
{
name=peng,
age=18
}
当我们获取的数据不在同域的情况,比如上边的例子请求域名改成example2.com,页面所使用的域名还是example.com,使用 JSONP 的方式去跨域。
-
根据上面的原理简介,首先我们在全局生命一个函数。
window.jsonp1 = function(data) { console.log(data) }
-
动态的往head标签中插入script标签
const head = document.getElementByTagName('head')[0] // 获取页面的head标签 const script = document.createElement('script') // 创建script标签 script.src = 'https://example2.com/apis/data?id=123&callback=jsonp1' // 给script标签赋值src地址 head.appendChild(script) // 最后插入script标签
-
最后需要script标签返回的内容是一个方法调用并传入参数是要返回的内容。
jsonp1({ name=peng, age=18 })
以上就对JSONP原理进行一个简易的实现。
真正实现一个JSONP网络请求库
以上对JSONP原理和实现有了初步了解,如果我们要在日常项目中使用,那就需要会封装一个完整的JSONP网络请求库。
下面我们对jsonp-pro网络请求库做一个源码分析,从中了解并学习如何封装一个JSONP网络请求库。
先来看看请求的通用方法 method 库
// 检查类型的方法,用于对方法传入类型的限制
/**
* object check method
*
* @param {*} item variable will be check
* @param {string} type target type. Type value is 'String'|'Number'|'Boolean'|'Undefined'|'Null'|'Object'|'Function'|'Array'|'Date'|'RegExp'
* @return {boolean} true mean pass, false not pass
*/
function typeCheck(item, type) {
// 使用 Object.prototype.toString.call 方法,因为这个方法获取类型最全
const itemType = Object.prototype.toString.call(item);
// 拼接结果来做判断
let targetType = `[object ${type}]`;
if (itemType === targetType) {
return true;
} else {
return false;
}
}
// 获取随机数字型字符串,使用时间戳+随机数拼接保证每次活的的字符串没有重复的
function randNum() {
// get random number
const oT = new Date().getTime().toString();
const num = Math.ceil(Math.random() * 10000000000);
const randStr = num.toString();
return oT + randStr;
}
export { typeCheck, randNum };
主文件,主要方法
import { typeCheck, randNum } from './methods';
// 传参的解释说明,非常详细。这里不做过多解释
/**
* Param info
* @param {string} url url path to get data, It support url include data.
* @param {Object=} options all options look down
* @param {(Object | string)=} options.data this data is data to send. If is Object, Object will become a string eg. "?key1=value1&key2=value2" . If is string, String will add to at the end of url string.
* @param {Function=} options.success get data success callback function.
* @param {Function=} options.error get data error callback function.
* @param {Function=} options.loaded when data loaded callback function.
* @param {string=} options.callback custom callback key string , default 'callback'.
* @param {string=} options.callbackName callback value string.
* @param {boolean} options.noCallback no callback key and value. If true no these params. Default false have these params
* @param {string=} options.charset charset value set, Default not set any.
* @param {number=} options.timeoutTime timeout time set. Unit ms. Default 60000
* @param {Function=} options.timeout timeout callback. When timeout run this function.
* When you only set timeoutTime and not set timeout. Timeout methods is useless.
*/
export default function(url, options) {
// 获取head节点,并创建scrpit节点
const oHead = document.querySelector('head'),
script = document.createElement('script');
// 声明变量,并给部分值添加默认值
let timer, // 用于时间定时器
dataStr = '', // 用于存传输的query
callback = 'callback', // 和上边的参数一个含义
callbackName = `callback_${randNum()}`, // 和上边的参数一个含义
noCallback = false, // 和上边的参数一个含义
timeoutTime = 60000, // 和上边的参数一个含义
loaded, // 和上边的参数一个含义
success; // 和上边的参数一个含义
const endMethods = []; // 存储最后要执行回调函数队列
// 如果没有url参数抛出异常
if (!url) {
throw new ReferenceError('No url ! Url is necessary !');
}
// 对url参数进行类型检查
if (!typeCheck(url, 'String')) {
throw new TypeError('Url must be string !');
}
// 对所有参数进行处理的方法对象,命名与参数key保持一直方便后续调用
const methods = {
data() {
// data 参数处理方法
const data = options.data;
if (typeCheck(data, 'Object')) {
// 如果是对象类型将对象转换成query字符串并赋值给上面声明过的变量
for (let item in data) {
dataStr += `${item}=${data[item]}&`;
}
} else if (typeCheck(data, 'String')) {
// 如果是字符串类型,直接赋值给上边变量
dataStr = data + '&';
} else {
// 其他情况抛出类型错误
throw new TypeError('data must be object or string !');
}
},
success() {
// 对成功参数方法进行处理
// 将成功方法赋值给上边的变量
success = options.success;
// 进行类型检查,异常抛出错误
if (!typeCheck(success, 'Function'))
throw new TypeError('param success must be function !');
},
error() {
// 对异常参数方法进行处理
// 进行类型检查,异常抛出错误
if (!typeCheck(options.error, 'Function')) {
throw new TypeError('param success must be function !');
}
// 类型检查通过,script标签添加异常事件回调
script.addEventListener('error', options.error);
},
loaded() {
// 将加载完成方法进行处理
// 将加载完成方法赋值给上边变量
loaded = options.loaded;
// 进行类型检查,异常抛出错误
if (!typeCheck(loaded, 'Function')) {
throw new TypeError('param loaded must be function !');
}
},
callback() {
// 将callback参数进行处理
callback = options.callback;
// 进行类型检查,异常抛出错误
if (!typeCheck(callback, 'String')) {
throw new TypeError('param callback must be string !');
}
},
callbackName() {
// 将callbackName参数进行处理
callbackName = options.callbackName;
// 进行类型检查,异常抛出错误
if (!typeCheck(callbackName, 'String')) {
throw new TypeError('param callbackName must be string !');
}
},
noCallback() {
// 将noCallback参数进行处理
noCallback = options.noCallback;
// 进行类型检查,异常抛出错误
if (!typeCheck(noCallback, 'Boolean')) {
throw new TypeError('param noCallback must be boolean !');
}
},
charset() {
// 将charse参数进行处理
const charset = options.charset;
if (typeCheck(charset, 'String')) {
// 设置script标签charset,浏览器一般默认是UTF8,如果有特殊的需要手动设置
script.charset = charset;
} else {
// 进行类型检查,异常抛出错误
throw new TypeError('param charset must be string !');
}
},
timeoutTime() {
// 将timeoutTime参数进行处理
timeoutTime = options.timeoutTime;
// 进行类型检查,异常抛出错误
if (!typeCheck(timeoutTime, 'Number')) {
throw new TypeError('param timeoutTime must be number !');
}
},
timeout() {
// 将timeout方法进行处理
// 进行类型检查,异常抛出错误
if (!typeCheck(options.timeout, 'Function')) {
throw new TypeError('param timeout must be function !');
}
function timeout() {
function outTime() {
// 移除无用的script节点
script.parentNode.removeChild(script);
// 删除命名在全局的方法
window.hasOwnProperty(callbackName) && delete window[callbackName];
// 清除定时器
clearTimeout(timer);
// 执行超时函数
options.timeout();
}
// 设置超时函数
timer = setTimeout(outTime, timeoutTime);
}
endMethods.push(timeout); // 超时函数放在队列中最后执行
}
};
// 遍历选项执行对应的方法
for (let item in options) {
methods[item]();
}
// 执行最后要执行的队列
endMethods.forEach(item => {
item();
});
// 如果没有回调,并且请求query不为空的情况。兼容是否有问号情况
// warn url include data
if (noCallback && dataStr != '') {
url.indexOf('?') == -1
? (url += `?${dataStr.slice(0, -1)}`)
: (url += `&${dataStr.slice(0, -1)}`);
}
// 有回调且兼容有无问号情况
if (!noCallback) {
// 添加全局方法
window[callbackName] = data => {
// 有成功回调则执行,并且将参数传入
success && success(data);
// 移除script标签
oHead.removeChild(script);
// 移除全局方法
delete window[callbackName];
};
url.indexOf('?') == -1
? (url += `?${dataStr}${callback}=${callbackName}`)
: (url += `&${dataStr}${callback}=${callbackName}`);
}
// 对url编码
url = encodeURI(url);
// 给script标签添加加载完成回调
function loadLis() {
// 移除加载完成方法
script.removeEventListener('load', loadLis);
// 参数中有回调则执行回调
loaded && loaded();
// 清除定时器
clearTimeout(timer);
}
// 添加加载完成方法
script.addEventListener('load', loadLis);
// 将url赋值给script标签
script.src = url;
// 最后将script标签插入
oHead.appendChild(script);
}
以上就完成实现了一个完整的JSONP网络请求库。
jsonp-pro
github: https://github.com/peng/jsonp...
npm: https://www.npmjs.com/package...