JSONP原理及实现

article/2025/9/30 19:53:06

文章目录

    • 基本原理
    • 执行过程
      • 优缺点
    • 案例分析
    • 前期准备
    • 后端代码
    • 前端简单实现
    • jQuery中ajax实现
    • 封装一个JSONP方法
      • 简易版
      • 同时多个请求
      • 最终版JSONP方法

基本原理

基本原理: 主要就是利用了 script 标签的src没有跨域限制来完成的。

执行过程

  1. 前端定义一个解析函数(如: jsonpCallback = function (res) {})
  2. 通过params的形式包装script标签的请求参数,并且声明执行函数(如cb=jsonpCallback)
  3. 后端获取到前端声明的执行函数(jsonpCallback),并以带上参数且调用执行函数的方式传递给前端
  4. 前端在script标签返回资源的时候就会去执行jsonpCallback并通过回调函数的方式拿到数据了。

优缺点

缺点:

  • 只能进行GET请求

优点:

  • 兼容性好,在一些古老的浏览器中都可以运行

案例分析

先来看看我们要实现一个什么效果:

在一个叫index.html的文件中有以下代码:

<script type='text/javascript'>window.jsonpCallback = function (res) {console.log(res)}
</script>
<script src='http://localhost:8080/api/jsonp?id=1&cb=jsonpCallback' type='text/javascript'></script>

然后我本地有一个文件server.js它会使用node提供一个服务,来模拟服务器。

并且定义一个接口/api/jsonp来查询id对应的数据。

当我打开index.html的时候就会加载script标签,并执行了此次跨域请求。

前期准备

  1. 我在本地新建一个文件夹node-cors
  2. 并在此目录下npm init,初始化package.json
  3. 安装koa(node的一个轻量级框架)
  4. 新建文件夹jsonp,并新建index.html和server.js,一个写前端代码,一个写后端
mkdir node-cors && cd node-cors
npm init
cnpm i --save-dev koa
mkdir jsonp && cd jsonp
touch index.html
touch server.js

后端代码

由于JSONP的实现需要前后端配合,先来写一下后端的实现:

(看不懂没关系,下面的前端简单实现会做解释)

const Koa = require('koa');
const app = new Koa();
const items = [{ id: 1, title: 'title1' }, { id: 2, title: 'title2' }]app.use(async (ctx, next) => {if (ctx.path === '/api/jsonp') {const { cb, id } = ctx.query;const title = items.find(item => item.id == id)['title']ctx.body = `${cb}(${JSON.stringify({title})})`;return;}
})
console.log('listen 8080...')
app.listen(8080);

写完之后,保存。

并在jsonp这个文件夹下执行:

node server.js

来启动服务,可以看到编辑器的控制台中会打印出"listen 8080…"

前端简单实现

后端已经实现了,现在让我们来看看前端最简单的一种实现方式,也就是写死一个script并发送请求:

index.html中:

<script type='text/javascript'>window.jsonpCallback = function (res) {console.log(res)}
</script>
<script src='http://localhost:8080/api/jsonp?id=1&cb=jsonpCallback' type='text/javascript'></script>

这两个script的意思是:

  • 第一个,创建一个jsonpCallback函数。但是它还没有被调用
  • 第二个,加载src中的资源,并等待请求的内容返回

整个过程就是:

  1. 当执行到第二个script的时候,由于请求了我们的8080端口,并且把id和cb这两个参数放到URL里。那么后台就可以拿到URL里的这两个参数。

  2. 也就是在后端代码中的const { id, cb } = ctx.query这里获取到了。

  3. 那么后端在拿到这两个参数之后,可能就会根据id来进行一些查询,当然,我这里只是模拟的查询,用了一个简单的find来进行一个查找。查找到id为1的那项并且取title。

  4. 第二个参数cb,拿到的就是"jsonpCallback"了,这里也就是告诉后端,前端那里是会有一个叫做jsonpCallback的函数来接收后端想要返回的数据,而后端你只需要在返回体中写入jsonpCallback()就可以了。

  5. 前端在得到了后端返回的内容jsonpCallback({“title”:“title1”}),发现里面是一段执行函数的语句,因此就会去执行第一个script中的jsonpCallback方法了,并且又是带了参数的,所以此时浏览器控制台会打印出{ title: ‘title1’ }

以此来达到一个简单的跨域的效果。

其实你想想,如果我们把第二个script标签换成以下代码,是不是也能达到同样的效果呢?

<!-- <script src='http://localhost:8080/api/jsonp?id=1&cb=jsonpCallback' type='text/javascript'></script> -->
<script type="text/javascript">jsonpCallback({ title: 'title1' })
</script>

jQuery中ajax实现

上面我们介绍了用script标签来实现,在jQuery的$.ajax()方法其实也提供了jsonp。

让我们一起来看看:

<script src="https://cdn.bootcss.com/jquery/3.5.0/jquery.min.js"></script>
<script>$.ajax({url: "http://localhost:8080/api/jsonp",dataType: "jsonp",type: "get",data: {id: 1},jsonp: "cb",success: function (data) {console.log(data);}});
</script>

在success回调中同样可以拿到数据。

封装一个JSONP方法

(此章会一步一步教你如何封装一个比较完美的JSONP方法)

简易版

先看下我们要实现的功能

定义一个JSONP方法,它接收四个参数:

  • url
  • params
  • callbackKey:与后台约定的回调函数是用哪个字段(如cb)
  • callback:拿到数据之后执行的回调函数
<script>function JSONP({url,params = {},callbackKey = 'cb',callback}) {// 定义本地的一个callback的名称const callbackName = 'jsonpCallback';// 把这个名称加入到参数中: 'cb=jsonpCallback'params[callbackKey] = callbackName;//  把这个callback加入到window对象中,这样就能执行这个回调了window[callbackName] = callback;// 得到'id=1&cb=jsonpCallback'const paramString = Object.keys(params).map(key => {return `${key}=${params[key]}`}).join('&')// 创建 script 标签const script = document.createElement('script');script.setAttribute('src', `${url}?${paramString}`);document.body.appendChild(script);}JSONP({url: 'http://localhost:8080/api/jsonp',params: { id: 1 },callbackKey: 'cb',callback (res) {console.log(res)}})
</script>

这样写打开页面也可是可以看到效果的。

同时多个请求

上面我们虽然实现了JSONP,但有一个问题,那就是如果我同时多次调用JSONP:

JSONP({url: 'http://localhost:8080/api/jsonp',params: { id: 1 },callbackKey: 'cb',callback (res) {console.log(res) // No.1}
})
JSONP({url: 'http://localhost:8080/api/jsonp',params: { id: 2 },callbackKey: 'cb',callback (res) {console.log(res) // No.2}
})

可以看到这里我调用了两次JSONP,只是传递的参数不同。但是并不会按我们预期的在No.1和No.2中分别打印,而是都会在No.2中打印出结果。这是因为后面一个callback把JSONP里封装的第一个callback给覆盖了,它们都是共用的同一个callbackName,也就是jsonpCallback。如下所示:

在这里插入图片描述

两次结果都是从76行打印出来的。

所以我们得改造一下上面的JSONP方法:

  • 让callbackName是一个唯一的,可以使用递增
  • 不要把回调定义在window中这样会污染全局变量,可以把它扔到JSON.xxx中

来看看改造之后的代码:

<script>function JSONP({url,params = {},callbackKey = 'cb',callback}) {// 定义本地的唯一callbackId,若是没有的话则初始化为1JSONP.callbackId = JSONP.callbackId || 1;let callbackId = JSONP.callbackId;// 把要执行的回调加入到JSON对象中,避免污染windowJSONP.callbacks = JSONP.callbacks || [];JSONP.callbacks[callbackId] = callback;// 把这个名称加入到参数中: 'cb=JSONP.callbacks[1]'params[callbackKey] = `JSONP.callbacks[${callbackId}]`;// 得到'id=1&cb=JSONP.callbacks[1]'const paramString = Object.keys(params).map(key => {return `${key}=${params[key]}`}).join('&')// 创建 script 标签const script = document.createElement('script');script.setAttribute('src', `${url}?${paramString}`);document.body.appendChild(script);// id自增,保证唯一JSONP.callbackId++;}JSONP({url: 'http://localhost:8080/api/jsonp',params: { id: 1 },callbackKey: 'cb',callback (res) {console.log(res)}})JSONP({url: 'http://localhost:8080/api/jsonp',params: { id: 2 },callbackKey: 'cb',callback (res) {console.log(res)}})
</script>

可以看到现在调用了两次回调,但是会分别执行JSONP.callbacks[1]和JSONP.callbacks[2]:

jsonp3.png

最终版JSONP方法

其实上面已经算比较完美了,但是还会有一个小问题,比如下面这种情况:

我改一下后端的代码

const Koa = require('koa');
const app = new Koa();
const items = [{ id: 1, title: 'title1' }, { id: 2, title: 'title2' }]app.use(async (ctx, next) => {if (ctx.path === '/api/jsonp') {const { cb, id } = ctx.query;const title = items.find(item => item.id == id)['title']ctx.body = `${cb}(${JSON.stringify({title})})`;return;}if (ctx.path === '/api/jsonps') {const { cb, a, b } = ctx.query;ctx.body = `${cb}(${JSON.stringify({ a, b })})`;return;}
})
console.log('listen 8080...')
app.listen(8080);

增加了一个/api/jsonps的接口。

然后前端代码增加了一个这样的请求:

JSONP({url: 'http://localhost:8080/api/jsonps',params: {a: '2&b=3',b: '4'},callbackKey: 'cb',callback (res) {console.log(res)}
})

可以看到,参数的a中也会有b这个字符串,这样就导致我们获取到的数据不对了:

jsonp1.png

后台并不知道a的参数是一个字符串,它只会按照&来截取参数。

所以为了解决这个问题,可以使用URI编码。

也就是使用:

encodeURIComponent('2&b=3')// 结果为
"2%26b%3D3"

只需要改一下JSONP方法中参数的生成:

// 得到'id=1&cb=JSONP.callbacks[1]'
const paramString = Object.keys(params).map(key => {return `${key}=${encodeURIComponent(params[key])}`
}).join('&')

来看一下完整版的JSONP方法:

<script>function JSONP({url,params = {},callbackKey = 'cb',callback}) {// 定义本地的唯一callbackId,若是没有的话则初始化为1JSONP.callbackId = JSONP.callbackId || 1;let callbackId = JSONP.callbackId;// 把要执行的回调加入到JSON对象中,避免污染windowJSONP.callbacks = JSONP.callbacks || [];JSONP.callbacks[callbackId] = callback;// 把这个名称加入到参数中: 'cb=JSONP.callbacks[1]'params[callbackKey] = `JSONP.callbacks[${callbackId}]`;// 得到'id=1&cb=JSONP.callbacks[1]'const paramString = Object.keys(params).map(key => {return `${key}=${encodeURIComponent(params[key])}`}).join('&')// 创建 script 标签const script = document.createElement('script');script.setAttribute('src', `${url}?${paramString}`);document.body.appendChild(script);// id自增,保证唯一JSONP.callbackId++;}JSONP({url: 'http://localhost:8080/api/jsonps',params: {a: '2&b=3',b: '4'},callbackKey: 'cb',callback (res) {console.log(res)}})JSONP({url: 'http://localhost:8080/api/jsonp',params: {id: 1},callbackKey: 'cb',callback (res) {console.log(res)}})
</script>

注意:

encodeURI和encodeURIComponent的区别:

  • encodeURI()不会对本身属于URI的特殊字符进行编码,例如冒号、正斜杠、问号和井字号;
  • 而encodeURIComponent()则会对它发现的任何非标准字符进行编码

例如:

var url = 'https://lindaidai.wang'encodeURI(url) // "https://lindaidai.wang"encodeURIComponent(url) // "https%3A%2F%2Flindaidai.wang"

另外,可以使用decodeURIComponent来解码。

decodeURIComponent("https%3A%2F%2Flindaidai.wang")
// 'https://lindaidai.wang'

http://chatgpt.dhexx.cn/article/UNxbXjr6.shtml

相关文章

什么是 JSONP?

前言 首先我们得先了解JSONP是怎么产生的。 最开始跨域请求数据没有现在方便&#xff0c;Ajax直接请求普通文件存在跨域无权限访问的问题&#xff0c;然后聪明的程序员想出了一套非官方的解决办法&#xff0c;程序员发现凡是带有“src”这个属性的标签都拥有跨域的能力&#x…

jsonp介绍

为什么要知道jsonp&#xff0c;jsonp的作用是什么&#xff1a; Jsonp(JSON with Padding) 是 json 的一种"使用模式"&#xff0c;可以让网页从别的域名&#xff08;网站&#xff09;获取资料&#xff0c;即跨域读取数据。原则上浏览器是不允许请求不同域名的数据的&…

前端跨域jsonp的细节,挡住面试官的连环提问

1.前言 在前端面试中&#xff0c;想必每一个人都会被问到跨域相关的问题&#xff0c;背过八股文的小伙伴肯定对跨域的解决对答如流&#xff0c;常见的跨域解决方案在网上有很多整理&#xff0c;但是如果问到实现的细节&#xff0c;你是否能够手写实现或者深入解读呢&#xff1f…

jsonp 的原理及应用

1. 什么是jsonp jsonp全称json with padding&#xff0c;填充式的json,jsonp是为跨域而生的 2. 那么有哪些标签可以跨域呢 <img src""> //图片 <link href""> //css <script src""> //程序我们可以使用script来帮助我们跨域…

JSONP详解

Jsonp(JSON with Padding) 是 json 的一种”使用模式”&#xff0c;可以让网页从别的域名&#xff08;网站&#xff09;那获取资料&#xff0c;即跨域读取数据。 为什么我们从不同的域&#xff08;网站&#xff09;访问数据需要一个特殊的技术(JSONP )呢&#xff1f;这是因为同…

使用JSONP解决跨域

1.首先需要知道什么是跨域 浏览器从一个域名的网页去请求另一个域名的资源时&#xff0c;域名、端口、协议任一不同&#xff0c;都是跨域 出于浏览器的同源策略限制 同源策略&#xff08;Sameoriginpolicy&#xff09;是一种约定&#xff0c;它是浏览器最核心也最基本的安全功…

虚拟机安装ubuntu全教程

主要流程 - 准备安装包&#xff08;包括ubuntu镜像、虚拟机压缩包、分区助手)安装虚拟机安装ubuntu、安装vmtool&#xff08;解决ubuntu全屏的问题&#xff09; 一、准备安装包 下载地址&#xff1a;http://pan.baidu.com/s/1hr39WGG 密码&#xff1a;tttq 二、安装虚拟机 …

ubuntu服务器ubuntu Server安装教程

记录一次系统安装到拷贝大数据文件的过程。 说在前: 1.系统U盘启动安装软件Rufus&#xff0c;自行百度下载2.下载Ubuntu Server镜像&#xff0c;官方地址即可一、安装 1、选择Ubuntu Server 2、语言选择&#xff0c;默认英语 3、有网络的话选择第一项升级系统&#xff0c;…

ubuntu安装图文教程

作为目前世界上最安全的操作系统&#xff0c;Linux逐渐被大多数人使用&#xff0c;而ubuntu作为Linux分支中最华丽美观的操作系统&#xff0c;有必要有一个好多安装教程 ubuntu系统是一个linux操作系统;ubuntu安装教程的每个版本类似&#xff0c;下面给您带来的是12.04版本的ub…

Ubuntu/Windows 双系统安装教程

前言 由于工作所用的开发环境是linux的&#xff0c;所以决定把自己电脑装一个windows/ubuntu双系统。Ubuntu不同版本的物理机安装流程都是一样的&#xff0c;而且极其简单&#xff0c;不要怕自己没装过把电脑整坏了&#xff0c;大不了连windows一起给它重装了。 双系统安装步…

安装Ubuntu系统详细教程

一. 前言 本篇文章详解介绍一下如何安装Ubuntu系统&#xff0c;笔者在安装的过程中踩过很多坑&#xff0c;重装了很多次&#xff0c;现在把安装过程中遇到的问题也列出来&#xff0c;供大家参考。 二. 准备工作 这个环节很重要&#xff0c;工欲善其事&#xff0c;必先利其器。 …

VMware虚拟机安装Ubuntu(超详细图文教程)

VMware虚拟机安装Ubuntu 1 Ubuntu下载2 打开VMware3 然后就可启动虚拟机4 等待吧5 重启后就完了&#xff0c; 到这就基本结束了6 下面可以调一下软件下载源 1 Ubuntu下载 Ubuntu下载地址&#xff1a;点这里 注&#xff1a;但官网下载比较慢 也可关注公众号Time木回复&#xf…

Ubuntu系统安装教程

1、首先打开VMware&#xff0c;然后点击创建新虚拟机 2、点击完新建虚拟机后&#xff0c;选择稍后安装操作系统&#xff0c;点击下一步 3、选择Linux&#xff0c;版本选择Ubuntu64位&#xff0c;点击下一步 4、虚拟机名称自己取&#xff0c;位置选择一个盘&#xff0c;创建一…

使用VMware安装Ubuntu虚拟机 - 完整教程

【前言】 本教程将演示通过 VMware 安装 Ubuntu &#xff0c;请提前下载好以下文件哦&#xff1a; ① VMware 软件 ② Ubuntu 的 光盘镜像文件&#xff08;.iso&#xff09; 【下载地址】 VMware 官网链接 https://www.vmware.com/ 本教程使用版本&#xff1a;VMware Worksta…

Ubuntu详细安装教程(小白友好型)

鼠标右键 ------>> 以管理员身份运行   2.右键“文件”——>>“新建虚拟机”   选择“自定义”&#xff0c;“下一步”         “下一步”         “稍后安装操作系统”——>>“下一步”         “Linux”——>>“Ubuntu64位”…

ubuntu 21.04安装教程

ubuntu 21.04安装教程 制作启动U盘*&#xff08;虚拟机安装此步省略&#xff09;*U盘刻录工具balenaEtcher 开始安装选择语言选择键盘布局网络设置代理设置源设置源设置为国内源&#xff0c;这里设置为阿里源&#xff1a; 分区设置用户名工具安装安装完成![在这里插入图片描述]…

Ubuntu安装教程【超多图】

大家好&#xff0c;我是坚果&#xff0c;我的公众号“坚果前端”&#xff0c; 文章目录 01前言02虚拟机的安装03Ubuntu镜像的下载04虚拟机硬件配置1.虚拟机安装完毕之后&#xff0c;界面如下图所示&#xff1a;2.在弹出的对话框中选择自定义&#xff0c;然后点击下一步&#x…

Ubuntu20.04安装详细图文教程(双系统)

Ubuntu安装 前言 最近想把自己开发环境换成linux的&#xff0c;查了一下还是ubuntu桌面比较美观并且作为生产系统生态良好&#xff0c;决定使用ubuntu。开始了着手查找安装Ubuntu双系统的方法。安装有三种&#xff1a; 虚拟机安装wubi安装U盘安装 第一种发挥不出硬件本身的…

新手安装 Ubuntu 操作系统步骤教程

新手安装 Ubuntu 操作系统 最近学习linux编程&#xff0c;需要安装一个 Ubuntu 操作系统&#xff0c;由于虚拟机的体验不是很好&#xff0c;所以便在电脑上试下装双系统。嘿嘿。话不多说&#xff0c;下面直接进入正题&#xff01; 1、下载 Ubuntu 操作系统 我们可以去官网下载一…

Ubuntu(Linux)虚拟机的安装教程(最为详细)

Linux&#xff08;Ubuntu&#xff09;虚拟机的安装教程&#xff08;最为详细&#xff09; 当年文鸯 “以匹马入数千骑中&#xff0c;辄杀伤百馀人&#xff0c;乃出&#xff0c;如此者六七&#xff0c;追骑莫敢逼。”,现在我阿猿七删七下Ubuntu&#xff0c;哭~~&#xff0c;真的…