前言
好记性不如烂笔头,所以学完跨域之后,我还是老实结合demo来整理一篇跨域实现方式的详记。当然,因为我现在学习阶段,并没有跨域的实战经历,所以这篇整理,纯粹停留在我对跨域的理解层面。
等后面接触到更多跨域知识,以及经历项目实战的跨域处理,有更透彻的进阶理解,就再做跨域知识的补充或跨域实战的记录。
这次说的跨域方式有四种:
1、JSONP
2、 CORS
3、 降域(document.domain)
4、 postMessage
一、为何要跨域——因为“同源策略”
1、什么是跨域
通俗来说,就是两个不同域名的网站的JavaScript脚本的交互。
常见交互有:一方发送请求要获取数据,对方响应并传输数据;操作网站页面的DOM元素等。
2、为何要跨域
现实场景中,肯定有很多时候是需要跨域请求、传输数据的。这些合理的用途会被浏览器默认阻止。
那么重点来了,为什么浏览器会默认阻止跨域操作呢?
因为所有浏览器都奉行“同源策略”。
同源策略:只允许同源的JS脚本(或者说接口)进行交互。不同源的情况下,不能读写对方的任何资源。
举例来说,http://www.jianshu.com
这个网址,http://
协议,www.jianshu.com
是域名,端口号不写的情况下默认是80
。
那么,同源需要满足下面三点,任何一点不同就视为不同源:
1、同协议:常见协议有http://、https://、file://(本地文件)、ftp://协议
2、同域名:比如www.example.com/dir2/other.html和www.example.com/dir/page.html
3、同端口号:URL默认不写端口,默认端口就是80。
(注意默认80端口和8080端口是不同的,两者不等同。)
同源策略是浏览器出于信息安全考虑,防止恶意网站窃取用户数据。
例如用户在登录某一网上银行网站后,又去登录其他恶意网站。如果没有同源策略,恶意网站的后台就可以获取网银网页的用户信息。因为浏览器对提交表单并没有同源策略的限制,假如用户登录网银后忘记退出登录。因为登录密码信息存储在cookie中,恶意网站就可以获取用户登录密码等信息,冒充用户,进行恶意操作。
二、使用Ajax跨域请求失败例子
在讲解跨域实现的方式之前,先来实例演示下,不使用任何跨域方式,只是纯粹ajax技术跨域或同域名请求数据时浏览器会做出的默认举动:
例子:当前页面显示3个新闻标题,点击“换一组”按钮,从指定接口获取数据,随机生成新的3个新闻标题。
index.html文件的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>ajax获取数据</title>
<style>
.container{
width: 900px;
margin: 0 auto;
}
</style>
</head>
<body>
<div class="container">
<ul class="news">
<li>随机变换新闻标题1</li>
<li>随机变换新闻标题2</li>
<li>随机变换新闻标题3</li>
</ul>
<button class="change">换一组</button>
</div>
<script>
function $(str){
return document.querySelector(str);
}
var change=$('.change');
/*ajax的写法*/
change.addEventListener('click',function(){
var xhr=new XMLHttpRequest();
xhr.open('get','http://bbhuangyh.com:8080/getNews',true); //指定接口获取数据
xhr.send();
xhr.onreadystatechange=function(){
if(xhr.readyState===4 && (xhr.status===200 || xhr.status===304)){
//将服务端返回的数据,作为自定义appendHtml()函数的参数
appendHtml(JSON.parse(xhr.responseText))
}
}
})
function appendHtml(news){
var html='';
for(var i=0; i<news.length;i++){
html+='<li>'+news[i]+'</li>';
}
//console.log(html);
$('.news').innerHTML=html;
}
</script>
</body>
</html>
在模拟的服务端,返回JSON格式的数据:
app.get('/getNews',function(req,res){
var news=[
"输入内容1",
"输入内容2",
"输入内容3",
"输入内容4",
"输入内容5",
"输入内容6",
"输入内容7",
"输入内容8",
"输入内容9"
]
var data=[];
for(var i=0;i<3;i++){
var index=parseInt(Math.random()*news.length); //随机选择news数组的下标
data.push(news[index]);
}
/*ajax的写法*/
res.send(data);
//console.log(data) //随机得到:[ '输入内容1', '输入内容4', '输入内容3' ]
})
1、同源域名登录的情况
设置host文件,让www.bbhuangyh.com和www.aahuangyh.com
等于127.0.0.1。然后使用http://bbhuangyh.com:8080/
域名登录。此时网页的域名和数据请求接口的域名xhr.open('get','http://bbhuangyh.com:8080/getNews',true)
相同。点击“换一组”按钮,就能从接口返回数据,获取替换页面数据,实现效果。
2、不同源域名登录的情况
数据请求接口的域名是'http://bbhuangyh.com:8080/getNews
,而现在使用localhost:8080
或者http://aahuangyh.com:8080/
这个完全不同的域名(即使实际都是指向127.0.0.1)来登录,点击“换一组”。这种情况下就是,不同源请求获取数据,浏览器会进行阻拦。点击“换一组”的操作无效。
浏览器返回的错误提醒:
XMLHttpRequest cannot load http://bbhuangyh.com:8080/getNews.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'http://localhost:8080' is therefore not allowed access.
三、跨域方法——JSONP
1、JSONP的原理
(1) <script> 标签是不受同源策略的限制的,它可以载入任意地方的 JavaScript 文件。
(2)对页面script元素的src指定接口路径。默认执行src所指路径,就会向后台发起跨域数据请求。
(3)在前端自定义会执行某项操作的函数。前端后台共同约定好这个自定义函数名。
(4)后端响应数据请求后,对响应数据进行包装:把响应数据转化为JSON格式的字符串,然后用约定好的函数名称包裹住。然后将这包装后的 JSON数据返回到前端的script标签中,作为JS语句在script中执行。这样恰好就可以调用执行约定好的函数,并且将数据作为参数传入。
通过这样的实现思路,其实就是把跨域的冲突给消解了,变成是执行获取script元素的src所指内容,而通过后台数据包装,src所指内容获取到的返回数据,刚好能够执行前端html定义的指定函数。所以在这种情况下,只要后端服务有打开,前端的html文件即使通过file协议在浏览器打开,也能正常执行获取数据。
适用情况:
(1)前后端间会有接触,能至少共同约定通过URL传递的参数名称(如例子中的callback,通过req.query.callback
得到函数名)或者是共同约定自定义的函数名称。
(2)JSONP只能发GET请求
2、JSONP的实例
前面讲解了原理,我们现在来看下具体的例子吧:
讲解这个跨域方法,我们还是使用前面点击“换一组”的例子demo。但JSONP的实现和Ajax没任何关系了,所以我们不再需要创建新的XMLHttpRequest对象。其本质是通过script标签的src属性来实现的,并且仍然需要服务器端支持,让src的link链接有数据返回。
对应的demo例子index.html文件代码改写如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>JSONP获取数据</title>
<style>
.container{
width: 900px;
margin: 0 auto;
}
</style>
</head>
<body>
<div class="container">
<ul class="news">
<li>随机变换新闻标题1</li>
<li>随机变换新闻标题2</li>
<li>随机变换新闻标题3</li>
</ul>
<button class="change">换一组</button>
</div>
<script>
function $(str){
return document.querySelector(str);
}
var change=$('.change');
// /*JSONP的写法*/
change.addEventListener('click',function(){
var script=document.createElement('script');
script.src='http://bbhuangyh.com:8080/getNews?callback=appendLi';
//callback为传给后端的参数,值为appendLi
//appendLi为约定好的函数名,可随意更改,只要和前端定义好的函数名一样就行
document.head.appendChild(script);
document.head.removeChild(script);
})
function appendLi(news){
var html='';
for(var i=0; i<news.length;i++){
html+='<li>'+news[i]+'</li>';
}
console.log(html);
$('.news').innerHTML=html;
}
</script>
</body>
</html>
在模拟的服务端,改写的代码:
app.get('/getNews',function(req,res){
var news=[
"输入内容1",
"输入内容2",
"输入内容3",
"输入内容4",
"输入内容5",
"输入内容6",
"输入内容7",
"输入内容8",
"输入内容9"
]
var data=[];
for(var i=0;i<3;i++){
var index=parseInt(Math.random()*news.length); //随机选择news数组的下标
data.push(news[index]);
}
var cb=req.query.callback;
//这里的callback是自定义的名字,跟随html的url请求里的自定义名称一样就行。
//优化做法:可以满足普通get方法获取,也可以jsonp方法获取
if(cb){
res.send(cb+'(' +JSON.stringify(data) + ')');
//JSON.stringify(data)转化为JSON格式的字符串
//console.log(cb+'(' +JSON.stringify(data) + ')')随机得到:appendLi(["输入内容8","输入内容6","输入内容4"])
}else{
res.send(data);
}
})
使用localhost:8080、aahuangyh.com或者file协议打开都能随机返回三条数据做显示,实现跨域:
四、跨域方法——CORS
前面举了个使用AJAX跨域失败的例子。而现在如果使用CORS的跨域方法的话,前面AJAX写法的demo,就能跨域生效。
CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。“它是W3C标准,是跨源AJAX请求的根本解决方法。”——引用
1、实现方法
CORS跨域方法的重点就在于,在后台代码上写上:res.header(“Accsess-Control-Allow-Origin”,“*”)
一般,使用ajax跨域请求时,浏览器会在请求头Request headers
加上“origin:发起请求的域名”
。
在后台代码加上res.header(“Accsess-Control-Allow-Origin”,“*”)
,这样会让浏览器在后端响应头Response headers
加上“Accsess-Control-Allow-Origin:*”
,*
号表示对任何域名发送的请求,都给予回应和答应。
2、兼容性
header('Access-Control-Allow-Origin:*')
是html5新增的一项标准功能。我们可以来看看CORS的兼容适用情况:
可以看到,IE11以下是不支持的。,无法兼容低版本的IE浏览器来使用。但胜在CORS用法简单,支持所有类型的HTTP请求。使用普通的XMLHttpRequest对象,用AJAX的写法就能实现跨域。
3、实例
来看实例demo吧:
还是前面AJAX跨域demo,html部分的写法相同,模拟后台部分的代码添加上这一句:
app.get('/getNews',function(req,res){
var news=[
"AAA",
"BBB",
"CCC",
"DDD",
"EEE",
"FFF",
"GGG",
"HHH",
"III"
];
var data=[];
for(var i=0;i<3;i++){
var index=parseInt(Math.random()*news.length);
data.push(news[index]);
}
//cors跨域写法
res.header("Access-Control-Allow-Origin","*");
res.send(data);
})
当然,也可以采用指定写法:res.header(“Accsess-Control-Allow-Origin”,“具体URL域名”)
,这样浏览器就只会对这个指定的URL域名的请求进行回应。
例如:
res.header("Access-Control-Allow-Origin","http://bbhuangyh.com:8080");
就是指定允许http://bbhuangyh.com:8080
来跨域访问xhr.open('get','http://aahuangyh.com:8080/getNews',true);
指定的这个接口资源
(4)其他
当然,还有其他一些配置。例如允许浏览器携带cookie来访问接口资源,后台要写:Access-Control-Allow-Credentials: true
,
对应的“Accsess-Control-Allow-Origin:*”
就不能是*号。而且前端部分的代码就需要配合写上:
var xhr=new XMLHttpRequest();
xhr.open('get',URL,true); //指定接口获取数据
xhr.withCredentials = true; //允许浏览器携带cookie来访问接口,前端配合要写这句
xhr.send();
xhr.onreadystatechange=handler;
五、跨域方法——降域
(1)适用情况
降域方法的实现是使用document.domain
,适用情景其实非常有限小众,常见于控制<iframe>窗口。比如当前页面下,有个iframe,其src地址是和我当前页面的网址域名不同域,但二级域名相同的情况下,而我需要对iframe的元素进行操作,这种场景下,我就使用降域方法。
(2)降域说明
什么是降域,实际举个例子,一目了然:
比如我当前页面的网址URL是:http://a.huangyh.com:8080/a.html <iframe>的src地址是:http://b.huangyh.com:8080/b.html
只有在两个域名是协议相同、端口相同、二级域名相同的情况下,使用document.domain="huangyh.com"
降域,都只取域名的huangyh.com
这部分,形同同域,然后在a.html和b.html都可以使用window对象对彼此进行dom操作。
(3)demo实例
在这个demo中,实现的是页面上有两个input,其中一个是iframe标签下打开的另一个页面的input。然后在其中任意一个input输入内容,另一个input会显示相同的内容。
(1)当前页面a.html的代码:点击查看
(2)iframe引用的b.html的代码:点击查看。需在服务端运行的情况下,查看效果。
通过对比来加深理解和印象:
对着两部分代码,可以先看看不降域,先设置同源的情况是如何:
(1)在当前页面的URL和iframe的src路径相同的情况下:
<body>
<div class="ct">
<h1>使用降域实现跨域</h1>
<div class="main">
<input type="text" placeholder="http://a.huangyh.com:8080/a.html">
</div>
<iframe src="http://a.huangyh.com:8080/b.html" frameborder="0"></iframe>
</div>
</body>
</html>
(2)window.frames[0].document.body.querySelector('#input'),就能够操作到右侧iframe窗口的input。因为本质是同源的。
(3)在左边Input输入值,右边input同步
现在,来修改iframe的src,与页面的URL不同域,就会报错:
<div class="main">
<input type="text" placeholder="http://a.huangyh.com:8080/a.html">
</div>
<iframe src="http://a.huangyh.com:8080/b.html" frameborder="0"></iframe>
这种情况下,就进行降域:
在两边的html的script中都加入“document.domain="huangyh.com"”
。结果两边的input输入都可以同步,实现页面URL和iframe的src不相同的情况下,也能跨域处理:
六、跨域方法——postMessage
(1)实现方式
HTML5中新增了window对象的window.postMessage方法,可以实现不同源的域名之间发送、监听消息。具体:
发送消息:windowObj.postMessage(message, targetOrigin);
- windowObj:需要接收消息的window对象
- message:发送的消息
- targetOrigin:接收消息的window对象所在的域名。可以是*号,也可以指定一个明确的URL
监听消息:使用监听事件message
- oragin:发送消息过来的域名
- data:发送的数据
- source:发送消息的window对象
(2)demo实例
简单说明之后,还是来看例子吧。
这里还是使用降域的这个例子,实现一样的操作效果。但换成postMessage的方式来做,对原有代码做一些改动:
a.html中的script部分:
<script>
document.querySelector('.main input').addEventListener('input',function(){
window.frames[0].postMessage(this.value,'*');
//window.frames[0]指iframe窗口
//postmessage是向外发送消息,把input输入的内容发送到是任何域名的iframe窗口上,因为有*号
})
window.addEventListener('message',function(e){
document.querySelector('.main input').value=e.data;
//自己所在的页面,监听消息,如果监听到消息,就把数据放到input上
})
</script>
b.html中的script部分:
<script>
document.querySelector('#input').addEventListener('input',function(){
window.parent.postMessage(this.value,'*')
//window.parent指iframe窗口的父窗口
//postmessage是向外发送消息,把input输入的内容发送到是任何域名的父窗口上
})
window.addEventListener('message',function(e){
document.querySelector('#input').value=e.data;
//自己所在的页面,监听消息,如果监听到消息,就把数据放到input上
})
</script>
实际在模拟后台运行后,打开a.html查看效果,结果在页面url和iframe的地址完全不同,也可以实现跨域数据互通:
后又后记:
这篇文断续写了挺久。写的时候,对跨域的认识相比学的时候更清晰了些。常翻常新。
参考文章:
https://segmentfault.com/a/1190000006908944
https://segmentfault.com/a/1190000003642057
http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html