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

更新日期: 2022-08-27阅读: 574标签: jsonp

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

Jsonp原理

同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。

jsonp跨域访问的原理及参数作用

什么是JSONP?先说说JSONP是怎么产生的?不管jQuery也好,ExtJs也罢,又或者是其他支持jsonp的框架,他们幕后所做的工作都是一样的,下面我来循序渐进的说明一下jsonp在客户端的实现

说说JSON和JSONP区别

由于Sencha Touch 2这种开发模式的特性,基本决定了它原生的数据交互行为几乎只能通过AJAX来实现。当然了,通过调用强大的PhoneGap插件然后打包,你可以实现100%的Socket通讯和本地数据库功能

原生的js实现jsonp的跨域封装

jsonp是利用浏览器请求script文件时不受同源策略的限制而实现的,伪造一个script标签,将请求数据的url赋值给script的src属性,并将该标签添加到html中,浏览器会自动发送请求,返回的一般时一段js代码,即函数的调用代码。

JSONP原理及其简单封装

一个众所周知的问题,Ajax直接请求普通文件存在跨域无权限访问的问题;jsonp不是ajax的一个特例,哪怕jquery等巨头把jsonp封装进了ajax,也不能改变!

使用node.js实现JSONP的实例

JSONP与JSON只有一字之差,我们在使用Jquery的Ajax调用的时候也是使用相同的方法来调用,两者的区别几乎只在于使用的dataType这个属性的不同。但是实际上JSON和JSONP是完全不同的两个东西,JSON是一个数据格式

JSON.stringify() 的深入理解

最近在看《你所不知道的javascript》[中卷]一书,第一部分是类型和语法。本文是基于这部分的产物。在强制类型转换->抽象值操作-> toString 部分,其中对工具函数 JSON.stringify(..) 将 JSON 对象序列化为字符串部分介绍进行了详细的介绍。

让 axios 支持 jsonp

因为不想让再引用新的第三方组件了,所以执念了一下,于是搜索到了下面的代码,调试了一下,发现确实能用,但是存在一个缺陷,就是如果存在连续多次的请求,都会回调到同一个函数上

封装 jsonp请求数据的方法

Jsonp(JSON with Padding) 是 json 的一种\\\"使用模式\\\",可以让网页从别的域名(网站)那获取资料,即跨域读取数据。为什么我们从不同的域(网站)访问数据需要一个特殊的技术( JSONP )呢?这是因为同源策略。

跨域解决方案之JSONP

同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!