理解跨域异步请求的 JSON-P
在开篇之前,我们也许知道跨域问题的存在,知道通过服务端开放跨域请求来使API实现跨域访问,甚至也知道JSON-P这种处理方式,然而可能只是基于阅读的基础上,未必有去做过实践,于是JSON-P只是纸上谈兵,的确很多文章都太不“直接了当”。现在,我尝试着用自己的方式来阐述一下,力求做到简明扼要,同时也是对自己知识储备的整理。
跨域
跨域就客户端而言是这样理解的:客户端(浏览器)为安全起见,特意设置了资源请求的门槛——“同源策略”,即不允许在当前域下访问其他来源的资源,这个“域”或“源”在浏览器里表现为实际请求的URL。也就是说,浏览器不允许客户端在访问A站的同时请求B站的资源。域名不同,IP不同,甚至端口号不同都被视为跨域,除非服务端通过HTTP头标识告知客户端放开这一限制。我们的目的是要越过这道坎。
JSON-P
JSON-P是绕过客户端同源策略的方法之一,实现跨域请求是需要前后端技术协作的,是对跨域异步请求的其他方法的补充。它并非一门技术,而是一种技巧。上文说到浏览器设置了同源请求的门槛,然而这种限制通常只针对cookie、本地存储、Ajax、跨域DOM获取等,对静态资源的请求并没有限制,比如在某个网站插入一段来自CDN提供的Bootstrap库文件:
<script src="//cdn.bootcss.com/bootstrap/4.0.0-alpha.5/js/bootstrap.min.js"></script>
src
指向的脚本可以被顺利的下载并执行。
JSON-P将通过这一特性来绕过浏览器的同源限制,配合后端返回加工过的内容以实现跨域请求。
实现
“JSON-P”全名JSON with Padding,很形象的诠释了JSON的获取方式(JSON数据以填充的方式来获取),我们用一个经典的例子来说明:
假设我们为 A站 后台设计一个API,允许第三方开发人员在 B站 通过这个API得到 A站 返回的形如JSON的数据{"name":"Warren","age":"28"}
。
这个API将会以文本形式返回一个带有入参的函数调用getUserInfo(data)
,把用户信息填充到入参中,于是最终API将会返回如下文本:
getUserInfo({"name":"Warren","age":"28"})
同时确保该函数(getUserInfo
)在 B站 客户端存在:
<script>
function getUserInfo(data){
alert('Your name is '+data.name);
alert('You\'re '+data.age+' years old.');
}
</script>
然后在 B站 页面上调用这个API:
<script src="http://a.com/api.js"></script>
当访问 B站 时就会依次弹出两个对话框。就这样,一个JSON-P请求完成了,它的确绕过了同源限制,实现在不同域间数据的交互。值得一提的是,客户端getUserInfo
函数里得到的data
实际上是JS的对象字面量(理论上可以是任何对象,这取决于服务端传递的内容),因为API返回的文本在浏览器上被当作JS代码解析并立即执行了,于是也就省去了JSON.parse()
的步骤。
进阶
发现了没?上面的实现方式和平时在本地写JS几乎一样呀!只不过调用getUserInfo()
函数的代码是通过后端书写并在客户端发送请求后返回的。那么问题来了,B站 开发人员希望借助这个API调取除用户信息之外更多的数据,比如天气、在 A站 的收藏……显然一个getUserInfo()
是不够的,比较灵活的做法是 A站 后端依据 B站 客户端发出的需求来决定返回什么样的数据,我们给调用API的链接添加查询参数,并把希望得到的数据类型告诉API:
<script src="http://a.com/api.js?getJSONP=getUserInfo"></script>
或者
<script src="http://a.com/api.js?getJSONP=getWeather"></script>
……
API根据查询参数getJSONP
的值判断客户端需要什么样的数据,填充进对应的函数并返回给客户端执行。这就能让一个API满足多种跨域请求。相应地,客户端还是免不了要设计对应的数据处理函数的。
异步
原理已经掌握,我们可以来点更有趣的,比如异步请求,聪明的读者一定猜到了——不就是动态创建script
标签或者改变已有标签的src
属性值么?没错!封装一个用原生JS创建script
标签的函数:
function getData(src){
var script = document.createElement('script');
script.src = src;
document.body.appendChild(script);
}
在任何时候调用它:
getData('http://a.com/api.js?getJSONP=getUserInfo');
建议在数据成功获取后保存数据到一个新的变量,并移除空闲的script
标签,最大限度的防治污染。
总结
跨域在某些需求领域算是门常用的技巧,JSON-P作为一种非标准的跨域请求实现有着先天的缺陷,它虽然巧妙,但实现起来繁琐怪异。一个现成的库来实现它再好不过了,比如JQuery的jsonp。然而仍难弥补只能接受GET请求和存在的安全隐患的缺点。未来JSON-P也许会被正式的跨域技术标准取代(比如CORS),但因为IE及其它老旧的浏览器的存在,新标准的超前使得JSON-P仍有存在的必要。