揭开JSONP的神秘面纱
/ / 点击 / 阅读耗时 10 分钟JSONP
JSON with padding (简称JSONP)是一种允许开发人员绕过(使用脚本元素的性质)浏览器强制执行的同源策略的技术。该政策禁止阅读任何来自不同网站的回复,这些网站的来源与目前使用的不同。
顺便说一句,该策略允许发送请求,但不允许读取请求的内容。
一个网站的来源由三个部分组成。首先, URI方案(https://), 主机名: 例如 www.baidu.com, 端口号: 80 或 443。
只要有其中任一部分不同,浏览器就认为是不同源。
如果想学习浏览器同源策略的更多内容,请点击这里
工作原理
假设我们在localhost:8000上,向JSON API的服务器发送请求。
1 | https://www.server.com/api/person/1 |
服务端响应是这样的:
1 | { |
由于众所周知的浏览器同源策略,请求被阻止,因为网站的起源和服务器不同。
通过将脚本src属性设置为API的URL,脚本将获取响应并在浏览器上下文中执行它。
1 | <script src="https://www.server.com/api/person/1" async="true"></script> |
但问题是,脚本元素自动解析并执行返回的代码。在本例中,返回的代码是上面显示的JSON片段。JSON将被解析为JavaScript代码,并因此抛出一个错误,因为它不是有效的JavaScript。
必须返回一段可运行的JavaScript脚本,以便浏览器能正确地解析和执行。如果我们将JSON代码分配给一个变量或将其作为参数传递给一个函数,JSON代码就可以正常工作—毕竟,JSON格式只是一个JavaScript对象。
因此,服务器可以返回JavaScript代码,而不是返回纯JSON格式。在返回的代码中,一个函数包装在JSON对象外面。函数名必须由客户机传递,因为代码将在浏览器中执行。通过查询参数中callback
作为包裹函数名。
同时,我们在全局(window对象上)上下文中创建一个函数,一旦解析并执行响应,就会调用这个函数。
1 | https://www.server.com/api/person/1?callback=callbackName |
1 | callbackName({ |
等价于如下:
1 | window.callbackName({ |
代码在浏览器的上下文中执行。函数将在全局作用域从脚本中下载的代码中执行。
为了让JSONP工作,客户机和服务器都必须支持它。虽然定义函数名称的参数没有标准名称,但是客户端通常会通过 callback
参数来传递这个函数名。
动手实现JSONP
1 | let jsonpID = 0; |
上面代码中,声明了一个名为 jsonpID
的变量—它将用于确保每个请求都有惟一的函数名。
首先,我们将对对象的引用保存在一个名为 head
的变量中。然后增加 jsonpID
以确保函数名是惟一的。函数返回一个 Promise
实例,通过创建一个 <script>
, jsonpCallback
和 jsonpID
组成的callbackName
。
然后,我们将脚本元素的 src
属性设置为指定的URL
。在服务端,我们将 callback
参数设置为callbackName
。
请注意,这个简化的实现不支持具有预定义查询参数的url,因此它不适用于https://logrocket.com/?param=true,因为我们要追加?最后再一次。
我们还将script 的 async
属性设置为 true
,确保不会阻塞页面的渲染。
最终会有三种可能的情况:
1、请求成功,并且执行window[callbackName]
,执行的 Promise
的 resolve
传入data.
2、script
标签发生异常,执行 Promise
的 reject
。
3、请求花费的时间超过预期时间,直接抛出超时错误。
1 | const timeoutId = window.setTimeout(() => { |
回调函数必须注册在 window
对象上,才能全局使用。在全局范围内执行一个名为callback()
的函数相当于调用window.callback()
。
通过在cleanup
函数中抽象清理过程,三个回调—超时、成功和错误侦听器—看起来完全相同。唯一的区别是他们是否解决或拒绝承诺。
1 | function cleanUp() { |
cleanUp
函数是为了在请求完成之后清理请求过程中产生的对象:该函数首先删除注册在窗口上的回调,该回调在成功响应时调用。然后从head中删除脚本元素并清除定时器对象。另外,只是为了确定,它将脚本引用设置为null,以便对其进行垃圾收集。
最后,我们将 <script>
标签添加到 head
, 会自动触发请求。
1 | jsonp('https://gist.github.com/maciejcieslar/1c1f79d5778af4c2ee17927de769cea3.json') |
总结结论
JSONP的原理很简单,只有有创造力的程序员才能想出来这种绕过限制的方法。JSONP是历史的用法,使用上存在一些限制条件,比如只能发送get请求和许多安全问题。
我们应该依赖于跨源资源共享(CORS)机制来提供安全的跨源请求。