1. 什么是jsonp?

下方是维基百科对JSON的解释
image.png

从这个解释中,我们可以知道,完成jsonp需要的步骤主要有以下两点:

  1. 向页面中插入一个带有请求链接的<script>标签
  2. 通过回调函数,获取需要的JSON数据

2. jsonp库是如何实现的?

jsonp是一个star数1.9k的仓库,实现了一个简单的jsonp方法

jsonp仓库传送门

2.1 传入参数

  • url

传入的url就是需要请求的链接地址

  • opts

param:传入的是缀在链接后的参数,默认为callback

timeout:请求超时时间,默认为60000

prefix:全局回调函数名称的前缀,默认为__jp

name:全局回调函数的名字,默认由前缀和自增数字生成

  • fn

回调函数的第一个参数是err,如果失败返回错误:Timeout,如果成功返回null。
第二个参数是data,也就是最终请求的内容

调用该函数时,还会返回一个取消函数,如果希望取消请求,直接调用返回方法即可。

2.2 分析代码

2.2.1 定义变量

image.png

count为计数器,noop为空函数(后面在重置全局函数时会用到)。


image.png

将2.1中定义的默认值,在代码里初始化,并且定义了变量。

2.2.2 设置超时定时器 & 清理页面中的代码

image.png

将页面插入的<script>标签代码删除,并将全局的回调方法置为空方法。如果有定时器则删除定时器


image.png

调用超时后,清除清除页面中的代码。如果有回调函数,将会抛出Timeout报错。


image.png

定义了返回的取消函数,本质上是调用cleanup函数清理全局页面中的代码。

2.2.3 将回调函数挂载到全局

image.png

将回调函数挂载到全局,返回数据后调用cleanup函数清理全局页面中的代码,并将数据返回给传入的fn函数

2.2.4 处理请求地址

image.png

处理请求地址,将encodeURIComponent后的参数拼接至url

2.2.5 挂载<script>并返回取消函数

image.png

创建<script>标签,并挂载到页面上。最后返回取消函数。

使用target.parentNode.insertBefore的原因是由于target.appendChild兼容性不佳。按照提交者的说法是:

make IE<=8 happy😁

3.如何实现一个自己的jsonp?

通过分析上面的代码,我们不难发现,主要是完成以下几个功能

  1. 实现请求超时报错
  2. 实现将回调函数挂载至window
  3. 实现处理url请求
  4. 实现创建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轮子。如果发现有问题,欢迎评论区留言。

5. 参考资料


修仙大橙子
108 声望17 粉丝

前端工程师