6

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

 2 years ago
source link: https://www.fly63.com/article/detial/12050
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

什么是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-pro

github: https://github.com/peng/jsonp-pro

npmhttps://www.npmjs.com/package/jsonp-pro

来自:https://segmentfault.com/a/1190000042354982

链接: https://www.fly63.com/article/detial/12050


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK