1. 什么是jsonp?
下方是维基百科对JSON的解释
从这个解释中,我们可以知道,完成jsonp需要的步骤主要有以下两点:
- 向页面中插入一个带有请求链接的
<script>
标签 - 通过回调函数,获取需要的JSON数据
2. jsonp库是如何实现的?
jsonp是一个star数1.9k的仓库,实现了一个简单的jsonp方法
2.1 传入参数
- url
传入的url就是需要请求的链接地址
- opts
param:传入的是缀在链接后的参数,默认为callback
timeout:请求超时时间,默认为60000
prefix:全局回调函数名称的前缀,默认为__jp
name:全局回调函数的名字,默认由前缀和自增数字生成
- fn
回调函数的第一个参数是err,如果失败返回错误:Timeout,如果成功返回null。
第二个参数是data,也就是最终请求的内容
调用该函数时,还会返回一个取消函数,如果希望取消请求,直接调用返回方法即可。
2.2 分析代码
2.2.1 定义变量
count
为计数器,noop
为空函数(后面在重置全局函数时会用到)。
将2.1中定义的默认值,在代码里初始化,并且定义了变量。
2.2.2 设置超时定时器 & 清理页面中的代码
将页面插入的<script>
标签代码删除,并将全局的回调方法置为空方法。如果有定时器则删除定时器
调用超时后,清除清除页面中的代码。如果有回调函数,将会抛出Timeout
报错。
定义了返回的取消函数,本质上是调用cleanup
函数清理全局页面中的代码。
2.2.3 将回调函数挂载到全局
将回调函数挂载到全局,返回数据后调用cleanup
函数清理全局页面中的代码,并将数据返回给传入的fn
函数
2.2.4 处理请求地址
处理请求地址,将encodeURIComponent
后的参数拼接至url
上
2.2.5 挂载<script>
并返回取消函数
创建<script>
标签,并挂载到页面上。最后返回取消函数。
使用target.parentNode.insertBefore
的原因是由于target.appendChild
兼容性不佳。按照提交者的说法是:
make IE<=8 happy😁
3.如何实现一个自己的jsonp?
通过分析上面的代码,我们不难发现,主要是完成以下几个功能
- 实现请求超时报错
- 实现将回调函数挂载至window
- 实现处理url请求
- 实现创建script标签,并插入页面中
第一、四部分的代码,我们可以继续使用。
第二部分的代码,实际上还是无法保证回调函数的名称不与全局的方法冲突,因此需要生成一个唯一的函数名称,如果检查名称有冲突则知道生成一个唯一的名称为止。
第三部分的代码,在处理的请求中,传入的参数用的是string
,但是平时开发常用的多为对象,因此在这里需要支持传入对象后并处理成字符串。
3.1 生成唯一函数名代码
function getRandomKey(length = 6) {
let randomKey = '';
for (let i = 0; i < length; i++) {
// 生成0~9和a-z的随机字符串
randomKey += ((Math.random() * 36) | 0).toString(36);
}
return randomKey;
}
function checkRandomKey(key, obj) {
// 检查当前生成的key值是否已经存在于obj中
return obj[key] === undefined
? key
: checkRandomKey(getRandomKey(), obj);
}
checkRandomKey(getRandomKey(), window);
将会在window上检测生成的随机字符串是否已被占用,如果被占用,则再生成一个。
3.2 拼接对象类型的参数
for (var key in params) {
param += `${key}=${encodeURIComponent(params[key])}&`;
}
将代码拼接成字符串,并且使用encodeURIComponent进行转义。
3.3 优化传入参数
将url
参数并入opts
中,并将opts
改名为config
(比较喜欢axios的设计,所以叫了一样的名字😁),fn
修改为callback
。
4. 最终代码
function jsonp(config, callback) {
let {url, params, name, prefix = '_jsonp_callback_', timeout = 60000} = config;
const target = document.getElementsByTagName('script')[0] || document.head;
let script;
let timer;
let callbackFunctionName;
let paramsString = '';
// 定义空函数
function noop() {
}
// 生成随机key值
function getRandomKey(length = 6) {
let randomKey = '';
for (let i = 0; i < length; i++) {
// 生成0~9和a-z的随机字符串
randomKey += ((Math.random() * 36) | 0).toString(36);
}
return randomKey;
}
function checkRandomKey(key, obj) {
// 检查当前生成的key值是否已经存在于obj中
return obj[key] === undefined
? key
: checkRandomKey(getRandomKey(), obj);
}
// 确定挂在window上的回调函数名称
callbackFunctionName = name || checkRandomKey(getRandomKey(), window);
// 清理不需要的代码
function cleanup() {
if (script.parentNode) script.parentNode.removeChild(script);
window[callbackFunctionName] = noop;
if (timer) clearTimeout(timer);
}
// 取消调用
function cancel() {
if (window[callbackFunctionName]) cleanup();
}
// 设置定时器
if (timeout) {
timer = setTimeout(function () {
cleanup();
if (callback) callback(new Error('Timeout'));
}, timeout);
}
// 将传入的params转化为字符串
if (params) {
for (var key in params) {
paramsString += `${key}=${encodeURIComponent(params[key])}&`;
}
}
// 拼接默认的callback内容
paramsString += `callback=${prefix}${callbackFunctionName}`;
// 将回调函数设置到window上
window[callbackFunctionName] = function (data) {
cleanup();
if (callback) callback(null, data);
};
// 将请求参数拼接至url上
url += (~url.indexOf('?') ? '&' : '?') + paramsString;
url = url.replace('?&', '?');
// 创建一个script标签并插入到页面中
script = document.createElement('script');
script.src = url;
target.parentNode.insertBefore(script, target);
// 返回取消函数
return cancel;
}
至此我们完成了我们自己的jsonp轮子。如果发现有问题,欢迎评论区留言。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。