跨域定义
协议、端口号、域名有一个不同就是跨域。
主域名相同,子域名不同也是跨域,emial.aa.com和time.aa.com就是主域名相同,子域名不同的跨域
协议不同或者端口号不同造成的跨域,前端无法解决
1、表单默认提交(get、post)、超链接访问域外的资源,这是允许的,因为在点击按钮/超链接时,浏览器地址已经变了,这就是一个普通的请求,不存在跨域;
2、ajax(借助xmlhttprequest)跨域请求,这是被禁止的,因为ajax就是为了接受接受响应,这违背了,不允许跨域读的原则
3、jsonp属于跨域读且形式限制为GET方式,它利用了script标签的特性;这是允许的。因为浏览器把跨域读脚本,当作例外,类似的img、iframe的src都可以请求域外资源
跨域的分类
1. 提交给后端跨域
jsonp get
iframe + form post
CORS 全类型
2. 前端跨域通信
2.1 父子页面通信
父子页面有两种,iframe嵌套的和window.open打开的。window.open打开的页面,在窗口模式时,被打开的页面就是当前页面的子页面,
tab模式时,只是形式不一样,也是子页面,可以通过window.opener来访问父页面。
iframe的通信方式: window.name, window.hash, postMessage
window.open的通信方式: postMessage
其中postMessage通信是最简单实现双向通信的方式。
2.2 两个独立页面通信
两个同域的独立页面通信可以使用localStorage,兄弟页面监听storage事件就行了。
两个跨域的独立页面可以使用一个birdge页面当做桥梁,做同域通信,两个页面分别用iframe嵌套它,用postMessage做跨域通信,就实现了两个跨域页面的通信。
具体如下图
postMessage storage postMessage
tab A <-------------------> iframe A [birdge.html] <-------------> iframe B [birdge.html] <------------------> tab B
跨域解决方法
1. jsonp(Get请求的跨域,安全性低)
1. 生成唯一函数名callback_uuid,在window对象上注册这个函数,window[callback_uuid] = function(){}
2. 将函数名callback_uuid发送到后端,后端生成数据JSON,拼装js文档callback_uuid(JSON),返回给客户端
3. 客户端将script标签插入到文档中,浏览器解析script标签,自动执行callback_uuid方法
4. 真正的callback函数是在callback_uuid中调用,而不是直接调用
缺点:
1. jsonp 在调用失败时,无法获得HTTP状态码
2. 只能支持GET请求
代码实现如下:
var jsonp = (function () { var num = 0; function foo(options) { let { url, params, callback } = options; num++; //通过闭包内的自增数字生成唯一的函数名 var jsonCallback = 'jsoncallback_' + num; //注册函数到全局对象上 window[jsonCallback] = function (data) { //清理回调函数名 window[jsonCallback] = null; //清理script标签 removeElement(jsonCallback); //调用真正的回调函数 callback && callback(data); } //插入script标签 var queryString = Object.keys(params).reduce((pre, cur) => { return pre + '&' + cur + '=' + option.params[cur]; }, ''); url += 'callback=' + jsonCallback + queryStirng; var script = document.createElement('script'); script.src = url; script.id = jsonCallback; document.getElementByTagName('head')[0].appendChild(script); } function removeElement(id) { var ele = document.getElementById(id), parent = ele.parentNode; if (parent && parent.nodeType != 11) { parent.removeChild(ele); } } return foo; }());
2. CORS(各种请求均可,IE8,9只能是GET和POST请求,通用性好)
3. iframe(传统POST的最佳选择)
3.1 iframe + form + 302 (post提交数据,url获取返回值 )
将<form>表单通过一个iframe来submit,将form的target属性设置为iframe的name,这样form的action URL就会在
iframe中打开,服务器返回的数据就会输出到iframe中。通过主页面和iframe的交互,完成对数据的读取。
举例:
<form action="http://www.b.com/io.php" method="POST" enctype="multipart/form-data" target="upload"> <input type="file" name="upload_file" /> <input type="hidden" name="backurl" value="http://www.a.com/receive" /> //注意这里! <input type="submit" value="开始上传" /> </form> <iframe name="upload" id="upload" style="display:none"></iframe> //name和id都设置为upload
提交到后端之后,直接通过302跳转的backurl,将返回结果加到backurl的查询字符串里面。backurl必须是和提交的页面
是同域的页面。这样iframe里面的页面可以通过window.top.callback(查询字符串参数)来调用父页面的方法,或者读取iframe
的src中的查询字符串
3.2 iframe + window.name
基本原理:设置了window.name的值后,页面刷新或跳转,window.name的值不发生变化。在页面的iframe的src设置为其他域的页面,
使用window.name赋值,然后跳转回和当前页面同域的一个代理页面,这时,可以从主页面取到iframe的name值,达到跨域传值的目的。
注意点:
1. window.name只能传字符串值,大小可达2MB
2. iframe的src为跨域页面时,是无法读取其name值的
代码实现:
function domainData(url, fn) { var isFirst = true; var iframe = document.createElement('iframe'); iframe.style.display = 'none'; var loadfn = function(){ if(isFirst){ //proxy.html为一个空文件 iframe.contentWindow.location = 'http://localhost:8000/proxy.html'; isFirst = false; } else { fn(iframe.contentWindow.name); //获取数据以后销毁这个iframe,释放内存;保证安全 iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe); iframe.src = ''; iframe = null; } }; iframe.src = url; if(iframe.attachEvent){ //IE8 iframe.attachEvent('onload', loadfn); } else { //other iframe.onload = loadfn; } document.body.appendChild(iframe); } //调用 domainData('http://localhost:8001/data.html', function(data){ alert(data); });
跨域的data.html页面内容
<script type="text/javascript"> window.name = "跨域得到的数据"; </script>
具体可参考: http://www.cnblogs.com/SherryIsMe/p/4752332.html
http://www.cnblogs.com/ibeisha/p/4059397.html
4. postmessage
postmessage和iframe配合可以实现向后端跨域post数据,并且将返回值传递给主页面。
postMessage发送消息
otherWindow.postMessage(message,targetOrigin);
otherWindow: 指目标窗口,是一个dom对象,是 window.frames
属性的成员或者由 window.open 方法创建的窗口
参数说明:
message: 是要发送的消息,类型为 String、Object (IE8、9 不支持Object),ie8,9只能发送字符串消息
targetOrigin: 是限定消息接收范围,一般是目标域名,不限制可使用 ‘*’
postMessage接收消息
//message函数中可以对消息的来源和数据的格式进行检查,增强安全性 var onmessage = function(){ var data = event.data, origin = event.origin; // 只获取需要的域,并非所有都可以跨域 if (event.origin != "need domain") { return false; } // 传输数据类型校验 if (typeof(data) !== 'object') { return false; } //handle data } if (typeof window.addEventListener != 'undefined') { window.addEventListener('message', onmessage, false); } else if (typeof window.attachEvent != 'undefined') { //for ie8 window.attachEvent('onmessage', onmessage); }
回调函数第一个参数接收 Event 对象,有三个常用属性:
data: 消息
origin: 消息来源地址
source: 源 DOMWindow 对象
具体例子
页面a和页面b跨域通信
页面1:www.a.com/a.html
页面2:www.b.com/b.html
页面代码:www.a.com/a.html
<iframe id="myIframe" src="http://www.b.com/b.html"></iframe>
<script> //依赖jquery var $myIframe = $('#myIframe'); // 注意:必须是在框架内容加载完成后才能触发 message 事件哦 $myIframe.on('load', function(){ var data = { act: 'article', // 自定义的消息类型、行为,用于switch条件判断等。。 msg: { subject: '跨域通信消息收到了有木有~', author: '跨域者' } }; // 不限制域名则填写 * 星号, 否则请填写对应域名如 http://www.b.com $myIframe[0].contentWindow.postMessage(data, '*'); }); // 注册消息事件监听,对来自 myIframe 框架的消息进行处理 window.addEventListener('message', function(e){ if (e.data.act == 'response') { alert(e.data.msg.answer); } else { alert('未定义的消息: '+ e.data.act); } }, false); </script>
页面代码:www.b.com/b.html
<script> // 注册消息事件监听,对来自 myIframe 框架的消息进行处理 window.addEventListener('message', function(e){ if (e.data.act == 'article') { alert(e.data.msg.subject); // 向父窗框返回响应结果 window.parent.postMessage({ act: 'response', msg: { answer: '我接收到啦!' } }, '*'); } else { alert('未定义的消息: '+ e.data.act); } }, false); </script>
更详细的例子可以参考:http://blog.csdn.net/qiqingjin/article/details/51326060
5. 后端代理或nginx代理
6. 图像ping
和JSONP一样的技术,只不过是利用img标签。img标签可以发送GET请求,但是无法获得响应数据,只能判断是否接收成功。非常适合追踪用户点击和在线广告曝光次数
let img = new Image() let count = 0; img.onload = img.onerror = function () { count++; } //src一设置,就会发送请求 img.src='https://www.test.com/index?data=test