即將到來的 SharedWorker API 能夠在 iframe 甚至瀏覽器標(biāo)簽或窗口中傳輸數(shù)據(jù)。它在幾年前就已在 Chrome 中得以實(shí)現(xiàn),不久前也在 Firefox 上實(shí)現(xiàn)了,不過它在 IE 和 Safari 中仍然難覓蹤影。還好,這個(gè) API 有一種擁有廣泛瀏覽器支持,但鮮為人知的替代方案。是時(shí)候探索它了!
現(xiàn)在,我需要對(duì)以下應(yīng)用情景找到一個(gè)優(yōu)雅的解決方案:假設(shè)有個(gè)人訪問了你的網(wǎng)站。他依次登錄,打開第二個(gè)標(biāo)簽頁并在那個(gè)標(biāo)簽頁里選擇了注銷。這時(shí),他所打開的第一個(gè)標(biāo)簽頁看起來仍然保留著「已登錄」的狀態(tài),但這時(shí)他的所有操作要么會(huì)重定向到登錄頁面,要么會(huì)直接讓他抓狂。更吸引人的解決方式則是判斷用戶是否已注銷,并對(duì)頁面做相應(yīng)的改變。譬如可以顯示一個(gè)對(duì)話框來提示用戶需要重新驗(yàn)證,或者顯示原本的登錄視圖。
這個(gè)功能可以通過 WebSocket API 來實(shí)現(xiàn),不過這就有些小題大做了。畢竟殺雞焉用牛刀,于是我開始尋找一些其它的跨標(biāo)簽頁通信方式。我首先想到的就是使用 cookies 或者 localStorage ,來周期性地通過 setInterval 檢查用戶是否登錄。對(duì)這個(gè)方案我并不滿意,因?yàn)檫@樣會(huì)把許多 CPU 周期耗費(fèi)在檢查一個(gè)可能自始至終都不會(huì)滿足的條件上。這時(shí)候我就覺得還不如就直接用 “comet”(又名輪詢)、服務(wù)器端事件或者 WebSockets 算了呢。
所以當(dāng)我發(fā)現(xiàn)自己是在騎驢找驢的時(shí)候還是很吃驚,因?yàn)榇鸢妇褪且恢币詠淼?localStorage!
你知道 localStorage 會(huì)觸發(fā)一個(gè)事件嗎?具體地說,不論其中的哪一項(xiàng)在另一個(gè)瀏覽上下文里被添加、修改或刪除時(shí),它都會(huì)觸發(fā)一個(gè)事件。實(shí)際上,這就意味著不論在哪個(gè)瀏覽器的標(biāo)簽頁里訪問了 localStorage,所有其它的標(biāo)簽頁都能通過 window 對(duì)象監(jiān)聽到這個(gè)事件,就像這樣:
window.addEventListener('storage', function (event) {
console.log(event.key, event.newValue);
});
event 對(duì)象有幾個(gè)相應(yīng)的屬性:
不論某個(gè)標(biāo)簽頁在何時(shí)修改了 localStorage,都會(huì)對(duì)其余的所有標(biāo)簽觸發(fā)事件。這就意味著我們只要為 localStorage 賦值,就能夠跨瀏覽器標(biāo)簽通信了。請(qǐng)看下面?zhèn)未a風(fēng)格的示例:
var loggedOn;
// TODO: call when logged-in user changes or logs out
logonChanged();
window.addEventListener('storage', updateLogon);
window.addEventListener('focus', checkLogon);
function getUsernameOrNull () {
// TODO: return whether the user is logged on
}
function logonChanged () {
var uname = getUsernameOrNull();
loggedOn = uname;
localStorage.setItem('logged-on', uname);
}
function updateLogon (event) {
if (event.key === 'logged-on') {
loggedOn = event.newValue;
}
}
function checkLogon () {
var uname = getUsernameOrNull();
if (uname !== loggedOn) {
location.reload();
}
}
大意就是當(dāng)用戶打開了兩個(gè)標(biāo)簽頁,在其中一個(gè)里執(zhí)行了注銷操作后返回另一個(gè)時(shí),頁面將重新載入,(如果可以的話)服務(wù)器端邏輯將把用戶重定向到其它位置。這個(gè)檢查只在當(dāng)前標(biāo)簽頁獲得焦點(diǎn)時(shí)執(zhí)行,這是因?yàn)橛脩艨赡茉谧N后立刻重新登錄,這種情況下不應(yīng)將其余標(biāo)簽頁的狀態(tài)全部設(shè)為已注銷。
這段代碼肯定還可以改進(jìn),不過它已經(jīng)很好地滿足了需求。更好的實(shí)現(xiàn)方式可能會(huì)立刻要求用戶登錄,但要注意它也可能以相反的方式來工作:用戶登錄后打開另一個(gè)已經(jīng)注銷的標(biāo)簽頁時(shí),代碼會(huì)檢查并重新載入頁面,然后服務(wù)器(再說一遍,如果可以的話)就可以把用戶重定向到登錄頁面的不老泉里,期盼著你能有一次打電話給這個(gè)網(wǎng)站的經(jīng)驗(yàn)。
更簡(jiǎn)單的 API
localStorage API 可以說是 web 瀏覽器最簡(jiǎn)單的 API 之一了,并且它還享有相當(dāng)不錯(cuò)的跨瀏覽器支持。不過,一些瀏覽器的仍然存在著 quirks,譬如無痕模式下的 Safari 在設(shè)置值時(shí)會(huì)拋出 QuotaExceededError 的異常,有某些瀏覽器不支持開箱即用的 JSON,還有一些舊版瀏覽器會(huì)讓你感到沮喪。
因此,我整合了一個(gè) local-storage 模塊,為 localStorage 提供了簡(jiǎn)化的 API,從而擺脫這些 quirks,在缺少 localStorage API 時(shí)會(huì)回退到內(nèi)存存儲(chǔ),并通過使你為特定鍵注冊(cè)或取消注冊(cè)監(jiān)聽器,使得對(duì) storage 事件的使用更加容易。
截止到寫這篇文章時(shí),local-storage@1.3.1 中最新的 API 端點(diǎn)(譯者注:2015-01-08) 如下:
ls(key, value?) 取得或設(shè)置鍵
ls.get(key) 取得鍵的值
ls.set(key, value) 為鍵指定值
ls.remove(key) 移除鍵
ls.on(key, fn(value, old, url)) 監(jiān)聽其它標(biāo)簽頁的鍵值改變并觸發(fā) fn
ls.off(key, fn) 取消之前使用 ls.on 注冊(cè)的監(jiān)聽器
同樣值得一提的是, local-storage 注冊(cè)了一個(gè)單一的 storage 對(duì)象處理器并保持你對(duì)每個(gè)鍵的跟蹤,而不是注冊(cè)多個(gè) storage 對(duì)象。
我對(duì)學(xué)習(xí)其它跨標(biāo)簽頁通信的底層實(shí)現(xiàn)方式也很有興趣!它對(duì)離線優(yōu)先的開發(fā)很有幫助,尤其是考慮到當(dāng)前 SharedWorker 還需要一段時(shí)間才能迎來廣泛支持,而 WebSocket 在離線使用的情境下也靠不住時(shí),這種通信方式就更有意義了。
聯(lián)系客服