java按钮触发另一个页面_前端跨页面通信,你知道哪些方法?
戳藍(lán)字「前端技術(shù)優(yōu)選」關(guān)注我們哦!?
引言
在瀏覽器中,我們可以同時打開多個Tab頁,每個Tab頁可以粗略理解為一個“獨(dú)立”的運(yùn)行環(huán)境,即使是全局對象也不會在多個Tab間共享。然而有些時候,我們希望能在這些“獨(dú)立”的Tab頁面之間同步頁面的數(shù)據(jù)、信息或狀態(tài)。
正如下面這個例子:我在列表頁點(diǎn)擊“收藏”后,對應(yīng)的詳情頁按鈕會自動更新為“已收藏”狀態(tài);類似的,在詳情頁點(diǎn)擊“收藏”后,列表頁中按鈕也會更新。
這就是我們所說的前端跨頁面通信。
你知道哪些跨頁面通信的方式呢?如果不清楚,下面我就帶大家來看看七種跨頁面通信的方式。
一、同源頁面間的跨頁面通信
以下各種方式的 在線 Demo 可以戳這里 >>
瀏覽器的同源策略在下述的一些跨頁面通信方法中依然存在限制。因此,我們先來看看,在滿足同源策略的情況下,都有哪些技術(shù)可以用來實(shí)現(xiàn)跨頁面通信。
1. BroadCast Channel
BroadCast Channel 可以幫我們創(chuàng)建一個用于廣播的通信頻道。當(dāng)所有頁面都監(jiān)聽同一頻道的消息時,其中某一個頁面通過它發(fā)送的消息就會被其他所有頁面收到。它的API和用法都非常簡單。
下面的方式就可以創(chuàng)建一個標(biāo)識為AlienZHOU的頻道:
const bc = new BroadcastChannel('AlienZHOU');各個頁面可以通過onmessage來監(jiān)聽被廣播的消息:
bc.onmessage = function (e) {const data = e.data;const text = '[receive] ' + data.msg + ' —— tab ' + data.from;console.log('[BroadcastChannel] receive message:', text);};
要發(fā)送消息時只需要調(diào)用實(shí)例上的postMessage方法即可:
bc.postMessage(mydata);Broadcast Channel 的具體的使用方式可以看這篇《【3分鐘速覽】前端廣播式通信:Broadcast Channel》。
2. Service Worker
Service Worker 是一個可以長期運(yùn)行在后臺的 Worker,能夠?qū)崿F(xiàn)與頁面的雙向通信。多頁面共享間的 Service Worker 可以共享,將 Service Worker 作為消息的處理中心(中央站)即可實(shí)現(xiàn)廣播效果。
Service Worker 也是 PWA 中的核心技術(shù)之一,由于本文重點(diǎn)不在 PWA ,因此如果想進(jìn)一步了解 Service Worker,可以閱讀我之前的文章【PWA學(xué)習(xí)與實(shí)踐】(3) 讓你的WebApp離線可用。
首先,需要在頁面注冊 Service Worker:
/* 頁面邏輯 */navigator.serviceWorker.register('../util.sw.js').then(function () {console.log('Service Worker 注冊成功');});
其中../util.sw.js是對應(yīng)的 Service Worker 腳本。Service Worker 本身并不自動具備“廣播通信”的功能,需要我們添加些代碼,將其改造成消息中轉(zhuǎn)站:
/* ../util.sw.js Service Worker 邏輯 */self.addEventListener('message', function (e) {console.log('service worker receive message', e.data);e.waitUntil(self.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()獲取當(dāng)前注冊了該 Service Worker 的所有頁面,通過調(diào)用每個client(即頁面)的postMessage方法,向頁面發(fā)送消息。這樣就把從一處(某個Tab頁面)收到的消息通知給了其他頁面。
處理完 Service Worker,我們需要在頁面監(jiān)聽 Service Worker 發(fā)送來的消息:
/* 頁面邏輯 */navigator.serviceWorker.addEventListener('message', function (e) {const data = e.data;const text = '[receive] ' + data.msg + ' —— tab ' + data.from;console.log('[Service Worker] receive message:', text);});
最后,當(dāng)需要同步消息時,可以調(diào)用 Service Worker 的postMessage方法:
/* 頁面邏輯 */navigator.serviceWorker.controller.postMessage(mydata);3. LocalStorage
LocalStorage 作為前端最常用的本地存儲,大家應(yīng)該已經(jīng)非常熟悉了;但StorageEvent這個與它相關(guān)的事件有些同學(xué)可能會比較陌生。
當(dāng) LocalStorage 變化時,會觸發(fā)storage事件。利用這個特性,我們可以在發(fā)送消息時,把消息寫入到某個 LocalStorage 中;然后在各個頁面內(nèi),通過監(jiān)聽storage事件即可收到通知。
window.addEventListener('storage', function (e) {if (e.key === 'ctc-msg') {const data = JSON.parse(e.newValue);const text = '[receive] ' + data.msg + ' —— tab ' + data.from;console.log('[Storage I] receive message:', text);}
});
在各個頁面添加如上的代碼,即可監(jiān)聽到 LocalStorage 的變化。當(dāng)某個頁面需要發(fā)送消息時,只需要使用我們熟悉的setItem方法即可:
mydata.st = +(new Date);window.localStorage.setItem('ctc-msg', JSON.stringify(mydata));注意,這里有一個細(xì)節(jié):我們在mydata上添加了一個取當(dāng)前毫秒時間戳的.st屬性。這是因?yàn)?#xff0c;storage事件只有在值真正改變時才會觸發(fā)。舉個例子:
window.localStorage.setItem('test',?'123');window.localStorage.setItem('test',?'123');
由于第二次的值'123'與第一次的值相同,所以以上的代碼只會在第一次setItem時觸發(fā)storage事件。因此我們通過設(shè)置st來保證每次調(diào)用時一定會觸發(fā)storage事件。
小憩一下
上面我們看到了三種實(shí)現(xiàn)跨頁面通信的方式,不論是建立廣播頻道的 Broadcast Channel,還是使用 Service Worker 的消息中轉(zhuǎn)站,抑或是些 tricky 的storage事件,其都是“廣播模式”:一個頁面將消息通知給一個“中央站”,再由“中央站”通知給各個頁面。
在上面的例子中,這個“中央站”可以是一個 BroadCast Channel 實(shí)例、一個 Service Worker 或是 LocalStorage。
下面我們會看到另外兩種跨頁面通信方式,我把它稱為“共享存儲+輪詢模式”。
4. Shared Worker
Shared Worker 是 Worker 家族的另一個成員。普通的 Worker 之間是獨(dú)立運(yùn)行、數(shù)據(jù)互不相通;而多個 Tab 注冊的 Shared Worker 則可以實(shí)現(xiàn)數(shù)據(jù)共享。
Shared Worker 在實(shí)現(xiàn)跨頁面通信時的問題在于,它無法主動通知所有頁面,因此,我們會使用輪詢的方式,來拉取最新的數(shù)據(jù)。思路如下:
讓 Shared Worker 支持兩種消息。一種是 post,Shared Worker 收到后會將該數(shù)據(jù)保存下來;另一種是 get,Shared Worker 收到該消息后會將保存的數(shù)據(jù)通過postMessage傳給注冊它的頁面。也就是讓頁面通過 get 來主動獲取(同步)最新消息。具體實(shí)現(xiàn)如下:
首先,我們會在頁面中啟動一個 Shared Worker,啟動方式非常簡單:
// 構(gòu)造函數(shù)的第二個參數(shù)是 Shared Worker 名稱,也可以留空const sharedWorker = new SharedWorker('../util.shared.js', 'ctc');然后,在該 Shared Worker 中支持 get 與 post 形式的消息:
/* ../util.shared.js: Shared Worker 代碼 */let data = null;self.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();
});
之后,頁面定時發(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', (e) => {const data = e.data;const text = '[receive] ' + data.msg + ' —— tab ' + data.from;console.log('[Shared Worker] receive message:', text);
}, false);sharedWorker.port.start();
最后,當(dāng)要跨頁面通信時,只需給 Shared Worker postMessage即可:
sharedWorker.port.postMessage(mydata);注意,如果使用addEventListener來添加 Shared Worker 的消息監(jiān)聽,需要顯式調(diào)用MessagePort.start方法,即上文中的sharedWorker.port.start();如果使用onmessage綁定監(jiān)聽則不需要。
5. IndexedDB
除了可以利用 Shared Worker 來共享存儲數(shù)據(jù),還可以使用其他一些“全局性”(支持跨頁面)的存儲方案。例如 IndexedDB 或 cookie。
鑒于大家對 cookie 已經(jīng)很熟悉,加之作為“互聯(lián)網(wǎng)最早期的存儲方案之一”,cookie 已經(jīng)在實(shí)際應(yīng)用中承受了遠(yuǎn)多于其設(shè)計(jì)之初的責(zé)任,我們下面會使用 IndexedDB 來實(shí)現(xiàn)。
其思路很簡單:與 Shared Worker 方案類似,消息發(fā)送方將消息存至 IndexedDB 中;接收方(例如所有頁面)則通過輪詢?nèi)カ@取最新的信息。在這之前,我們先簡單封裝幾個 IndexedDB 的工具方法。
打開數(shù)據(jù)庫連接:
}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ù)
});
}
查詢/讀取數(shù)據(jù)
}catch (err) {reject(err);
}
});
}
剩下的工作就非常簡單了。首先打開數(shù)據(jù)連接,并初始化數(shù)據(jù):
openStore().then(db => saveData(db, null))對于消息讀取,可以在連接與初始化后輪詢:
openStore().then(db => saveData(db, null)).then(function (db) {setInterval(function () {query(db).then(function (res) {if (!res || !res.data) {return;}const data = res.data;const text = '[receive] ' + data.msg + ' —— tab ' + data.from;console.log('[Storage I] receive message:', text);
});
}, 1000);
});
最后,要發(fā)送消息時,只需向 IndexedDB 存儲數(shù)據(jù)即可:
openStore().then(db => saveData(db, null)).then(function (db) {// …… 省略上面的輪詢代碼// 觸發(fā) saveData 的方法可以放在用戶操作的事件監(jiān)聽內(nèi)saveData(db, mydata);});
小憩一下
在“廣播模式”外,我們又了解了“共享存儲+長輪詢”這種模式。也許你會認(rèn)為長輪詢沒有監(jiān)聽模式優(yōu)雅,但實(shí)際上,有些時候使用“共享存儲”的形式時,不一定要搭配長輪詢。
例如,在多 Tab 場景下,我們可能會離開 Tab A 到另一個 Tab B 中操作;過了一會我們從 Tab B 切換回 Tab A 時,希望將之前在 Tab B 中的操作的信息同步回來。這時候,其實(shí)只用在 Tab A 中監(jiān)聽visibilitychange這樣的事件,來做一次信息同步即可。
下面,我會再介紹一種通信方式,我把它稱為“口口相傳”模式。
6. window.open + window.opener
當(dāng)我們使用window.open打開頁面時,方法會返回一個被打開頁面window的引用。而在未顯示指定noopener時,被打開的頁面可以通過window.opener獲取到打開它的頁面的引用 —— 通過這種方式我們就將這些頁面建立起了聯(lián)系(一種樹形結(jié)構(gòu))。
首先,我們把window.open打開的頁面的window對象收集起來:
let childWins = [];document.getElementById('btn').addEventListener('click', function () {const win = window.open('./some/sample');childWins.push(win);});
然后,當(dāng)我們需要發(fā)送消息的時候,作為消息的發(fā)起方,一個頁面需要同時通知它打開的頁面與打開它的頁面:
// 過濾掉已經(jīng)關(guān)閉的窗口childWins = childWins.filter(w => !w.closed);if (childWins.length > 0) {mydata.fromOpenner = false;childWins.forEach(w => w.postMessage(mydata));
}if (window.opener && !window.opener.closed) {mydata.fromOpenner = true;window.opener.postMessage(mydata);
}
注意,我這里先用.closed屬性過濾掉已經(jīng)被關(guān)閉的 Tab 窗口。這樣,作為消息發(fā)送方的任務(wù)就完成了。下面看看,作為消息接收方,它需要做什么。
此時,一個收到消息的頁面就不能那么自私了,除了展示收到的消息,它還需要將消息再傳遞給它所“知道的人”(打開與被它打開的頁面):
需要注意的是,我這里通過判斷消息來源,避免將消息回傳給發(fā)送方,防止消息在兩者間死循環(huán)的傳遞。(該方案會有些其他小問題,實(shí)際中可以進(jìn)一步優(yōu)化)
window.addEventListener('message', function (e) {const data = e.data;const text = '[receive] ' + data.msg + ' —— tab ' + data.from;console.log('[Cross-document Messaging] receive message:', text);// 避免消息回傳if (window.opener && !window.opener.closed && data.fromOpenner) {window.opener.postMessage(data);}// 過濾掉已經(jīng)關(guān)閉的窗口
childWins = childWins.filter(w => !w.closed);// 避免消息回傳if (childWins && !data.fromOpenner) {childWins.forEach(w => w.postMessage(data));
}
});
這樣,每個節(jié)點(diǎn)(頁面)都肩負(fù)起了傳遞消息的責(zé)任,也就是我說的“口口相傳”,而消息就在這個樹狀結(jié)構(gòu)中流轉(zhuǎn)了起來。
小憩一下
顯然,“口口相傳”的模式存在一個問題:如果頁面不是通過在另一個頁面內(nèi)的window.open打開的(例如直接在地址欄輸入,或從其他網(wǎng)站鏈接過來),這個聯(lián)系就被打破了。
除了上面這六個常見方法,其實(shí)還有一種(第七種)做法是通過 WebSocket 這類的“服務(wù)器推”技術(shù)來進(jìn)行同步。這好比將我們的“中央站”從前端移到了后端。
關(guān)于 WebSocket 與其他“服務(wù)器推”技術(shù),不了解的同學(xué)可以閱讀這篇《各類“服務(wù)器推”技術(shù)原理與實(shí)例(Polling/COMET/SSE/WebSocket)》
此外,我還針對以上各種方式寫了一個 在線演示的 Demo >>
二、非同源頁面之間的通信
上面我們介紹了七種前端跨頁面通信的方法,但它們大都受到同源策略的限制。然而有時候,我們有兩個不同域名的產(chǎn)品線,也希望它們下面的所有頁面之間能無障礙地通信。那該怎么辦呢?
要實(shí)現(xiàn)該功能,可以使用一個用戶不可見的 iframe 作為“橋”。由于 iframe 與父頁面間可以通過指定origin來忽略同源限制,因此可以在每個頁面中嵌入一個 iframe (例如:http://sample.com/bridge.html),而這些 iframe 由于使用的是一個 url,因此屬于同源頁面,其通信方式可以復(fù)用上面第一部分提到的各種方式。
頁面與 iframe 通信非常簡單,首先需要在頁面中監(jiān)聽 iframe 發(fā)來的消息,做相應(yīng)的業(yè)務(wù)處理:
/* 業(yè)務(wù)頁面代碼 */window.addEventListener('message', function (e) {// …… do something});
然后,當(dāng)頁面要與其他的同源或非同源頁面通信時,會先給 iframe 發(fā)送消息:
/* 業(yè)務(wù)頁面代碼 */window.frames[0].window.postMessage(mydata, '*');其中為了簡便此處將postMessage的第二個參數(shù)設(shè)為了'*',你也可以設(shè)為 iframe 的 URL。iframe 收到消息后,會使用某種跨頁面消息通信技術(shù)在所有 iframe 間同步消息,例如下面使用的 Broadcast Channel:
/* iframe 內(nèi)代碼 */const bc = new BroadcastChannel('AlienZHOU');// 收到來自頁面的消息后,在 iframe 間進(jìn)行廣播window.addEventListener('message', function (e) {bc.postMessage(e.data);});
其他 iframe 收到通知后,則會將該消息同步給所屬的頁面:
/* iframe 內(nèi)代碼 */// 對于收到的(iframe)廣播消息,通知給所屬的業(yè)務(wù)頁面bc.onmessage = function (e) {window.parent.postMessage(e.data, '*');};
下圖就是使用 iframe 作為“橋”的非同源頁面間通信模式圖。
其中“同源跨域通信方案”可以使用文章第一部分提到的某種技術(shù)。
總結(jié)
今天和大家分享了一下跨頁面通信的各種方式。
對于同源頁面,常見的方式包括:
廣播模式:Broadcast Channe / Service Worker / LocalStorage + StorageEvent
共享存儲模式:Shared Worker / IndexedDB / cookie
口口相傳模式:window.open + window.opener
基于服務(wù)端:Websocket / Comet / SSE 等
而對于非同源頁面,則可以通過嵌入同源 iframe 作為“橋”,將非同源頁面通信轉(zhuǎn)換為同源頁面通信。
本文在分享的同時,也是為了拋轉(zhuǎn)引玉。如果你有什么其他想法,歡迎一起討論,提出你的見解和想法~
總結(jié)
以上是生活随笔為你收集整理的java按钮触发另一个页面_前端跨页面通信,你知道哪些方法?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: tornado celery mysql
- 下一篇: pythonweb面试常见问题_pyth