同源策略
浏览器基于用户隐私考虑,禁止不同源的网站之间相互调用AJAX,对于页面加载不同源的CSS样式表、图像和脚本等资源是可以进行跨域请求。这里说的同源策略是仅指浏览器而规定,用命令行的话并不存在同源策略。
所谓同源是指同协议、同域名和同端口。例如http://xxx.com与http://xxx.com就属于同源,后面接的路径可以不同。
这里说的禁止AJAX的跨域请求,并不是说浏览器不会向服务器发起请求,而是服务器不会把响应内容给浏览器。
CORS
CORS是一个W3C标准,全称为‘跨域资源共享’(Cross-origin resource sharing),实质就是跨域Http请求,它允许浏览器向跨域服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源之间发请求。
1.简单模式
当初,开发人员向浏览器厂商提出要求,让AJAX也能发起跨域Http请求,浏览器最后规定只要在服务器里的响应头添加一个字段,即Access-Control-Allow-Origin,这个字段接受的值为请求时的Origin字段,用来指定可接受某个域名的请求,也可以时*,表示接受任意域名的请求。1
response.setHeader("Access-Control-Allow-Origin","http://xxx.com")
在服务器里设置这段响应头,表示服务器会接受http://xxx.com的AJAX请求,并返回响应。如果Origin指定的源不在许可范围内,服务器会返回一个正常的HTTP回应,浏览器发现,这个回应的头信息没有Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。
例如:http://yyy.com想用AJAX请求http://xxx.com/style.css这个CSS样式表。JS代码如下:1
2
3
4
5
6let xhr = new XMLHttpRequest()
xhr.open("GET","http://xxx.com/style.css")
xhr.onload = function(){
console.log(xhr.responseText)
}
xhr.send()

如果无法在服务器里的响应头添加Access-Control-Allow-Origin片段,如http-server,可以在命令行写:http-server -c-1 --cors="Access-Control-Allow-Origin: http://xxx.com"
2.非简单模式
非简单请求是指对服务器有特殊要求的请求,
在简单模式下,默认CORS只能为GET和POST请求,若请求方法是PUT或DELETE,或者Content-Type字段类型为application/json,就必须在服务器添加一个响应头Access-Control-Allow-Methods:GET,POST,PUT,OPTIONS,DELETE,PATCH,HEAD,这个字段表示服务器支持的所有跨域请求的方法。1
2Access-Control-Allow-Origin: http://xxx.com
Access-Control-Allow-Methods: GET,POST,PUT,DELETE,PATCH,HEAD,OPTIONS
对于这种非简单模式,浏览器会发送两个请求。
从上图中可以看到,浏览器发送了两个AJAX请求,第一个请求可以成为’预检‘请求,请求方法是OPTIONS,是浏览器自动发的。浏览器询问目标服务器是否允许PUT请求,如果允许则会发第二次真正地PUT请求。
JSONP
JSONP表示JSON+Padding。如console.log({"name":"allen"}),其中console.log(表示左Padding,)表示右Padding。
我们知道浏览器可以发起<script src=http://xxx.com/xxx.js></script>脚本跨域请求。让目标网站的xxx.js里放置数据,其形式是JSON+Padding。1
2
3window.xxxUser = ({
"name":"allen",
"age":18})
对于非同源网站用JS动态使用script加载xxx.js脚本。JS代码如下:1
2
3
4
5
6
7let script = document.createElement('script')
script.src = 'http://xxx.com/xxx.js'
script.onload = function(){
console.log('加载xxx.js成功')
console.log(window.xxxUser)
}
document.head.appendChild(script)
这样便可以把xxx.js里的数据放在window对象下的xxxUser属性上,用DOM便可以调用了。
然而,这样存在一个问题,若window.xxxUser对象本来是有值的,这样会覆盖原来的值。同时我们并不知道xxx.js里的数据到底在那个window对象里。
我们可以通过传参,指定xxx.js里的数据放在哪个window对象里。
xxx.js:1
2
3
4window['{{callback}}'] = ({
"name":"allen",
"age":18
})
想发跨域请求的网站的JS代码如下:1
2
3
4
5
6
7window.allen = {}
let script = document.createElement('script')
script.src = 'http://xxx.com/xxx.js?callback=allen'
script.onload = function(){
console.log(window.allen)
}
document.head.appendChild(script)
这样便可以根据查询字符串的参数自定义window对象的属性名,在服务器后台,把读取的xxx.js代码里的占位符,替换为查询字符串的参数值`allen’。
当然,我们可以把传参改成用函数名。
xxx.js:1
2
3{{callback}}({
"name":"allen",
"age":18})
在后台把xxx.js里的变成以查询字符串的参数作为函数名,最后xxx.js里的代码变成了xxx({}),相当于调用了xxx函数,并传入了JSON字符串作为参数。于是可以不用写script.onload了,只要加载成功一定会调用这个xxx函数。相应的JS代码如下:1
2
3
4
5
6function xxx(data){
console.log(data)
}
let script = document.createElement('script')
script.src = 'http://xxx.com/xxx.js?callback=xxx'
document.head.appendChild(script)
这段代码还存在一个问题,若同时加载两个相同的script标签,你无法判断这两个哪个先加载成功。我们可以对上面的代码做下封装。1
2
3
4
5
6
7
8
9
10
11function jsonp(url,fn){
let functionName = 'allen' + parseInt(Math.random()*1000,10)
//把传的匿名函数放在window上
window[functionName] = fn
let script = document.createElement('script')
script.src = url + '?callback=' + functionName
document.head.appendChild(script)
}
jsonp('http://xxx.com',function(data){
console.log(data)
})
除了用函数封装,还可以用jQuery,代码如下:1
2
3
4
5$.ajax({
url: 'http://xxx.con/xxx.js',
dataType: 'jsonp',
success: function(data){console.log(data)}
})
AJAX与JSONP的区别
- AJAX是用JS发的请求,而JSONP是用
script标签。 - AJAX可以用任意的请求方法,而JSONP只能是GET请求,因为浏览器默认
script的请求只能是GET。 - JSONP不太安全,大家都可以访问,没有跨域限制,而AJAX有跨域限制。
- AJAX并不支持IE8及以下的浏览器,而JSONP可以支持老式浏览器,以及可以向不支持CORS的网站请求数据。