不同頁面通訊與跨域

lhyt發表於2018-05-26

0. 前言

相信跨域有什麼手段,大家都背得滾瓜爛熟了。現在我們來做一些不在同一個tab頁面或者跨域的實踐。

1. localstorage

1.1 onstorage事件

localstorage是瀏覽器同域標籤共用的儲存空間,所以可以用來實現多標籤之間的通訊。html5出現了一個事件: onstorage,我們在window物件上新增監聽就可以監聽到變化: window.addEventListener('storage', (e) => console.log(e))

需要注意,此事件是非當前頁面對localStorage進行修改時才會觸發,當前頁面修改localStorage不會觸發監聽函式。如果實在是要,自己重寫一個方法吧,要不就在修改的時候把自己改的內容po上去。

示例: js:

if(!localStorage.getItem('a')){
	localStorage.setItem('a',1)
}else{
	var s = localStorage.getItem('a')
	localStorage.setItem('a',+s+1)
}
window.addEventListener('storage', (e) => console.log(e))
複製程式碼

我們新建兩個html分別叫1.html和2.html,並加上上面的js,於是我們每次開啟或者重新整理該頁面就會給a加上1。需要注意的是,如果是雙擊開啟,是在file://協議下的,而且不會觸發storage事件,但是會給a加上1,所以可以做一個功能,計算本地某個檔案被開啟了多少次。如果我們用伺服器開啟,我們的不同tab頁面通訊完成了,而且是實時的。

2. 玩轉iframe

我們都知道frame可以跨域,那麼我們來試一下。下面例子,都是一個html內嵌iframe,當然你直接開啟iframe那個檔案,沒什麼意義的

2.1 利用hash變化傳遞資訊實現父子視窗通訊(能跨域)

父視窗:1.html

html:

<iframe name="myIframe" src="http://localhost:1000/2.html"></iframe>
複製程式碼

js:

var originURL = myIframe.location.href
var i = document.querySelector('iframe')
i.onload= function(){//這是非同步載入的iframe
  i.src += '#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
}
複製程式碼

子視窗:2.html

window.onhashchange = function() {
    console.log(window.location.hash)
}
複製程式碼

我們開啟父視窗,發現子視窗的js已經跑起來了。

既然能跨域,我們直接雙擊開啟1.html,發現還是可以,這個例子雙擊開啟和伺服器開啟都能達到目的

2.2 父呼叫子頁面的js或者反過來呼叫

父調子:還是基於前面的條件

var i = document.querySelector('iframe')
i.onload= function(){
myIframe.window.f();
}
複製程式碼

子:2.html

function f(){
	console.log('this is 2.html')
}
複製程式碼

子調父: 子:2.html

parent.fn1()
複製程式碼

父:1.html

function fn1(s){
	console.log('this is 1.html')
}
}
複製程式碼

當然,你直接開啟2.html是沒意義的而且是報錯:Uncaught TypeError: parent.fn1 is not a function

這個需要注意,不能跨域,所以雙擊開啟以及不同域是報錯的:Uncaught DOMException: Blocked a frame with origin "null" from accessing a cross-origin frame.,只能伺服器開啟

2.3 window.name (能跨域)

類似於vue、react的prop父子傳值,只要在父視窗設定iframe標籤的name,在子視窗就可以讀到。

父視窗:1.html

<iframe  name="myIframe" src="http://localhost:1000/2.html"></iframe>
複製程式碼

子視窗:2.html

console.log(window.name)
複製程式碼

少年,放心地雙擊開啟吧,有效果的。

2.4 postmessage(能跨域)

H5之後為window新增了window.postMessage()方法,第一個引數是要傳送的資料,第二個引數是域名。

父視窗:1.html

<iframe id="test" name="myIframe" src="http://localhost:1000/2.html"></iframe>

//js
var frame = document.querySelector('iframe')
frame.onload = function(argument) {
	window.frames[0].postMessage('data from html1', '*');
}
複製程式碼

子視窗:2.html

window.onmessage = function (e) {
	console.log(e.data)
}
複製程式碼

可以跨域,所以能直接雙擊開啟可以看見效果。

上面的父子視窗,是指一個html裡面的iframe標籤引入另一個html。

3. 非同域的兩個tab頁面通訊

也就是兩個毫無關係的tab頁面通訊(比如我開啟一個baidu和一個github),怎麼通?

當然baidu和github能不能通訊,我們不知道,得問他們家的開發。前面我們已經知道,iframe能跨域,localstorage能使得兩個tab頁面通訊。那我們就來試一下,iframe橋接兩個互不相干的tab頁面。注意,bridge是一個html,其他兩個tab是指瀏覽器開啟的兩個html檔案。你可以另外建立兩個不同的html,也可以建立兩個一模一樣的html,然後雙擊開啟也好、伺服器開啟也好,有兩個就可以了。

下面,我們把橋接的iframe叫做bridge.html吧。我們用node開啟,監聽本地的1000埠。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <h1>hi</h1>
  </body>
  <script type="text/javascript">
window.addEventListener("storage", function(ev){
    if (ev.key == 'info') {
        window.parent.postMessage(ev.newValue,'*');
    }
});


window.addEventListener('message',function(e){
    // 接受到父文件的訊息後,廣播給其他的同源頁面
    localStorage.setItem('info',e.data);
});
  </script>
</html>
複製程式碼

我們再來兩個頁面,內容如下。接著我們可以以n種不同方式分別開啟,反正是非同源就可以了

<!DOCTYPE html>
<html>
<head>
	<title></title>
</head>
<body>
    <input id="ipt" type="text" name="">
    <button onclick="sub()">sub</button>
    <p id="cont"></p>
    <iframe src="http://localhost:1000/" style="display: none"></iframe>
</body>
<script type="text/javascript">
    var ipt = document.querySelector('#ipt')
    function sub(){
      document.querySelector('iframe').contentWindow.postMessage(ipt.value,'*');
      cont.innerHTML +='我:'+ ipt.value + '<br>'
      ipt.value = ''
    }
    window.addEventListener('message',function(e){
      if(e.data) cont.innerHTML +='對方:'+ e.data + '<br>'
      
    });
</script>
</html>
複製程式碼

然後一個簡單的聊天就搞定了,試試看。加上websocket的話,還可以非同源聊天呢,其他的可以自己隨意設定了。

1

從1到2的資訊實時傳遞更新就這樣子成功了,反之亦然。

4.MessageChannel

顧名思義,資訊通道。允許我們建立一個新的訊息通道,並通過它的兩個MessagePort 屬性傳送資料m,而且在 Web Worker 中可用。可以控制檯列印,發現有兩個屬性,portl1和port2。一個頁面內嵌與iframe最常用這種方法。

就像一條管道,一邊出一邊進,我們可以給postmessage方法加上第三個引數:

var channel = new MessageChannel();
channel.port1.addEventListener("message", function(e){
    window.parent.postMessage(e,'*',[channel.port2]);
    channel = null;
});
複製程式碼

深拷貝

n種不同的物件型別?環引用?怎麼做到特別容易的深拷?

然而真的做到了:

var obj ={a:1,b:2,c:{d:3,e:[{f:1,g:2}]},h:null}
obj.h = obj
var res 
new Promise(resolve => {
    var channel = new MessageChannel();
    channel.port2.onmessage = ev => resolve(ev.data);
    channel.port1.postMessage(obj);
  }).then(data=>{res = data})
複製程式碼

傳了資料給管道,管道傳回來一個長得一模一樣的資料回來,實現了深拷貝。我們叫他結構化克隆,能處理物件迴圈依賴和大部分的內建物件。比如postMessage發訊息給子視窗或者WebWorker的時候就會經常用到,拿到資料進行處理,但不汙染原資料。

相關文章