AJAX 与跨域通信(三):跨域解决方案

承接上文,继续补充跨域方案:postMessagelocation.hashWebSocket、Nginx 反向代理、Nodejs 中间件代理。

6.postMessage

HTML5 提供了 postMessageonmessage 两个 api 用于在跨域站点页面之间进行通信。

假设A域要向B域发送消息,那么:

  • 一方面,我们在A域页面中通过 iframe 引入B域,之后用B域的 window 对象调用 postMessage方法(谁接受消息,谁就去调用)。这个方法接受两个参数,第一个参数是发送的消息, 它可以是任何类型的数据,但部分浏览器只支持字符串格式;第二个参数是可以接受消息的域,如果不想限定某个域(比如B)去接受消息,那么可以传 *
  • 另一方面,B域监听 message 事件,一旦接收到消息就调用某个函数接受数据。message 事件的事件对象有三个属性,event.data 表示接受到的数据,event.origin 为消息发送方的源,event.source 为消息发送方的窗口对象的引用。

B域接收到了消息,要通知A域,其实就是上面的过程反过来。

B域要向A域发送消息,那么:

  • 一方面,B 域的 window.parent 可以访问父级(A域)窗口对象,我们在B域里通过该对象调用 postMessage 方法,发送通知给A域
  • 另一方面,A域监听 message 事件,收到B域通知,进行相应处理

核心代码如下:

http://test.com/a.html

<iframe id="myIframe" src="http://anothertest.com/b.html"></iframe>
<script>
    const iframe = document.querySelector('#myIframe');
    iframe.onload = function(){
        iframe.contentWindow.postMessage('我是数据','http://anothertest.com/b.html');
    }
    window.onmessage = function(event){
        console.log('我收到的B域通知是:'+ event.data); // 我收到的B域通知是:B域收到A域的消息了,通知你一声
    }
</script>
http://anothertest.com/b.html

<script>
    window.onmessage = function(event){
        console.log('我接受到的消息是:'+event.data); //我接受到的消息是:我是数据
        console.log('发送消息的源是:'+event.origin);
        console.log('发送消息的窗口对象是:'+event.source);
        window.parent.postMessage('B域收到A域的消息了,通知你一声','http://test.com/a.html');
    }
</script>

那么这就是简单的跨域窗口间通信了,不过这只是客户端层面上的,如果A域的客户端要发送 AJAX 请求给B域服务端呢?只要稍微改进上面的方法就可以,也就是说,B域客户端充当一个中转站,A 域客户端先通过上面的方法把数据发送给B域客户端,B域客户端再把数据转发给B域服务端(这两个是同源的,直接发送 AJAX 请求);然后,反过来也一样,B域返回的数据经由B域客户端交给A域客户端。

代码如下:

http://anothertest.com/b.html

<script>
    window.onmessage = function(event){
        const url = 'http://anothertest.com/test.php?msg=' + event.data;
        request(url);
    }
    // 发送请求
    function request(url){
        const xhr = new XMLHttpRequest();
        xhr.onreadystatechange = response;
        xhr.open('GET',url);
        xhr.send(null);
    }
    // 返回响应结果
    function response(){
        if(xhr.readyState == 4){
            if((xhr.status >= 200 && xhr.status <300) || xhr.status == 304){
                  window.parent.postMessage('服务端返回的数据是:'+ xhr.responseText,'http://test.com/a.html');
            }
        }
    }
</script>

另外还要关注安全问题。 postMessage 本质上是依赖于客户端脚本设置了相应的 message 监听事件,因此只要有消息通过postMessage发送过来,我们的脚本都会接收并进行处理 —— 而任何域都可以通过 postMessage 发送跨域信息,因此对于设置了事件监听器的页面来说,判断到达页面的信息是否安全是非常重要的。通常可以通过 event.origin 检测消息方是否在消息源白名单中。

7. location.hash

默认情况下,改变页面的 url 会导致页面跳转,但是 hash 是个例外,譬如将 http://test.com/a.html#hash 改为 http://test.com/a.html#anotherhash ,并不会引起页面跳转,所以我们可以利用 hash 来传输数据。

假设A域有 a.html 和 b.html,B域有 c.html,且 a.html 和 c.html 之间要进行跨域通信。

  • 一方面,我们在 a.html 中通过 iframe 引入 c.html,引用的 src 带上 hash —— 实际上这时候已经通过 hash 的方式把数据传给 c.html 了
  • 另一方面,在 c.html 中,我们对这个数据进行一些处理,之后想办法返回给 a.html。怎么返回呢?假定 a、c 同域,那么可以通过将数据赋值给 window.parent.location.hash 的方式,让 a.html 的 hash 改变,同时 a.html 监听这个改变,保存传过来的数据。但问题是,a、c 是不同源的,我们无法在 c.html 中通过 window.parent 去访问 a.html。那么谁能和 a.html 直接通信呢?肯定是和 a.html 同源的 html,因此我们想到,在 c.html 中利用 iframe 引入与 a.html 同源的 b.html,引用的 src 带上 hash —— 实际上这时候已经通过 hash 的方式把数据传给 b.html 了,而 b.html 拿到数据后,由于它和 a.html 是同源的,所以可以直接将数据赋值给 window.parent.parent.location.hash ,之后,a.html 监听 hash 改变,保存数据。

如下图所示:

下面我们看一下代码是怎么写的。

像前面说的,我们创建 iframe 引用 c.html,通过 hash 传值,同时监听 a.html 的 hash 改变 —— 这里有两种方式,我们可以直接用 onhashchange 监听,也可以设置一个定时器,每隔两秒轮询一次 hash,一旦改变就打印数据。

// a.html

<p>No changes yet</p>
<script>
var p = document.getElementsByTagName('p')[0];
var iframe = document.createElement('iframe');
iframe.src = 'http://localhost:3001/c.html#getdata';   // location.hash为'#getdata'
iframe.style.display = 'none';
document.body.appendChild(iframe);


// 原生 onhashchange 监听,有些浏览器不支持 onhashchange
window.onhashchange = function(){
    let data = location.hash ? location.hash.substring(1) : ''
    p.innerHTML = data;
}

//定时器轮询 hash
// function checkHash() {
//     let data = location.hash ? location.hash.substring(1) : ''
//     p.innerHTML = data;
// }
// setInterval(checkHash, 2000);   // 每隔2s监听hash值是否发生变化
</script>

这里我们根据不同的参数采取不同的处理,因为传过来的是 #getdata,所以调用 callBack 函数,函数首先尝试直接用 parent.location.hash 改变 a.html 的 hash,发现是不同源的,更改失败,改为将数据传给 b.html。

// c.html

<script>
switch(location.hash){
    case '#getdata':
        callBack();
        break;
    case '#getAnotherData':
        //do something……
        break;
}
function callBack(){
    var message = 'name=Sam';
    try {
        parent.location.hash = message;   // 因为不同域,这里通过 parent.location.hash 直接更改会报错
    } catch(e){
        // 采用 b.html 作为中转站
        var ifrproxy = document.createElement('iframe');
        ifrproxy.style.display = 'none';
        ifrproxy.src = 'http://localhost:3000/b.html#' + message; // 注意该文件在3000端口下
        document.body.appendChild(ifrproxy);
    }
}
</script>

由于 b.html 和 a.html 同源,所以可以直接更改 a.html 的 hash。更改后触发 a.html 中的事件,打印数据。

// b.html

<script>
    parent.parent.location.hash = self.location.hash;
</script>

location.hash 跨域的大致过程就是这样,当然,它的缺点也很明显:

  • 数据直接暴露在了 url 中
  • 数据容量和类型有限

8. WebSocket

传统的 http 协议有一个缺陷:通信只能由客户端发起,服务端无法主动向客户端推送信息。比如,服务端这边某个状态发生变化,它是无法主动通知客户端的,而只能由客户端采用轮询的方式,每隔一段时间发送一次请求进行探测。

这时候出现了一种新的叫做 WebSocket 的协议,它使用ws://(非加密)和 wss://(加密)作为协议前缀,特点在于支持双向通信 —— 客户端可以主动向服务端发送信息,服务端也可以主动向客户端推送信息 。那么这和跨域有什么关系呢?事实上,WebSocket 本身就不受同源策略的影响,这意味着,一旦客户端与服务端建立的是 WebSocket 连接,天然就可以实现跨域资源共享。

8.1 建立 WebSocket 连接

客户端要求升级至 WebSocket 协议:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

服务端同意升级:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

8.2 发起请求

同源策略限制了不同源之间无法发送 AJAX 请求,但是 WebSocket 发送的并不是 AJAX 请求,而是 WebSocket 请求。在了解怎么发起 ws 请求之前,先看一下一些相关属性。

  • WebSocket对象的readyState属性用来表示对象实例当前所处的连接状态,有四个值:
    • 0:表示正在连接中(CONNECTING);
    • 1:表示连接成功,可以通信(OPEN);
    • 2:表示连接正在关闭(CLOSING);
    • 3:表示连接已经关闭或打开连接失败(CLOSED);
  • 另外还有四个事件属性:
    • onopen:用于指定连接成功后的回调函数;
    • onclose:用于指定连接关闭后的回调函数;
    • onmessage:用于指定收到服务器数据后的回调函数;
    • onerror:用于指定报错时的回调函数;
  • 另外还提供了 bufferedAmount属性,表示还剩下多少字节的二进制数据没有发送出去
let ws = new WebSocket('ws://localhost:3001'); // ws://localhost:3000是响应请求的地址
ws.onopen = function (){
    console.log('连接成功,准备发送信息!');
    ws.send('发送信息');
}    
ws.onmessage = function (e){
    console.log('后端返回的是' + e.data);
}

不过, 原生的 WebSocket API 使用起来不太方便,可以使用 Socket.io,它很好地封装了 WebSocket 接口,提供了更简单、灵活的接口,也对不支持 WebSocket 的浏览器提供了向下兼容。

9.Nginx 反向代理

因为还没学习 Nginx,这里就先不写了。只做个记录,以后学习了再来补充。

10. Nodejs 中间件代理

原理和 nginx 相同,通过代理服务器,实现数据的转发 。

参考:

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏生信技能树

生存分析就是一个任人打扮的小姑凉

我这里选择最方便的 网页工具:https://xenabrowser.net/heatmap/ 选择合适的数据集及样本信息还有基因来演示一下,随便选择一个基因...

23340
来自专栏数据猿

什么将会替代 JavaScript 呢?

JavaScript 正在蓬勃发展。但由于 WebAssembly 的出现,它的衰落可能只是一个时间问题。

11020
来自专栏完美Excel

问与答68: 如何改变复选框颜色?

A:在Excel中有3种不同类型的复选框,包括:用户窗体中的复选框、表单控件中的复选框、ActiveX控件中的复选框。下面分别介绍如何修改它们内部颜色。

9620
来自专栏织梦Dedecms如何开启https

织梦Dedecms如何开启https安全协议?

现在越来越多的网站开始使用https协议,其实百度从2014年底就已经开始支持https了,并且据说在排名上,同权重的网站,开启https会优待提升排名。先不管...

11630
来自专栏包子铺里聊IT

Baozi Training Leetcode solution 103:BinaryTree Zigzag Traversal

Lime 小绿电动车2019年亏损$300M, 继Uber, Lyft, WeWork 之后,能不能赚钱还是王道呀。寒冬将至,地主家也没有余粮了。。。

7220
来自专栏芋道源码1024

一个 Java 对象到底有多大?

编写Java代码的时候,大多数情况下,我们很少关注一个Java对象究竟有多大(占据多少内存),更多的是关注业务与逻辑。但是殊不知,在我们不经意间,大量的内存被无...

10310
来自专栏盟主来了

19.9.29日报:怎样用make_css_property_names.py来生成CSSPropertyNames.cpp

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

9040
来自专栏业余草

BAT 大厂的大型网站页面静态化你了解吗?

我们小伙伴们在访问淘宝、网易等大型网站时有没有考虑到,网站首页、商品详情页以及新闻详情页面是如何处理的?怎么能够支撑这么大流量的访问呢?

10410
来自专栏web编程技术分享

第一讲 Html是个啥?

建议:视频只是作为知识点补充,不要一开始就直接看视频,最好是在阅读之后,带着疑问去看视频。

7410
来自专栏老码农的一亩三分地

IT兄弟连 HTML5教程 CSS3揭秘 CSS简介

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

5920

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励