JSONP详细实现

2,839 阅读6分钟

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

什么是JSONP

JSONP是JSON with Padding的略称,是json的一种‘使用模式’,是非官方的一种跨域读取数据的方案

由于浏览器同源策略的限制,浏览器只允许请求当前源(域名、协议、端口)的资源,而 Web页面上调用js文件时则不受是否跨域的影响,比如 script 元素(凡是拥有"src"这个属性的标签都拥有跨域的能力)。利用 script 元素的这个开放策略,网页可以跨域读取数据,而这种使用模式就是所谓的 JSONP。

使用模式的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据( callbackFunction( 服务端返回的json格式数据 ) ),这样客户端就可以随意定制自己的回调函数来自动处理返回数据了。

注意要点:

  • 1.通过客户端的script标签发出的请求,是GET请求。也就是说JSONP只支持GET请求。
  • 2.服务端返回的数据不是 JSON,而是 JavaScript,也就是说 contentType 为 application/javascript ,内容格式为callbackFunction(JOSN)

JSON vs JSONP

JSON是一种数据交换格式,而JSONP是一种依靠开发人员的聪明才智创造出的一种非官方跨域数据交互协议。我们拿最近比较火的谍战片来打个比方,JSON是地下党们用来书写和交换情报的“暗号”,而JSONP则是把用暗号书写的情报传递给自己同志时使用的接头方式。看到没?一个是描述信息的格式,一个是信息传递双方约定的方法。

JSON

JSON: JavaScript Object Notation(JavaScript 对象表示法)

JSON 是存储和交换文本信息的语法,类似 XML。

JSON 比 XML 更小、更快,更易解析。

JSON vs XML

与 XML 相同之处
  • JSON 是纯文本
  • JSON 具有"自我描述性"(人类可读)
  • JSON 具有层级结构(值中存在值)
  • JSON 可通过 JavaScript 进行解析
  • JSON 数据可使用 AJAX 进行传输

与 XML 不同之处
  • 没有结束标签
  • 更短
  • 读写的速度更快
  • 能够使用内建的 JavaScript eval() 方法进行解析
  • 使用数组
  • 不使用保留字

JSON优点

1、基于纯文本,跨平台传递极其简单;

2、Javascript原生支持,后台语言几乎全部支持;

3、轻量级数据格式,占用字符数量极少,特别适合互联网传递;

4、可读性较强,虽然比不上XML那么一目了然,但在合理的依次缩进之后还是很容易识别的;

5、容易编写和解析,当然前提是你要知道数据结构;

对于 AJAX 应用程序来说,JSON 比 XML 更快更易使用:

使用 XML
  • 读取 XML 文档
  • 使用 XML DOM 来循环遍历文档
  • 读取值并存储在变量中
使用 JSON
  • 读取 JSON 字符串
  • 用 eval() 处理 JSON 字符串

JSONP VS Ajax

1、ajax和jsonp这两种技术在调用方式上“看起来”很像,目的也一样,都是请求一个url,然后把服务器返回的数据进行处理,因此jquery和ext等框架都把jsonp作为ajax的一种形式进行了封装;

2、但ajax和jsonp其实本质上是不同的东西。ajax的核心是通过XmlHttpRequest获取非本页内容,而jsonp的核心则是动态添加

3、所以说,其实ajax与jsonp的区别不在于是否跨域,ajax通过服务端代理一样可以实现跨域,jsonp本身也不排斥同域的数据的获取。

4、还有就是,jsonp是一种方式或者说非强制性协议,如同ajax一样,它也不一定非要用json格式来传递数据,如果你愿意,字符串都行,只不过这样不利于用jsonp提供公开服务。

总而言之,jsonp不是ajax的一个特例,哪怕jquery等巨头把jsonp封装进了ajax,也不能改变着一点!

相同点:

  • 使用的目的一致,都是客户端向服务端请求数据,将数据拿回客户端进行处理。

不同点:

  • ajax请求是一种官方推出的请求方式,通过xhr对象去实现,jsonp是民间发明,script标签实现的请求。
  • ajax是一个异步请求,jsonp是一个同步请求
  • ajax存在同源检查,jsonp不存在同源检查,后端无需做解决跨域的响应头。
  • ajax支持各种请求的方式,而jsonp只支持get请求
  • ajax的使用更加简便,而jsonp的使用较为麻烦。

JSONP的实现

常规实现:

优化:

  • 请求结束需要移除本次请求产生的 script 标签window上的回调函数
  • 需要考虑同时多个 JSONP 请求的情况,callbackFunction 挂在 window 上的属性名需要唯一。

全局回调函数

我们在发送请求之前需要先准备一个函数callbackFunction ,将其拼接在url后传给服务端,服务端会返回callbackFunction( 服务端返回的json格式数据 ), 然后浏览器会执行这个带有数据参数的回调函数。

callbackFunction 需要注册在 window 对象上,因为 script 加载后的执行作用域是window作用域。

 //声明一个全局函数 'callback',用于接收响应数据(服务器返回的数据),后面在函数里要变
 window.callbackFunction = (res)=>{
     console.log(res)
 }

创建用于拼接url、请求参数、回调函数名的函数

例如http://localhost:3002?a=1&b=2&callback=callbackFunction中的callback是前后端约定好的属性。

 const objectToQuery = function (url, paramsObj, callbackFnName) {
   const paramsArr = [];
   for ( let param in paramsObj) {
     paramsArr.push(encodeURIComponent(param)+ '=' +encodeURIComponent(paramsObj[param]));
   }
   return url+'?' + ( paramsArr.length > 0 ? paramsArr.join('&')+'&' : '' )  + `callback=${callbackFnName}`;
 }

创建用于创建script标签,发出GET请求的函数

 const loadScript = function (url) {
   let script = document.createElement("script");  // 创建一个script标签
   script.type = "text/javascript";   // 文档类型设置为js文档
   script.setAttribute('src', url);
   document.body.appendChild(script);
 }

完整的常规实现JSONP代码:

 ​
 ​
 ​
 const loadScript = function (url,paramsObj, callbackFn) {
   
   //用于拼接url、请求参数、回调函数名的函数
   const objectToQuery = function (url, paramsObj, callbackFn) {
     const paramsArr = [];
     for ( let param in paramsObj) {
       paramsArr.push(encodeURIComponent(param)+ '=' +encodeURIComponent(paramsObj[param]));
     }
     return url+'?' + ( paramsArr.length > 0 ? paramsArr.join('&')+'&' : '' )  + `callback=${callbackFn}`;
   }
 ​
   const callbackFnName = callbackFn.name
   url = objectToQuery(url,paramsObj, callbackFnName)
   let script = document.createElement("script");  // 创建一个script标签
   script.type = "text/javascript";   // 文档类型设置为js文档
   script.setAttribute('src', url);
   document.body.appendChild(script);
 ​
   //声明一个全局函数 'callbackFn',用于接收响应数据(服务器返回的数据)
   window[callbackFnName] = (res)=>{
     callbackFn && callbackFn(res)
     //请求结束需要移除本次请求产生的 script 标签和window上的回调函数
     document.body.removeChild(script);
     delete window[callbackFnName];
   }
 }
 ​
 //调用 创建script标签,发出GET请求的函数
 loadScript( 'http://localhost:3002/login', { }, function cb (res) { console.log(res); } )
 ​

后端node代码:

需要安装node,并下载express包

 const express = require('express')
 ​
 const hostname = 'localhost';
 const port = 3002;
 ​
 const app = express();
 app.use(express.json())
 //定义/的路由
 app.get('/', (req, res) => {
   console.log('收到消息了');
   res.json({msg: '欢迎', code: 200});
 })
 app.get('/login', (req, res) => {
   console.log('进来了');
   res.send(`${req.query.callback}('nihaoya!!')`)
 })

Promise实现:

 ​
 const loadScript = function (url,paramsObj, callbackFn) {
   
   //用于拼接url、请求参数、回调函数名的函数
   const objectToQuery = function (url, paramsObj, callbackFn) {
     const paramsArr = [];
     for ( let param in paramsObj) {
       paramsArr.push(encodeURIComponent(param)+ '=' +encodeURIComponent(paramsObj[param]));
     }
     return url+'?' + ( paramsArr.length > 0 ? paramsArr.join('&')+'&' : '' )  + `callback=${callbackFn}`;
   }
 ​
   const callbackFnName = callbackFn.name
   url = objectToQuery(url,paramsObj, callbackFnName)
   let script = document.createElement("script");  // 创建一个script标签
   script.type = "text/javascript";   // 文档类型设置为js文档
   script.setAttribute('src', url);
   document.body.appendChild(script);
 ​
   return new Promise((resolve, reject) => {
     //声明一个全局函数 'callbackFn',用于接收响应数据(服务器返回的数据)
     window[callbackFnName] = (res)=>{
       try{
         resolve(res)
       }catch(e){
         reject(e)
       }finally{
         //请求结束需要移除本次请求产生的 script 标签和window上的回调函数
         document.body.removeChild(script);
         delete window[callbackFnName];
       }
     }
   })
 ​
 }
 ​
 //调用 创建script标签,发出GET请求的函数
 let p1 = loadScript( 'http://localhost:3002/login', { }, function cb (res) { console.log(res); } )
 p1.then(val => {
   console.log(val);
 })

参考:

juejin.cn/post/694769…

juejin.cn/post/684490…

www.cnblogs.com/dowinning/a…

\