前端跨頁面通信

跨頁面通信主要分兩大類

  • 同源頁面間的跨頁面通信
  • 非同源頁面間的跨頁面通信

同源頁面間的跨頁面通信

1.BroadCast Channel(廣播式通信)

顧名思義,BroadCast Channel 會創(chuàng)建一個所有同源頁面都可以共享的(廣播)頻道,故其中某個頁面發(fā)送的消息可以被其他頁面監(jiān)聽到

如何使用?
  • 創(chuàng)建一個用于廣播的通信頻道
const bc = new BroadcastChannel('broadCast')

該構造函數(shù)接受一個 DOMString 作為 name,用以標識這個 channel。在其他頁面,可以通過傳入相同的 name 來使用同一個廣播通信。
這個 name 值可以通過實例的 .name 屬性獲得

console.log(bc.name)
// broadCast
  • 消息監(jiān)聽
    BroadCast Channel 創(chuàng)建完成后,就可以在頁面監(jiān)聽廣播的消息
bc.onmessage = function(e) {
    console.log('receive:', e.data);
};

對于錯誤也可以綁定監(jiān)聽

bc.onmessageerror = function(e) {
    console.warn('error:', e);
};

除了為 .onmessage 賦值這種方式,也可以使用 addEventListener 來添加 'message' 監(jiān)聽。

bc.addEventListener('message', function (e) {
     console.log('receive:', e.data);
})
  • 發(fā)送消息
    BroadCast Channel 實例也有一個對應的 postMessage 方法用于發(fā)送消息
bc.postMessage('hello')
  • 關閉廣播
    上面的代碼實現(xiàn)了多個頁面間的廣播通信,有時我們會希望取消當前頁面的廣播監(jiān)聽:
  1. 一種方式是取消或者修改相應的 'message' 事件監(jiān)聽
  2. 另一種更加簡便的方式就是使用 BroadCast Channel 實例為我們提供的 close方法。
bc.close()

兩者的區(qū)別在于,取消 'message' 監(jiān)聽只是讓頁面不對廣播消息進行響應,BroadCast Channel 仍然存在;而調(diào)用 close 方法會切斷與 BroadCast Channel 的連接,瀏覽器會嘗試回收該對象,在關閉后調(diào)用 postMessage 會報錯:Channel is closed,如果之后又在需要廣播,需要重新創(chuàng)建一個相同 name 的 BroadCast Channel。

  • 兼容性
    BroadCast Channel 是一個非常好用的多頁面消息同步 API, 但是兼容性卻不是很樂觀:
兼容性

2.LocalStorage

LocalStorage作為前端最常用的本地存儲,大家應該非常熟悉了,但是 StorageEvent 這個和它相關的事件我們可能不太常用到。

如何使用?
  • 消息監(jiān)聽
    當 LocalStorage 發(fā)生變化是,會觸發(fā) storage 事件。利用這個特性。我們可以在發(fā)送消息時,把消息寫入到某個 LocalStorage 中,然后在各個頁面內(nèi),通過監(jiān)聽 storage事件即可收到通知。
window.addEventListener('storage', function(e) {
  if (e.key === 'localStorageData') {
    console.log('[Storage I] receive message:', e.newValue)
  }
})
  • 發(fā)送消息
    在需要監(jiān)聽的各個頁面上添加以上代碼,即可監(jiān)聽到 LocalStorage 的變化。當某個頁面需要發(fā)送消息時,只需要使用我們熟悉的 setItem方法即可:
const data = {
    txt: 'hello',
    timeStamp: +(new Date)
}
window.localStorage.setItem('localStorageData', JSON.stringify(data));

這里有個細節(jié)就是我們在 data 上添加了一個時間戳,這是因為 storage 事件只有在值真正改變時才會觸發(fā),所以通過加時間戳來保證每次調(diào)用時一定會觸發(fā) storage 事件。

  • 兼容性
    LocalStorage的兼容性如下:
兼容性

3.Service Worker

Service Worker 是一個可以長期運行在后臺的 Worker, 能夠?qū)崿F(xiàn)與頁面的雙向通信,將 Service Worker 作為消息的處理中心(中央站)即可實現(xiàn)廣播效果。
Ps: Service Worker 也是 PWA的核心技術之一,但是本次分享重點不在 PWA,所以不做進一步展開。

如何使用?
  • 在頁面注冊 Service Worker (需先判斷當前瀏覽器是否支持 Service Worker ,避免由于瀏覽器不兼容導致的 bug )
if('serviceWorker' in window.navigator) {
    navigator.serviceWorker.register('./sw.js').then(function (registration) {
        console.log('success',registration)
    }).catch(function (err) {
        console.log('fail',err)
    })
  }

其中 sw.js 是對應的 Service Worker 腳本。Service Worker 本身并不自動具備“廣播通信”的功能,我們需要添加些代碼,將其改造成消息中轉(zhuǎn)站:

this.addEventListener('message', function (e) {
  e.waitUntil(
      this.clients.matchAll().then(function (clients) {
          if (!clients || clients.length === 0) {
              return;
          }
          clients.forEach(function (client) {
              client.postMessage(e.data);
          });
      })
  );
});

我們在 Service Worker 中監(jiān)聽了 message 事件,獲取頁面(從 Service Worker 的角度叫 client)發(fā)送的消息。然后通過 self.clients.matchAll() 獲取當前注冊了該 Service Worker 的所有頁面,通過調(diào)用每個 client 的 postMessage 方法,向頁面發(fā)送消息,這樣就把從一個頁面收到的消息通知給了其他頁面。

  • 消息接收
if('serviceWorker' in window.navigator) {
  navigator.serviceWorker.addEventListener('message',function (e) {
    console.log('[Service Worker] receive message:', e.data);
  })
}
  • 發(fā)送消息
    在向 Service Worker 發(fā)送消息時,我們需要在 serviceWorker 實例上調(diào)用 postMessage 方法,這里我們用到的是 navigator.serviceWorker.controller
if('serviceWorker' in window.navigator && navigator.serviceWorker.controller){
  navigator.serviceWorker.controller.postMessage('hello')
}
  • 兼容性
    如下圖所示, IE 和 Opera Mini 完全不支持,主流瀏覽器中 Edge 17以下不支持, Safair 和 IOS Safair 剛剛開始支持,而火狐和 Chrome 支持良好,大家在使用的時候最好還是做一下判斷。
兼容性
總結

上面我們看到的三種實現(xiàn)跨頁面通信的方式,不管是建立廣播頻道的 Broadcast Channel,還是使用 Service Worker 的消息中轉(zhuǎn)站,或者是監(jiān)聽 storage 事件,都是“廣播模式”:一個頁面將消息通知給一個中央站,再由“中央站”通知給各個頁面。
下面介紹的另外兩種跨頁面通信方式,本質(zhì)是“共享存儲 + 輪詢模式”。

4.Shared Worker

Shared Worker 在實現(xiàn)跨頁面通信的問題在于,它無法主動通知所有頁面,因此我們需要使用輪詢的方式來拉取最新的數(shù)據(jù)。

如何使用?
  • 在頁面啟動 Shared Worker
// 構造函數(shù)的第二個參數(shù)是 Shared Worker 名稱,也可以留空
const sharedWorker = new SharedWorker('../util.shared.js', 'ctc');
  • 完善 util.shared.js
let data = null;
this.addEventListener('connect', function (e) {
    const port = e.ports[0];
    port.addEventListener('message', function (event) {
        // get 指令則返回存儲的消息數(shù)據(jù)
        if (event.data.get) {
            data && port.postMessage(data);
        }
        // 非 get 指令則存儲該消息數(shù)據(jù)
        else {
            data = event.data;
        }
    });
    port.start();
});
  • 消息監(jiān)聽
    在需要監(jiān)聽的頁面定時發(fā)送 get 指令的消息給 Shared Worker,輪詢最新的消息數(shù)據(jù),并在頁面監(jiān)聽返回的的信息:
// 定時輪詢,發(fā)送 get 指令的消息
setInterval(function () {
  sharedWorker.port.postMessage({get: true})
}, 1000)

// 監(jiān)聽 get 消息的返回數(shù)據(jù)
sharedWorker.port.addEventListener('message',function (e) {
    console.log(e.data)
}, false)
sharedWorker.port.start()
  • 發(fā)送消息
sharedWorker.port.postMessage('hello');

注意:如果使用 addEventListener來添加 Shared Worker 的消息監(jiān)聽,需要顯示調(diào)用 MessagePort.start 方法,即上文中的 sharedWorker.port.start();如果使用 onmessage 綁定監(jiān)聽的話則不需要。

  • 兼容性
兼容性

5.IndexedDB

IndexedDB 與 Shared Worker 方案類似,消息發(fā)送方將消息存至 IndexedDB 中,接收方則通過輪詢?nèi)カ@取最新的信息。

如何使用?
  • 打開數(shù)據(jù)庫連接
function openStore() {
    const storeName = 'ctc_aleinzhou';
    return new Promise(function (resolve, reject) {
        if (!('indexedDB' in window)) {
            return reject('don\'t support indexedDB');
        }
        const request = indexedDB.open('CTC_DB', 1);
        request.onerror = reject;
        request.onsuccess =  e => resolve(e.target.result);
        request.onupgradeneeded = function (e) {
            const db = e.srcElement.result;
            if (e.oldVersion === 0 && !db.objectStoreNames.contains(storeName)) {
                const store = db.createObjectStore(storeName, {keyPath: 'tag'});
                store.createIndex(storeName + 'Index', 'tag', {unique: false});
            }
        }
    });
}
  • 存儲數(shù)據(jù)
function saveData(db, data) {
    return new Promise(function (resolve, reject) {
        const STORE_NAME = 'ctc_aleinzhou';
        const tx = db.transaction(STORE_NAME, 'readwrite');
        const store = tx.objectStore(STORE_NAME);
        const request = store.put({tag: 'ctc_data', data});
        request.onsuccess = () => resolve(db);
        request.onerror = reject;
    });
}
  • 查詢/讀取數(shù)據(jù)
function query(db) {
    const STORE_NAME = 'ctc_aleinzhou';
    return new Promise(function (resolve, reject) {
        try {
            const tx = db.transaction(STORE_NAME, 'readonly');
            const store = tx.objectStore(STORE_NAME);
            const dbRequest = store.get('ctc_data');
            dbRequest.onsuccess = e => resolve(e.target.result);
            dbRequest.onerror = reject;
        }
        catch (err) {
            reject(err);
        }
    });
}
  • 在頁面打開數(shù)據(jù)庫,并初始化數(shù)據(jù)
openStore().then(db => saveData(db, null))
  • 消息監(jiān)聽
    在數(shù)據(jù)庫連接并初始化后輪詢
openStore().then(db => saveData(db, null)).then(function (db) {
    setInterval(function () {
        query(db).then(function (res) {
            if (res && res.data) {
                console.log('IndexedDB] receive message:', res.data);
            }
        });
    }, 1000);
});
  • 發(fā)送消息(向 IndexedDB 存儲數(shù)據(jù))
openStore().then(db => saveData(db, null)).then(function (db) {
    saveData(db, 'hello');
});
  • 兼容性
兼容性
總結

除了“廣播模式”,我們又了解了“共享存儲+長輪詢”這種模式的兩種方法,可能我們會覺得長輪詢沒有廣播模式優(yōu)雅,但實際上我們在使用“共享存儲”形式時,不一定要搭配輪詢。
例如在多 Tab 場景下,我們可能會離開 Tab A 到另一個 Tab B 中操作,過一會又切換到 Tab B,如果需要將之前在 Tab B 中操作的信息同步回來,只需在 Tab A 中監(jiān)聽 visibilitychange 這樣的時間,來做一次信息同步即可。


非同源頁面間的跨頁面通信

  • 首先模擬場景,假設現(xiàn)在有兩個不同源的頁面,iframePage.html 和 index.html
<!-- index.html -->
<iframe ref={node => (this.iframeWrapper = node)} src="xxx/xxx/iframePage.html"
  • 父頁面向子頁面發(fā)送消息
this.iframeWrapper.onload = function(){
  // iframe加載完立即發(fā)送一條消息
  this.iframeWrapper.contentWindow.postMessag('MessageFromIndex','*');
}

我們知道 postMessage 是掛載到window 對象上的,所以等 iframe 加載完畢后,調(diào)用 postMessage 給子頁面發(fā)送消息。
postMessage 方法的第一個參數(shù)是要發(fā)送的數(shù)據(jù),可以是任何原始類型的數(shù)據(jù)。第二個參數(shù)可以設置發(fā)送到哪個 url ,如果當前子頁面的 url 和設置的不一致,則會發(fā)送失敗,我們設置成*,則不限制。

  • 子頁面消息監(jiān)聽
function receiveMessageFromIndex ( event ) {
 console.log( 'receiveMessageFromIndex', event )
}

// 監(jiān)聽 message 事件
window.addEventListener("message", receiveMessageFromIndex, false);
  • 子頁面向父頁面發(fā)送消息
parent.postMessage({msg: 'MessageFromIframePage'}, '*');
  • 父頁面接收消息
function receiveMessageFromIframePage (event) {
    console.log('receiveMessageFromIframePage', event)
}

//監(jiān)聽message事件
window.addEventListener("message", receiveMessageFromIframePage, false);
  • 兼容性
兼容性

總結

對于同源頁面,常見的方式包括:

  • 廣播模式:Broadcast Channe / Service Worker / LocalStorage
  • 共享存儲模式:Shared Worker / IndexedDB / cookie
    對于非同源頁面,則可通過嵌入同源 iframe 作為 “橋”,將非同源頁面通信轉(zhuǎn)換為同源頁面通信。
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容