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

article/2025/9/30 19:21:42

1.前言

在前端面试中,想必每一个人都会被问到跨域相关的问题,背过八股文的小伙伴肯定对跨域的解决对答如流,常见的跨域解决方案在网上有很多整理,但是如果问到实现的细节,你是否能够手写实现或者深入解读呢?其实很多情况下,面试官不仅仅会考察第一层的概念,还会追问第二层、第三层内容,所以对于实现原理的掌握是必要的,今天笔者就整理一下跨域的基本方式,重点介绍jsonp的实现~

2.跨域方案

常见的跨域解决方案有8种左右,在面试中能答出4-5种就可以了~

2.1 如果只是想要实现主域名下的不同子域名的跨域操作,我们可以使用设置document.domain 来解决

document.domain 设置为主域名,来实现相同子域名的跨域操作,这个时候主域名下的 cookie 就能够被子域名所访问。同时如果文档中含有主域名相同,子域名不同的 iframe 的话,我们也可以对这个 iframe 进行操作。

2.2 使用 location.hash 的方法

我们可以在主页面动态的修改 iframe 窗口的 hash 值,然后在 iframe 窗口里实现监听函数来实现这样一个单向的通信。因为在 iframe 是没有办法访问到不同源的父级窗口的,所以我们不能直接修改父级窗口的 hash 值来实现通信,我们可以在 iframe 中再加入一个 iframe ,这个 iframe 的内容是和父级页面同源的,所以我们可以 window.parent.parent 来修改最顶级页面的 src,以此来实现双向通信。

2.3 使用 window.name 的方法

主要是基于同一个窗口中设置了 window.name 后不同源的页面也可以访问,所以不同源的子页面可以首先在 window.name 中写入数据,然后跳转到一个和父级同源的页面。这个时候父级页面就可以访问同源的子页面中 window.name 中的数据了,这种方式的好处是可以传输的数据量大。

2.4 使用 postMessage 来解决的方法

这是一个 h5 中新增的一个 api。通过它我们可以实现多窗口间的信息传递,通过获取到指定窗口的引用,然后调用 postMessage 来发送信息,在窗口中我们通过对 message 信息的监听来接收信息,以此来实现不同源间的信息交换。如果是像解决 ajax 无法提交跨域请求的问题,我们可以使用 jsonpcorswebsocket 协议、服务器代理来解决问题。

2.5 使用 jsonp 来实现跨域请求

它的主要原理是通过动态构建 script 标签来实现跨域请求,因为浏览器对 script 标签的引入没有跨域的访问限制 。通过在请求的 url 后指定一个回调函数,然后服务器在返回数据的时候,构建一个 json 数据的包装,这个包装就是回调函数,然后返回给前端,前端接收到数据后,因为请求的是脚本文件,所以会直接执行,这样我们先前定义好的回调函数就可以被调用,从而实现了跨域请求的处理。这种方式只能用于 get 请求。

2.6 使用 CORS 的方式

CORS 是一个 W3C 标准,全称是"跨域资源共享"。CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,因此我们只需要在服务器端配置就行。浏览器将 CORS 请求分成两类:简单请求和非简单请求。对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是会在头信息之中,增加一个 Origin 字段。Origin 字段用来说明本次请求来自哪个源。服务器根据这个值,决定是否同意这次请求。对于如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin 字段,就知道出错了,从而抛出一个错误,ajax 不会收到响应信息。如果成功的话会包含一些以 Access-Control- 开头的字段。非简单请求,浏览器会先发出一次预检请求,来判断该域名是否在服务器的白名单中,如果收到肯定回复后才会发起请求。

2.7 使用 websocket 协议,这个协议没有同源限制

2.8 使用服务器来代理跨域的访问请求

就是有跨域的请求操作时发送请求给后端,让后端代为请求,然后最后将获取的结果发返回。

3.jsonp详解

3.1 基本原理

Jsonp(JSON with Padding)json 的一种"使用模式",可以让网页从别的域名(网站)那获取资料,即跨域读取数据。在上文中已经说明,jsonp 的基本原理,主要就是利用了 script 标签的 src 没有跨域限制来完成的。

let a = 123;
this.document.getElementById('123')

3.2 执行过程

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

3.3 优缺点

缺点:只能进行GET请求,而且需要后端配合进行函数逻辑书写。

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

3.4 案例分析

先来看看我们要实现一个什么效果,在一个叫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标签,并执行了此次跨域请求。

前期准备

  • 我在本地新建一个文件夹node-cors
  • 并在此目录下npm init,初始化package.json
  • 安装koa(node的一个轻量级框架)
  • 新建文件夹jsonp,并新建index.htmlserver.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..."

前端简单实现OK👌,后端已经实现了,现在让我们来看看前端最简单的一种实现方式,也就是写死一个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端口,并且把idcb这两个参数放到URL里。那么后台就可以拿到URL里的这两个参数。

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

  3. 那么后端在拿到这两个参数之后,可能就会根据id来进行一些查询,当然,我这里只是模拟的查询,用了一个简单的find来进行一个查找。查找到id1的那项并且取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中的jsonp实现

上面👆我们介绍了用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回调中同样可以拿到数据。

3.5 完整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.1No.2中分别打印,而是都会在No.2中打印出结果。这是因为后面一个callbackJSONP里封装的第一个callback给覆盖了,它们都是共用的同一个callbackName,也就是jsonpCallback。如下所示:

image.png

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

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

callbackName是一个唯一的,可以使用递增
不要把回调定义在window中这样会污染全局变量,可以把它扔到JSON.xxx
OK👌,来看看改造之后的代码:

<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]

image.png

继续改进

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

我改一下后端的代码

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这个字符串,这样就导致我们获取到的数据不对了:

后台并不知道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>

注意⚠️:

encodeURIencodeURIComponent的区别:

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'

欢迎微信公众号【前端栈无不胜】
会定期推送Js、Vue、React、Node、算法、面试等大前端技术博客、精选文章!

在这里插入图片描述
也欢迎添加我的个人微信:
在这里插入图片描述


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

相关文章

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;真的…

Ubuntu18.04安装教程每一步都有、多图。(Win、Ubuntu双系统)

目录 一、安装准备二、制作启动盘三、安装四、遇到的问题 一、安装准备 1、一张SD卡或者U盘&#xff0c;容量8G或者以上。需要好的U盘&#xff0c;使用SD卡的读卡器也需要能正常使用的&#xff0c;不能会出现文件损坏错误&#xff0c;导致无法安装。 2、Ubuntu系统镜像源下载…

Ubuntu18.04安装教程

Ubuntu18.04安装教程 一、准备工作1.下载 Ubuntu 镜像2.制作U盘启动盘3.给 Ubuntu 分配硬盘空间 二、安装 Ubuntu18.041.设置启动项2.正式安装1&#xff09;选择语言2&#xff09;键盘布局3&#xff09;无线连网4&#xff09;更新选项5&#xff09;选择安装类型手动分区(重点) …

【Ubuntu-22.04.2新手安装教程】

新手安装教程 很多小伙伴在Ubuntu的安装上总会有疑问&#xff0c;今天就来给小可爱们来一套完整的Ubuntu快速安装教程 VMware Workstation Pro安装 本多的VMware Workstation Pro是从电脑管家那里安装的&#xff0c;具体怎么做小可爱就自己来吧&#xff0c;很简单的&#xf…