浏览器跨域问题小体会-使用原生js跨域访问豆瓣api
2017年第一篇博客,从去年11月份开始到现在已经几个月没有动笔了。写这篇博客的契机是,有个哥们在自己的博客系统上想加载个人在豆瓣上的读书信息。而且
1. 不想使用ruby来做这个事情(后端请求),因为他觉得拿到信息后放到后台处理,最后再丢到前端去渲染的方式很挫。
2. 如果在浏览器端使用js来fetch对应数据的话会产生跨域问题。
OK,容我从下面几个方面来写写近期对跨域问题的理解
1. 怎样才算跨域?
2. 我们要如何发送跨域请求?
3. 如何跨域调用豆瓣的api来获取对应的图书数据?
一. 怎样才算跨域
跨域,其实简单地去理解就是不同域名之间的http请求。比如我朋友的豆瓣api是 https://api.douban.com/v2/book/user/119280372/collections
,我需要从浏览器通过js来获取这个url返回的数据,我使用比较传统的做法是用XMLHttpRequest
创建一个对象来做这个事情。
var req = new XMLHttpRequest()
req.open('GET', 'https://api.douban.com/v2/book/user/119280372/collections')
req.send(null)
好像很合理,但是如果我是在百度的首页做这个事情,浏览器会给我这个反馈
XMLHttpRequest cannot load https://api.douban.com/v2/book/user/119280372/collections. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://www.baidu.com' is therefore not allowed access.
表明这是一个跨域请求。犀牛书里面有个比较简单的判断请求是否跨域的方法就是同时判断需要访问的url的域名
以及端口号
,如果 (域名不同
|| 端口号不同
) 为逻辑true
, 则表示对这个url的请求是一个跨域请求。看上面的例子,首先域名就不一样了,马上就能够判断这是一个跨域请求了。
二. 如何发送跨域请求
这个时候我们会有疑问,我们<img>
这个标签不是可以获取到其他域名下的图片数据吗?非常正确
。我们可以给<img>
标签设置src
属性为https://unsplash.it/1000/150
来获取这个图片服务网站上面的图片数据,从程序员的角度去理解的话,它发送了一个GET
请求,但是它的局限性就在于,它就只能发送这个GET请求了,而且我们似乎不能做更多的事情了。
下面介绍两种方式
1. XMLHttpRequest
有些浏览器本身就支持XMLHttpRequest
所产生对象的跨域请求,我们还可以自定义需要的请求方法(GET, POST), 一般情况下是通过判断该对象是否具有withCredentials
这个属性来判断的。
var supprotCORS = (new XMLHttpRequest()).withCredentials !== undefined
>> undefined
supprotCORS
>> true
这样就表示我这个浏览器的XMLHttpRequest
所产生的对象是支持跨域请求的。但是问题来了,为什么刚才我用它来请求豆瓣的api的时候还会有跨域的错?
因为跨域请求还需要服务端支持,产生跨域请求的时候还是要选择值得信赖的
合作伙伴
。
举个离我们最近的例子。有些CDN网络它本身服务就是支持跨域请求的。我们可以做个实验。我们使用https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css
这个url来请求css资源。当然我们要发送的是跨域请求:
这里表示这个请求已经成功了,并且没有产生跨域问题,因为这个服务本身支持跨域请求的,具体怎么支持,这个要服务端的小伙伴自己摸索了。
2. JSONP
我们还是没有解决豆瓣上的跨域问题。之前同学解决的方式是使用下面代码
$.ajax({
type : "get", //jquey是不支持post方式跨域的
async: false,
url : "https://api.douban.com/v2/book/user/119280372/collections", //跨域请求的URL
dataType : "jsonp",
//传递给请求处理程序,用以获得jsonp回调函数名的参数名(默认为:callback)
jsonp: "callback",
//自定义的jsonp回调函数名称,默认为jQuery自动生成的随机函数名
jsonpCallback:"success_jsonpCallback",
//成功获取跨域服务器上的json数据后,会动态执行这个callback函数
success : function(json){
}
});
但是我表示我完全不知道它在说什么,jsonp是什么?直到最近看犀牛书刚好碰到这个跨域相关的内容。才稍微有点理解。
JSONP是用<script>
元素作为Ajax传输的技术。
咋一看,跟<img>
标签是不是有点儿像,他们都是可以通过设置src
属性对应的url来从服务端获取数据,而且他们本身是允许跨域的。但是他们却只能单向地从服务端获取数据。这就可以理解了为什么上述的jquery代码里面写着jsonp只支持跨域GET
请求了吧(<script>
标签其实是无法发送POST这类修改服务端数据的请求的)。然后,我们再回顾一下<script>
标签是怎么工作的:
1. 从URL获取对应的js脚本。
2. 浏览器运行对应脚本。
这样看来似乎我们就可以通过,把对应的豆瓣url放入到script标签的src属性里面。然后,添加这个标签到文档里面,它就会自动请求url并且获取对应的数据。
不过这里也是有问题的:
我们获取服务端返回的数据之后,浏览器会把对应的数据当作js执行,万一我们获取的数据不是js脚本,怎么办? 我们看下面的例子:
var script = document.createElement('script'); // 创建一个script标签
script.setAttribute('src': 'https://api.douban.com/v2/book/user/119280372/collections'); // 设置script标签的src属性为对应的url
document.body.appendChild(script); // 把script标签插入到body元素的最后,插入之后就会直接发起跨域请求。
我在浏览器端运行上述脚本,但是很不幸:
Paste_Image.png原因上面也说了,其实豆瓣返回的是json数据,我们script标签加载之后把它当作js来运行了,所以报错是可以理解的。
接下来咋办, 当服务端返回的是json
数据的时候,如果我们能够把json数据放进函数里面进行处理就好了。
当我们获得的数据是 {name: "lanzhiheng"}
的时候,我多么希望我能够通过:
callback({name: "lanzhiheng"})
来对返回的数据进行处理啊!!!
我们其实可以告诉服务器让它给我们返回一个JSONP响应,而不单单是JSON数据,常用的方式是在url后面添加一个?jsonp=callback
这样的查询字符串,形如
https://api.douban.com/v2/book/user/119280372/collections?jsonp=callback
然后我们返回的数据就会有个名为callback
的函数包裹着。当然想得美
! 这个东西既然是服务端支持,当然不会写死。除了jsonp还可能会有其他的参数名字,对于豆瓣而言,它是用callback来作为参数名的,为了避免混淆我把url改成下面这样
https://api.douban.com/v2/book/user/119280372/collections?callback=handleResponse
尝试在浏览器调用这个方法。
Paste_Image.pngAwesome,我们返回的json数据已经被一个handleResponse
函数包裹着,我们只需要提前对handleResponse进行定义,就可以对这堆json数据为所欲为了。回顾上面的jquery代码,也就不难理解jsonp
参数名对应的值为何是callback
了吧?
三. 如何跨域调用豆瓣的api来获取对应的图书数据
其实在介绍jsonp的时候已经基本上把核心代码都写出来了,最为核心的代码其实就是
var script = document.createElement('script'); // 创建一个script标签
script.setAttribute('src': 'https://api.douban.com/v2/book/user/119280372/collections'); // 设置script标签的src属性为对应的url
document.body.appendChild(script); // 把script标签插入到body元素的最后,插入之后就会直接发起跨域请求。
我们可以稍微把代码写得好一点,定义一个函数给他自动追加查询字符串。(犀牛书里面的对应例子精简版本)
var responseHandler; // 定义一个全局作用域的函数
function getJSONP(url, cb) {
if (url.indexOf('?') === -1) {
url += '?callback=responseHandler';
} else {
url += '&callback=responseHandler';
}
// 创建script 标签
var script = document.createElement('script');
// 在函数内部实现包裹函数,因为要用到cb
responseHandler = function(json) {
try {
cb(json)
} finally {
// 函数调用之后不管发生什么都要移除对应的标签,留着也没用
script.parentNode.removeChild(script);
}
}
script.setAttribute('src', url)
document.body.appendChild(script);
}
OK,现在测试一下这个函数,我们需要传入一个对返回数据进行处理的函数。我这里简单地把数据打印出来
Paste_Image.png很好, 结果就是我们需要的,我们已经可以把跨域请求所返回的JSON数据打印出来了,当然后续是否需要进行更多的操作这就取决于你了!