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

6.postMessage

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

注意:这里一定要记住,不是发送端的窗口去调用 postMessage 方法,而是接收端的窗口去调用

举个例子,假设 A 域要向 B 域发送消息,那么:

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

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

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

  • 一方面,B 域的 window.parent 可以访问父级(A域)窗口对象,我们在 B 域里通过该对象调用 postMessage 方法,发送通知给 A 域
  • 另一方面,A域监听 onmessage 事件,收到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 相同,通过代理服务器,实现数据的转发 。

参考: