likes
comments
collection

jsonp 原理详解及 jsonp-pro 源码解析

作者站长头像
站长
· 阅读数 0

什么是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 的方式去跨域。

  1. 根据上面的原理简介,首先我们在全局生命一个函数。

    window.jsonp1 = function(data) {
     console.log(data)
    }
  2. 动态的往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标签
  3. 最后需要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-progithub: https://github.com/peng/jsonp...npm: https://www.npmjs.com/package...