JSONP 方式实现跨域请求数据

2024-04-12 21:32

本文主要是介绍JSONP 方式实现跨域请求数据,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

在了解 浏览器的同源政策(SOP) 后知道了浏览器的同源政策对保护用户信息安全的重要性。但是有些时候我们确实需要两个网站间的数据共享。例如腾讯天气的天气数据就是从其他服务器上获取到的,并不是腾讯天气服务器上的数据。那它们是怎么做到跨域资源(数据)共享的呢?在这里学习一下 jsonp 方式实现跨域资源共享。

JSONP 实现原理

并不是页面中所有的请求都受同源政策的限制。例如外链外部 js 文件文件时是不受同源政策的限制的,这也是为什么在使用 jQuery 时,我们并不是一定要将库文件下载到项目后才能引入使用。我们也可以直接引入 jQuery 的线上文件,并不影响正常使用。它的原理是虽然此时进行了跨域(线上资源在 jQuery 的官网上),但是 script 标签的 src 属性外链的资源请求不受浏览器同源政策的影响。这也是我们使用 JSONP 实现跨域资源共享的原理。

JSONP 的基础原理如下示例代码所示(注意 A 服务器监听 3000 端口,B 服务器监听 3001 端口,下面的所有示例都在同源政策的基础上改进,所以不会再做具体说明。):

<!-- A服务器响应的页面中的代码 -->
<body><h2>A服务器的页面</h2><!-- 全局作用域下定义fn1函数(用来处理接收数据) --><script>function fn1(data) {console.log(data);}</script><!-- 利用script标签发送跨域请求 --><script src="http://localhost:3001/jsonp1"></script>
</body>

如下图是响应的结果,想必从图中也你可以猜出了服务器端的代码。

在这里插入图片描述
在这里插入图片描述

// B服务器响应路由的代码
B.get('/jsonp1', (req, res) => {res.send('fn1({name: "tkop", age: 18})');
});

结合上面的代码示例总结 JSONP 的原理:

script 标签可以向服务器发送请求,最常见的是请求 js 脚本文件,客户端接收到服务器响应的 js 脚本文件后会立即执行。但是这个请求响应过程不受同源政策的限制,即使请求头信息中的 Sec-Fetch-Site 字段值不是 same-origin(同源),即使响应头中没有 Access-Control-Allow-Origin 字段,浏览器依旧会将服务响应的数据视为 js 脚本并在客户端运行。

JSOPN 正是利用了这一点,在客户端接收到响应前提前在全局作用域下声明一个用于接收处理响应数据的函数。再利用 script 标签发送请求,此时服务器端返回的不再是一个脚本文件,而是返回该函数的调用(函数调用的字符串)。并且将客户端需要的数据作为实参传递给函数。在客户端接收到响应后会将返回结果作为 js 脚本执行(调用返回的函数),这样就达到了获取并处理数据的目的。为了更加清楚原理我们再继续看看下面的示例代码。

<!-- A服务器响应的页面中的代码 -->
<body><h2>A服务器的页面</h2><script src="http://localhost:3001/jsonp1"></script><script>console.log(data);</script>
</body>

这次我不想直接在全局定义数据处理函数了,我想直接得到一个全局变量作为响应得到的数据(这个前后端交流成本非常高,而且还有后端开发者给你定义变量。。。。这是不可能的,纯粹是为了演示原理而已)。

// B服务器响应路由的代码
B.get('/jsonp1', (req, res) => {res.send('var data = "这就是响应数据"');
})

这就是上面的客户端代码即使没有声明 data 也不会报错的原因,返回的数据就是 data 声明

JSONP 模仿 ajax 请求

上面已经介绍了 jsonp 的基本原理,可以看出这种方案实际上已经不属于 ajax 请求的范围了。但是它可以模仿 ajax 请求,使用其原理 jsonp 是怎么模仿 ajax 请求的呢?

  1. 上面示例中的请求都是在页面加载过程就发送出去的,不像 ajax 根据客户端需要去发送。为实现这一点,我们可以在只有需要发送请求时在页面动态追加 script 标签。请求会在 script 标签被追加到页面时发送

  2. 发送请求接收到响应后在页面追加的 script 标签需要删除。如果不进行删除,每发送一次请求页面就会增加一个 script 标签。但是怎么判断 script 标签已经追加到页面且请求已经发送了呢?可以监听 script 标签的 onload 事件实现

  3. 另一个是函数名的问题。服务器返回的某函数的调用时,函数名必须与客户端声明的函数名称保持一致。这增加了前后端的沟通成本,不利于高效开发。为什么不在前端将函数名作为参数传递到后端呢?后端在获取到该参数后作为函数名并返回函数调用。

客户端代码如下:

<body><h2>A服务器的页面</h2><button id="btn">向B发送ajax请求</button><!-- 提前定义全局函数fn2 --><script>function fn2(data) {console.log(data);}</script><script type="text/javascript">var btn = document.getElementById('btn');btn.onclick = function() {// 创建script标签var script = document.createElement('script');// 设置script标签的src属性script.src = 'http://localhost:3001/jsonp2?callback=fn2';// 将script标签追加到页面中document.body.appendChild(script);// 在script标签追加完成请求发送后将其删除script.onload = function() {document.body.removeChild(script);}}</script>
</body>

服务器端代码如下:

B.get('/jsonp2', (req, res) => {var callback = req.query.callback;res.send(callback + '({ name: "tkop", age: 18 })')
})

上面的代码个人亲自敲完后运行没有出错,可以发送请求和获得响应数据,并达到预期结果。

JSONP 函数封装

JSONP 也可以像 ajax 那样将发送请求处理响应的过程封装成一个函数。在需要发送请求时,只需要调用并传入指定参数即可。参考 ajax 函数的封装过程,按照思考的步骤进行 JSONP 函数的封装。

  1. 请求地址、请求参数和响应处理函数可以作为函数的参数传入。但是由于 JSONP 方式只能发送 get 方式的请求,所以也就没有请求方式判断和请求参数格式不同等问题的处理(相比较于以前 ajax 函数的封装简单了不少)

  2. 响应处理函数作为参数传入 jsonp 请求函数,理想做法是以匿名函数的方式传入。如果是在全局作用域声明一个命名函数在将其传入 jsonp 请求函数,这是 jsonp 的封装性非常低

  3. 这样又会出现两个问题。问题 1:函数必须要挂载在全局作用域内,在后端返回函数调用时才可以找到相应的函数。问题 2:在发送请求时必须将函数名作为参数传递给后端

  4. 针对以上两个问题,可以直接在函数内部封装给函数命名和将其挂载在全局作用域下的代码

  5. 但是还是需要解决一个问题,就是不能在内部为所有请求的函数起同一个函数名,这样会造成函数覆盖的问题。例如给按钮 1 和按钮 2 都绑定点击事件,点击后均是发送 jsonp 请求,如果它们的响应处理函数命名相同,那无疑都会执行后面定义的那个函数

  6. 所以需要给每个函数的命名是随机而不同的

服务器端 jsonp 函数封装的过程和发送 jsonp 请求的代码

<body><h2>A服务器的页面</h2><button id="btn">向B发送jsonp请求</button><script type="text/javascript">var btn = document.getElementById('btn');function jsonp(options) {// 处理其他参数的拼接var params = '';for (var k in options.data) {params += '&' + k + '=' + options.data[k];}// 响应处理函数var fnName = 'myJsonp' + Math.random().toString().replace('.', '');window[fnName] = options.success;// 请求完整地址var url = options.url + '?callback=' + fnName + params;// 创建script标签var script = document.createElement('script');// 设置script标签的src属性script.src = url;// 将script标签追加到页面中document.body.appendChild(script);// 在script标签追加完成请求发送后将其删除script.onload = function() {document.body.removeChild(script);window[fnName] = null;}}btn.onclick = function() {jsonp({// 地址url: 'http://localhost:3001/jsonp3',// 其他请求参数data: {name: 'tkop',age: 18},// 响应处理函数success: function(data) {console.log(data);}})}</script>
</body>

服务器端的响应代码

B.get('/jsonp3', (req, res) => {var callback = req.query.callback;var data = JSON.stringify({ name: 'TKOP', password: 123456 });var result = callback + '(' + data + ')';res.send(result)
});

对服务器端的响应代码可以做一下优化,响应对象下有一个 jsonp() 方法封装了回调函数名的获取和与数据字符串拼接为函数调用的过程。所以使用这个方法的后端实现示例代码如下:

B.get('/jsonp3', (req, res) => {res.jsonp({ name: 'TKOP', password: 123456 });
});
// 响应结果
//typeof myJsonp07452599508118731 === 'function' && myJsonp07452599508118731({"name":"TKOP","password":123456});

写完验证没有问题后,我在想:这样岂不是每发送一个请求就会在全局挂载一个全局函数吗?需不需要在接收到响应并调用处理函数后将它删了?我觉得有必要,所以处理需要删除 script 标签外我另外添加了清除全局函数的代码。

在这里插入图片描述
封装结果如上图所示,发送几次请求后没有问题,如果试一下给其他按钮绑定点击事件发送 jsonp 请求的也没有发现问题。

其他方式

实现跨域的方式还有 CORS 和 WebSocket ,但是在此记录的是利用服务器作为中间人向其他服务器请求数据的方式。因为同源政策只是浏览器为保护用户信息安全而在客户端执行的策略,而服务器是没有同源政策的。浏览器不跨域向服务器请求数据时,服务器转而向其他服务器请求数据,在得到其他服务器响应的数据后转手响应给浏览器。这也隐式实现了跨域的数据请求。原理如下:

aaa: A 哥我希望得到这些数据。
A : 这些数据在 B 上,我去跟他要吧,你自己去要的话,他不会给你。
A : B 哥我要这些数据。
B : 给你。
A :aaa 这是你要的数据。

具体的实现服务器端需要使用到第三方库(request)向另一个服务器发送请求和接收处理响应。

// aaa 页面脚本向自己的服务器发送ajax请求
ajax({type: 'get',url: 'http://localhost:3000/server',success: function (data) {console.log(data);}
})// A服务器的响应路由
app.get('/server', (req, res) => {// 向B服务器发送请求request('http://localhost:3001/cross', (err, response, body) => {res.send(body);})
});// B服务器响应路由
app.get('/cross', (req, res) => {res.send('ok')
});

有关 CORS 和 WebSocket 有空再总结。。。。

这篇关于JSONP 方式实现跨域请求数据的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/898295

相关文章

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

SpringBoot中@Value注入静态变量方式

《SpringBoot中@Value注入静态变量方式》SpringBoot中静态变量无法直接用@Value注入,需通过setter方法,@Value(${})从属性文件获取值,@Value(#{})用... 目录项目场景解决方案注解说明1、@Value("${}")使用示例2、@Value("#{}"php

SpringBoot分段处理List集合多线程批量插入数据方式

《SpringBoot分段处理List集合多线程批量插入数据方式》文章介绍如何处理大数据量List批量插入数据库的优化方案:通过拆分List并分配独立线程处理,结合Spring线程池与异步方法提升效率... 目录项目场景解决方案1.实体类2.Mapper3.spring容器注入线程池bejsan对象4.创建

PHP轻松处理千万行数据的方法详解

《PHP轻松处理千万行数据的方法详解》说到处理大数据集,PHP通常不是第一个想到的语言,但如果你曾经需要处理数百万行数据而不让服务器崩溃或内存耗尽,你就会知道PHP用对了工具有多强大,下面小编就... 目录问题的本质php 中的数据流处理:为什么必不可少生成器:内存高效的迭代方式流量控制:避免系统过载一次性

Python的Darts库实现时间序列预测

《Python的Darts库实现时间序列预测》Darts一个集统计、机器学习与深度学习模型于一体的Python时间序列预测库,本文主要介绍了Python的Darts库实现时间序列预测,感兴趣的可以了解... 目录目录一、什么是 Darts?二、安装与基本配置安装 Darts导入基础模块三、时间序列数据结构与

Python使用FastAPI实现大文件分片上传与断点续传功能

《Python使用FastAPI实现大文件分片上传与断点续传功能》大文件直传常遇到超时、网络抖动失败、失败后只能重传的问题,分片上传+断点续传可以把大文件拆成若干小块逐个上传,并在中断后从已完成分片继... 目录一、接口设计二、服务端实现(FastAPI)2.1 运行环境2.2 目录结构建议2.3 serv

C#实现千万数据秒级导入的代码

《C#实现千万数据秒级导入的代码》在实际开发中excel导入很常见,现代社会中很容易遇到大数据处理业务,所以本文我就给大家分享一下千万数据秒级导入怎么实现,文中有详细的代码示例供大家参考,需要的朋友可... 目录前言一、数据存储二、处理逻辑优化前代码处理逻辑优化后的代码总结前言在实际开发中excel导入很

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

Nginx部署HTTP/3的实现步骤

《Nginx部署HTTP/3的实现步骤》本文介绍了在Nginx中部署HTTP/3的详细步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前提条件第一步:安装必要的依赖库第二步:获取并构建 BoringSSL第三步:获取 Nginx

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详