[前端原生技术]jsonp
【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://www.cnblogs.com/cnb-yuchen/p/18031965
出自【进步*于辰的博客】
在学习了Jsoup这个知识点之后,发觉js的这一特点真的很强大——动态解释。
本文以实用性的角度对Jsonp进行阐述,故在有些方面不是很详细或“不到位”,推荐一篇博文《jsonp原理详解——终于搞清楚jsonp是啥了》(转发)。
参考笔记三,P47。
1、jsonp是什么?
在学习jsonp之前,我们先来了解一个概念——同源策略。
“同源策略”是为了保证服务器的安全性,从而对客户端请求进行一定限制的规定,规定当客户端与服务端在协议、域名(IP地址)、端口都相同时才允许访问。
若有一种不同时仍进行访问,称之为“跨域访问”。
由于js文件的访问不受“同源策略”限制,故若将请求“模拟”成js请求(以引入js文件<script>
的形式发送请求,服务端响应js脚本),便可解决“跨域”问题,这种“模拟”或“格式约定”就是jsonp,它是一种非正式的传输协议。
示例。
1 <script src="http://127.0.0.1:8081/j1.js"></script>
除了js请求外,凡是具有src
属性的标签同样不受“跨域”限制。
示例。
1 <img src="http://127.0.0.1:8081/1.jpg">
2 <iframe src="http://127.0.0.1:8081/1.html"></iframe>
2、jsonp的原理
2.1 基本思路
从以上对jsonp的简述可以得出如下结论:
jsonp的原理是由客户端发出请求,服务端响应js脚本,再对js脚本进行“动态解释”。因此,客户端的请求方法任意(前提是客户端处理服务端响应的运行环境使用的是“解释”)。
注意:这里说的是“请求方法”(如:<script>
、ajax),不是“请求方式”(如:get、post)。
这个结论与上面简述有所不同,上面说的是“以引入js文件 <script> 的形式发送请求(下文简称“js请求”)”,而这里却是“任意请求方法”,为什么?而且这段总结看起来云里雾里,下面我会用一个个示例逐步进行说明。。。
大家注意一个关键词“动态解释”,什么意思呢?就是当服务端响应后,客户端对响应体进行解释,将其加入到当前上下文中,并同时执行。(这就是为何服务端响应必须是js脚本的原因)
可能不太好理解,举几个例子。
客户端仍是:
<script src="http://127.0.0.1:8081/j1.js"></script>
1、若服务端中j1.js
的代码是:
console.log("1'm js脚本.")
则会在控制台打印1'm js脚本.
。
2、若j1.js
的代码是:
var test = confirm('You need a js脚本?')
if(test)
console.log('1'm js脚本.')
else
console.log('I can't help it.')
则会弹出确认框,显示的消息是You need a js脚本?
。
3、若j1.js
的代码是:
var script = document.querySelector('script')
console.log(script.src)
则会在控制台打印http://127.0.0.1:8081/j1.js
。
You are OK。
客户端是通过<script>
标签发送js请求(写死了。。。),我们改一下。
var script = document.createElement('script')
script.src = 'http://127.0.0.1:8081/j1.js'
document.body.append(script)
这种形式有一个名称——“动态添加js脚本”。
PS:走到这一步,大家就知道jsonp是如何实现“跨域”了吧。。。
可是,如果仅仅是这样,并没有什么意义,因为我们要实现的是客户端与服务端之间的数据交互。
既然客户端都会对服务端响应的js脚本进行解释(动态解释),那么我们可以换一个思维。
服务端怎样能在js脚本中将数传递给客户端呢?一个典型的办法:函数。也就是这样:
客户端代码。
var script = document.createElement('script')
script.src = 'http://127.0.0.1:8081/j1.js'
document.body.append(script)
var handlerRes = function(res) {
console.log(res)
}
j1.js
的代码。
handlerRes('I"m js脚本')// 函数调用语句
// 在客户端中,handlerRes()称为“回调函数”
这样就将字符串'I"m js脚本'
传递给客户端了。
还不够,这只是实现了服务端 → 客户端的数据传递,若要实现客户端 → 服务端的数据传递,则需要借助request
对象,这就需要在服务端搭建服务器。
这里服务器是通过Node.js中的
express
模块进行搭建,详述可查阅博文《JS服务端技术—Node.js知识点锦集》的第4项。
我们再改一下,也就是向服务器路由发送js请求。
客户端代码:
var script = document.createElement('script')
script.src = 'http://127.0.0.1:8081/g1?userid=1001'
document.body.append(script)
var handlerRes = function(res) {
console.log(res)
}
服务器在j1.js
中搭建,代码是:
const express = require('express')
const ser = express()
ser.get('/g1', (req, res) => {
var id = req.query.userid
console.log(id)// 打印:1001
})
ser.listen(8081, () => {
console.log('create3')
})
这样客户端 → 服务器的数据传递就实现了,可是服务器怎么响应呢?响应需要使用res
对象,难道是这样?
res.send(id)
没错,在F12的网络那里可以看到,响应是1001
。的确成功响应了,可是没有意义(客户端没反应。。。或者说客户端处理不了这个1001
)。怎么办呢?
前面提到,只要服务器响应js脚本,客户端就可以进行“动态解释”。那么我们就把这个1001
放进js脚本中,也就是这样:
res.send('handlerRes(' + id +')')
这样响应体就是'handlerRes(1001)'
,解释后打印1001
。
到这一步,基本功能实现了。。。既实现了客户端与服务器的数据交互(请求-响应),又解决了“跨域”问题。
2.2 扩展实现
综上所述,jsonp解决“跨域”问题的代码是这样:
客户端代码。
var script = document.createElement('script')
script.src = 'http://127.0.0.1:8081/g1?userid=1001'
document.body.append(script)
var handlerRes = function(res) {
console.log(res)
}
服务端代码。
const express = require('express')
const ser = express()
ser.get('/g1', (req, res) => {
var id = req.query.userid
res.send('handlerRes(' + id +')')
})
ser.listen(8081, () => {
console.log('create3')
})
虽然功能实现了,不过代码的整体情况差强人意。例如:
- 每次客户端都需要动态添加
<script>
;(有点“冗余”) - 服务器必须提前知道回调函数名是什么。
这样很不方便,我们可以再换个思维。。。在我们平时使用的技术中,哪一种跟上述这种情况类似?没错,ajax,于是客户端可以这样优化:
$.ajax({
url: 'http://localhost:8081/g1?userid=1001',
type: 'get',
success: res => {
console.log(res)
}
})
var handlerRes = function(res) {
console.log(res)
}
这样就出现了一个问题——跨域。因为这只是一个普通的ajax异步请求,而不是js请求,自然受“同源策略”限制。
无妨,我们在服务器加上这一条代码:
res.set('access-control-allow-origin', '*')// 允许任意请求
这是解决“跨域”问题的另一种方法(在此不讨论),不过,这样不就直接解决了“跨域”问题,与jsonp有什么关系?没错,的确没有直接关系。我这样写是为了便于大家看到效果,继续看。。。
现在第一个问题解决了,第二个问题如何解决?我们可以给请求加一个参数funName
,值为回调函数名。也就是这样:
客户端代码。
$.ajax({
url: 'http://localhost:8081/g1?userid=1001&funName=handlerRes',
type: 'get',
success: res => {
console.log(res)
}
})
var handlerRes = function(res) {
console.log(res)
}
服务端代码。
const express = require('express')
const ser = express()
ser.get('/g1', (req, res) => {
res.set('access-control-allow-origin', '*')
var id = req.query.userid
var fn = req.query.funName
res.send(fn + '(' + id +')')
})
ser.listen(8081, () => {
console.log('create3')
})
这样处理后,响应结果同上,即'handlerRes(1001)'
。既然响应体相同了,是不是效果也相同?完全不同。
因为上一种是js请求,客户端会对响应体中的js脚本进行“动态解释”,即'handlerRes(1001)'
会被“解释”为回调函数的调用语句,因此打印1001
;而ajax并不会对响应体进行处理,即会将'handlerRes(1001)'
视为字符串直接打印(这样客户端就无法处理1001
,所以还不行。。。)。
ajax与jsonp的区别在哪?就是是否会对响应体进行“动态解释”。也就是说,如果ajax也能实现“动态解释”,这个优化就成功了。。。
还真有。由于ajax与jsonp这两种技术在调用方式和目的上都很相似,故jq把jsonp作为ajax的一种形式进行了封装,就是这样:
$.ajax({
url: 'http://localhost:8081/g1?userid=1001',
type: 'get',
dataType: 'jsonp',
jsonp: 'funName',
jsonpCallback: 'handlerRes',
success: res => {
console.log(res)
}
})
var handlerRes = function(res) {
console.log(res)
}
可见,多了三个属性dataType
、jsonp
和jsonpCallback
,大家对ajax都很熟悉了,dataType
是响应体的数据类型;后面两个大家可能没见过,它们的含义暂不多言(大家结合上文便可知是做什么的)。
同时将服务器中的“跨域”设置去掉,最终的结果是:
这样大家就明白了吧(请求行和响应体与上一个示例相同)。我们再看一下控制台:
???怎么突然就既实现了“动态解释”,又解决了“跨域”问题?
这就是dataType: 'jsonp'
的作用了,也就是jq在ajax上对jsonp的封装。即:
dataType: 'jsonp'
设置使得客户端将响应体视为js脚本进行“动态解释”,从而解决“跨域”问题。
PS:言至于此,大家都已经完全明白了在使用ajax请求时如何通过jsonp解决“跨域”问题了吧。。。
思考:
- 另外两个属性到底是做什么的?或者说有没有必要自定义?
- 为什么控制台打印了两次
1001
?
2.3 补充说明
上面我留下了两个思考,我先回答第二个,因为:
- 首先,客户端将响应的字符串
'handlerRes(1001)'
视为js脚本进行“动态解释”,结果为回调函数的调用语句,于是打印一个1001
; - 然后,设置了
dataType: 'jsonp'
的ajax请求可是说是js请求,但本质仍是ajax请求。成功的ajax请求自然会调用success(res)
,又打印一个1001
。
所以控制台打印了两个1001
。
旁白:我们IT人士普遍有一个“通病”——强迫症,或者说是“严谨”。
ajax请求既会调用success(res)
,也会调用回调函数(也就是有两个位置可以处理响应体),这就有点“不严谨”了。换言之,肯定有某种情况的配置可以做到只有一个位置。
这里唯一可以配置的地方就只有jsonp
和jsonpCallback
这两个属性,那我们就详细了解一个这两个属性:
jsonp的思想就是将响应的js脚本进行“动态解释”,从而添加进上下文,并执行,故需要js脚本中包含回调函数的调用语句。js脚本由服务器提供,那么服务器就必须知道回调函数名是什么,这个名称自然由客户端提供,那么,客户端在请求时会将回调函数名以某种形式(添加参数)封装在请求中(请求行),例如:
funName=handlerRes
。
jsonp
:值为回调函数名对应的请求参数名,默认值为'callback'
;jsonpCallback
:值为回调函数名,默认值为由jq生成的随机字符串。
大致介绍是这样,效果如下:(在都不配置的情况下)
现在我们写一个完整的示例。
客户端代码。
$.ajax({
url: 'http://localhost:8081/g1?userid=1001',
type: 'get',
dataType: 'jsonp',
success: res => {
console.log(res)
}
})
服务器代码。
const express = require('express')
const ser = express()
ser.get('/g1', (req, res) => {
var id = req.query.userid
var fn = req.query.callback
res.send(fn + '(' + id +')')
})
ser.listen(8081, () => {
console.log('create3')
})
Of Course!!控制台只打印了一个1001
,功能实现。
能不能再简化一点?这样:
ser.get('/g1', (req, res) => {
var id = req.query.userid
res.send('xx(' + id +')')
})
响应体是函数xx()
的调用语句,就是忽略客户端发来的回调函数名。看一下控制台:
报错了,因为找不到回调函数xx()
,这样客户端会认为这个ajax请求失败了,也就不会调用success()
(ajax还有一个属性error(e)
,大家可以自行测试)。所以:
若想
success(res)
中的res
能接收到响应数据(指服务器响应的js脚本中函数调用语句的实参),要求服务器响应的js脚本中的回调函数名必须与参数jsonpCallback
的值相同。
也就是这样:
$.ajax({
url: 'http://localhost:8081/g1?userid=1001',
type: 'get',
dataType: 'jsonp',
jsonpCallback: 'xx',
success: res => {
console.log(res)
}
})
或者都不指定(使用默认值),那么服务器响应的js脚本中的回调函数名与参数jsonpCallback
的值都是jQuery37107813896465729704_1702010749715
(随机字符串),故res
可成功接收响应数据。
吐槽:研究了半天,转了这么一大弯,最后就只是在ajax那里添加一个属性
dataType: 'jsonp'
就解决了“跨域”问题,这不消遣人嘛。。。哈哈,虽同归,但殊途,我们这么研究过来就对jsonp有了一个全面的认识,并且对前端原生的底层逻辑有了更深的了解,而不是“套用式”学习。
3、一个比较好的示例
虽然上面已经可以实现“跨域访问”,但有点单调了(服务器的响应数据是1001
,仅是一个数字)。我们来补充一下:
客户端代码。
$.ajax({
url: 'http://localhost:8081/g1?userid=1',
type: 'get',
dataType: 'jsonp',
success: res => {
try {
var user = JSON.parse(res)
console.log(user)
console.log('用户名:' + user.name)
} catch(e) {
console.log(res)
}
}
})
服务器代码。
const express = require('express')
const ser = express()
var users = [
{
id: 1001,
name: '进步',
pass: '2023'
}, {
id: 1002,
name: '于辰',
pass: '2021'
}
]
ser.get('/g1', (req, res) => {
var id = req.query.userid
var fn = req.query.callback
var result = users.find(item => {
if (item.id == id) {
var jsonstr = JSON.stringify(item)
res.send(fn + "('"+ jsonstr + "')")
return true
}
})
if(!result)// 未找到
res.send(fn + "('未找到')")
})
ser.listen(8081, () => {
console.log('create3')
})
控制台:
如果userid
为1003
,则:
这是不是大家期望的效果?
PS:至于为何要将对象(js对象 / json对象)转为json字符串
再进行响应,这一点我暂未作研究,似乎与“数据在网络传输时格式必须是字符串”有关。当然,也是可以将jsonstr
换成item
,只是客户端无法处理。
4、最后
首先,相信本文能帮助到大家。
在本文中,我写了很多个示例,大家可能看得有点晕。。。无妨,大家不用注意示例中具体的业务或目的,主要留意“前后变化”即可,关键是技术的运用,示例本身无关紧要。
jsonp其实很简单,就是实现对服务器响应的js脚本的“动态解释”,剩下的就是对js上下文的操作了。
js是一种“弱类型”的编程语言,在某种程度上说“很灵活”或者说“解释功能很强大”,所以“用函数调用语句将响应数据带回客户端”只是其中一种方法,至于其他方法需要大家自行扩展了。(本人暂且只想到这一种较好的方法,因此以这种方法作为阐述jsonp的示例)
本文完结。
[前端原生技术]jsonp的更多相关文章
- web前端技术与原生技术的竞争, 及未来的发展
用户界面领域: web技术与原生技术之争 除了浏览器中运行之外, html5的技术也在app领域和移动端的安卓, iOS, 以及桌面端的window, linux以及OS X展开了竞争. 同样属于用户 ...
- react 前端项目技术选型、开发工具、周边生态
react 前端项目技术选型.开发工具.周边生态 声明:这不是一篇介绍 React 基础知识的文章,需要熟悉 React 相关知识 主架构:react, react-router, redux, re ...
- Web前端开发大系概览 (前端开发技术栈)
前言 互联网建立50多年了,网站开发技术日新月异,但web前端始终离不开浏览器,最终还是HTML+JavaScript+CSS这3个核心,围绕这3个核心而开发出来大量技术框架/解决方案. 我从2000 ...
- 通往成功的钥匙--Web前端开发技术
互联网是一个服务性行业,用户对网站良好的体验度,直接影响到网站的效果.无论你做了多少广告推广,没有用户体验度等于零.Web前端技术是为了解决用户体验度而诞生的.无论是百度.新浪.阿里巴巴等大型网站,还 ...
- 网易云通过KCSP认证,云原生技术实力再获认可
近日,网易云通过KCSP认证,正式成为CNCF官方认可的Kubernetes服务提供商,也标志着网易云在云原生领域的技术实力得到了业界认可. Kubernetes是第一个从CNCF毕业的开源项目,凭借 ...
- 《CNCF × Alibaba云原生技术公开课》知识点自测(一):第一堂“云原生”课
(单选)1.容器启动后,我会时常 SSH 进入到容器里然后写很多文件.请问这破坏了云原生理念了吗? A. 否 B. 是 (单选)2.云原生架构必须选型 Kubernetes 方案. A. 否 B ...
- CNCF 旗下首个为中国开发者量身打造的云原生课程,《CNCF x Alibaba 云原生技术公开课》即将上线
伴随着以 Kubernetes 为代表的云原生技术体系的日益成熟以及 CNCF 生态的逐渐壮大,“云原生”已然成为了未来云计算时代里一个当仁不让的关键词.但是,到底什么是“云原生”?云原生与 CNCF ...
- 重磅课程|《CNCF x Alibaba 云原生技术公开课》正式开讲!
到底什么是“云原生”?云原生与 CNCF.Kubernetes 是什么关系?作为云计算时代的开发者和从业者,我们该如何在“云原生”的技术浪潮中站稳脚跟,将云原生落地.实现个人的自我升级呢? 201 ...
- 牛年 dotnet云原生技术趋势
首先祝大家:新年快乐,牛年大吉,牛年发发发! 2020年的春节,新冠疫情使得全球业务停滞不前,那时候,没有人知道会发生什么,因此会议被取消,合同被搁置,项目被推迟,一切似乎都停止了.但是我们却见证了I ...
- 第3届云原生技术实践峰会(CNBPS 2020)重磅开启,“原”力蓄势待发!
CNBPS 2020将在11月19-21日全新启动!作为国内最有影响力的云原生盛会之一,云原生技术实践峰会(CNBPS)至今已举办三届. 在2019年的CNBPS上,灵雀云CTO陈恺喊出"云 ...
随机推荐
- Python异步编程原理篇之IO多路复用模块selector
selector 简介 selector 是一个实现了IO复用模型的python包,实现了IO多路复用模型的 select.poll 和 epoll 等函数. 它允许程序同时监听多个文件描述符(例如套 ...
- Centos7使用memtester测试内存
memtester http://pyropus.ca/software/memtester/ 下载并编译, 不用安装 wget http://pyropus.ca/software/memteste ...
- Python三次样条插值与MATLAB三次样条插值简单案例
1 三次样条插值 早期工程师制图时,把富有弹性的细长木条(所谓样条)用压铁固定在样点上,在其他地方让它自由弯曲,然后沿木条画下曲线,成为样条曲线. 设函数S(x)∈C2[a,b] ,且在每个小区间[x ...
- Go微服务框架go-kratos实战学习08:负载均衡基本使用
微服务框架 go-kratos 中负载均衡使用 一.介绍 在前面这篇文章 负载均衡和它的算法介绍,讲了什么是负载均衡以及作用.算法介绍. go-kratos 的负载均衡主要接口是 Selector,它 ...
- linux和unix中的IO模型总结
高性能网络IO编程模型 一.I/O模型简介 在一个 linux 操作系统中,一般分为用户空间和内核空间. 用户空间一般就是我们进行应用程序编程的地方. 内核空间就是 linux 操作系统自己运行的一些 ...
- Docker方式快速启动一个Redis实例
安装Redis有多种方式,除了可以通过各个平台的软件包工具安装外,还可以直接从源码安装. 但是,安装Redis可能会遇到一些这样的问题,比如: 1.网络环境比较差,下载耗时比较长 2.从源码编译安装时 ...
- 使用Gulp压缩静态资源
如果希望对在静态页面中引入的相关资源进行压缩(比如:CSS,JavaScript,图片等),可以使用Gulp实现. 当然,还可以使用其他打包工具,比如:Grunt,Webpack等等. Gulp是什么 ...
- 【LeetCode二叉树#19】有序数组转换为二叉搜索树(构造二叉树)
将有序数组转换为二叉搜索树 力扣题目链接(opens new window) 将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树. 本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个 ...
- nftables语法及例子
先上我自己实际测试通过的例子,用例子便于在实践中学习: # 0 --- 说明 ---下面例子中的单引号目的是为了避免nftable参数中的星号.花括号.分号等符号被shell展开解释掉了,导致nft命 ...
- kafka 工作流程及文件存储机制
1.kafka的数据存储 文件存储格式: .log 和 .index Kafka 中消息是以 topic 进行分类的, 生产者生产消息,消费者消费消息,都是面向 topic的. topic ...