閱讀目錄
- 一:頁面窗口向 service worker 通信
- 二:service worker 向所有打開的窗口頁面通信
- 三:service worker 向特定的窗口通信
- 四:學習 MessageChannel 消息通道
- 五:窗口之間的通信
- 六:從sync事件向頁面傳遞消息
一:頁面窗口向 service worker 通信
Service Worker 沒有直接操作頁面DOM的權限。但是可以通過postMessage方法和web頁面進行通信。讓頁面操作DOM。並且這種操作是雙向的。
頁面向service worker 發送消息,首先我們要獲取當前控制頁面的 service worker。可以使用 navigator.serviceWorker.controller 來獲取這個service worker. 之后我們就可以使用 service worker 中的 postMessage() 方法,該方法接收的第一個參數為消息本身,該參數可以是任何值,可以是js對象,字符串、對象、數組、布爾型等。
比如如下代碼是 頁面向service worker 發送了一條簡單對象的消息:
navigator.serviceWorker.controller.postMessage({ 'userName': 'kongzhi', 'age': 31, 'sex': 'men', 'marriage': 'single' });
消息一旦發布,service worker 就可以通過監聽 message 事件來捕獲它。如下代碼:
self.addEventListener("message", function(event) { console.log(event.data); });
在代碼演示之前,我們來看下我們項目中的目錄結構如下:
|----- service-worker-demo7 | |--- node_modules # 項目依賴的包 | |--- public # 存放靜態資源文件 | | |--- js | | | |--- main.js # js 的入口文件 | | | |--- store.js # indexedDB存儲 | | | |--- myAccount.js | | |--- styles | | |--- images | | |--- index.html # html 文件 | |--- package.json | |--- webpack.config.js | |--- sw.js
如上就是我們目前的項目架構,這篇文章的項目架構是基於上篇文章的架構的基礎之上的,可以請移步查看上一篇文章。
因此在入口文件 main.js 代碼添加如下代碼:
// 頁面向 service worker 發送一條消息 if ("serviceWorker" in navigator && navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage({ 'userName': 'kongzhi', 'age': 31, 'sex': 'men', 'marriage': 'single' }); }
在我們的sw.js 里面,我們監聽 message 消息即可;添加如下代碼所示:
self.addEventListener("message", function(event) { console.log(event.data); console.log(event); });
注意:當我們第一次刷新頁面注冊service worker的時候並沒有發送消息,那是因為第一次刷新頁面的時候並沒有注冊service worker,只有注冊完成后,我們再刷新頁面就可以打印消息出來了。因此我們上面加了 if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {} 這個來判斷。
如上打印 console.log(event.data); 消息如下所示:
當我們打印 console.log(event); 的時候;如下圖所示:
如上 打印 event 的時候,我們除了打印 event.data 可以獲取到消息之外的數據,我們還可以拿到 event.source 里面包含了發送消息的窗口的相關信息。
窗口向service worker 通信的具體用途如下:
比如說我們網站有很多很多頁面,是一個非常大型的網站,我們不可能對每個頁面進行緩存,我們可以對用戶訪問的頁面來進行緩存,那么這個時候我們可以通過 postMessage() 方法向用戶發送一條消息,告訴用戶該頁面需要被緩存了。
因此我們對某個頁面添加 js 代碼如下:
navigator.serviceWorker.controller.postMessage("cache-current-page");
當用戶訪問該頁面的時候,會發送一條消息到我們的 service worker 中,service worker 可以監聽這些消息,並使用事件的 source 屬性,判斷需要緩存那個頁面;具體判斷代碼如下:
self.addEventListener('message', function(event) { if (event.data === "cache-current-page") { var sourceUrl = event.source.url; if (event.source.visibilityState === 'visible') { // 緩存 sourceUrl 和相關的文件 } else { // 將sourceUrl和相關的文件添加到隊列中。稍后緩存 } } });
如上代碼;在sw.js 中我們可以根據 sourceUrl 來 確定需要緩存那個頁面,因為不同的頁面,他們的 sourceUrl 是不相同的。從那個頁面發送消息過來,那么就對應那個頁面的url。並且代碼里面根據頁面的可見狀態來判斷對應請求緩存哪個頁面。
二:service worker 向所有打開的窗口頁面通信
在service worker 內,我們可以使用 service worker 的全局對象中的clients對象,獲取 service worker作用域內所有當前打開的窗口。clients包含了一個 matchAll() 方法,我們可以使用這個方法獲取service worker 作用域內所有當前打開的窗口。
matchAll() 返回一個promise對象。返回一個包含0個或多個 WindowClient 對象的數組。
為了有多個頁面,因此我們需要在項目中的根目錄添加一個新頁面,比如叫 a.html. 因此目錄結構變成如下:
|----- service-worker-demo7 | |--- node_modules # 項目依賴的包 | |--- public # 存放靜態資源文件 | | |--- js | | | |--- main.js # js 的入口文件 | | | |--- store.js # indexedDB存儲 | | | |--- myAccount.js | | |--- styles | | |--- images | | |--- index.html # html 文件 | |--- package.json | |--- webpack.config.js | |--- sw.js | |--- a.html
因此在 sw.js 代碼中添加如下代碼:
self.clients.matchAll().then(function(clients) { console.log(clients); clients.forEach(function(client) { console.log(client); if (client.url.includes('/a.html')) { // 首頁 client.postMessage('hello world' + client.id); } }); });
然后我們在 main.js 代碼下 添加如下代碼:
if ("serviceWorker" in navigator && navigator.serviceWorker) { navigator.serviceWorker.addEventListener("message", function(event) { console.log(event.data); }) }
如上如果運行正常的話,就可以在控制台中看到類似如下信息:hello world7f71806e-7699-45f3-8d5b-50fdc67b34fc
注意:但是把我們的代碼放到 servcie worker 頂部是不行的,如果把代碼放在事件之外的話,它只會在 service worker 腳本加載后,service worker 安裝前以及任何客戶端監聽之前,它只會執行一次。因此我們需要放到 install 事件中,比如我之前緩存所有的頁面中install 事件中,放在如下代碼中即可:
// 監聽 install 事件,把所有的資源文件緩存起來 self.addEventListener("install", function(event) { event.waitUntil( caches.open(CACHE_NAME).then(function(cache) { return cache.addAll(CACHE_URLS); }).then(function(){ return self.clients.matchAll({includeUncontrolled: true}); }).then(function(clients){ console.log(clients); clients.forEach(function(client) { client.postMessage('hello world' + client.id); }); }) ) });
如上代碼,我們打印 console.log(clients); 可以看到如下信息:
現在不管我們的頁面是在線也好還是離線也好,都會執行代碼。我們可以在service worker 安裝並緩存所有的資源文件后,立即會向用戶發送一條消息。
三:service worker 向特定的窗口通信
除了上面的 matchAll()方法之外,clients對象還有另一個方法。我們可以通過 get()方法獲取單個客戶端的對象。通過傳遞一個已知客戶端的ID給get()方法,我們就可以得到一個promise。當其完成的時候我們就會得到 WindowClient對象,之后我們就可以使用該對象,給客戶端發送消息。
比如我們之前的客戶端的ID為 "87f07759-2e9e-4ecd-a9b2-3c64f843b9c7";
那么我們就可以使用該get()方法獲取該ID,然后會返回一個Promise對象,如下代碼所示:
self.clients.get("87f07759-2e9e-4ecd-a9b2-3c64f843b9c7").then(function(client) { client.postMessage("hello world"); });
有以下兩種方式可以找到客戶端的id, 第一種方式是使用 clients.matchAll()迭代所有的打開的客戶端,通過WindowClient對象的id屬性獲取。如下代碼所示:
self.clients.matchAll().then(function(clients) { clients.forEach(function(client){ self.clients.get(client.id).then(function(client) { client.postMessage("Messaging using clients.matchAll()"); }) }) });
第二種方法是通過postMessage事件的source屬性獲取,如下代碼所示:
self.addEventListener("message", function(event) { self.clients.get(event.source.id).then(function(client) { client.postMessage("Messaging using clients.get(event.source.id)"); }); });
四:學習 MessageChannel 消息通道
我們前面的demo使用了 WindowClient 或 service worker 對象發送消息,並且只看到了 postMessage()只接收了第一個參數。
但是我們的postMessage方法可以接收第二個參數,我們可以使用該參數來保持雙方之間的通信渠道打開。可以來回發送消息。
那么這種通信 是通過 MessageChannel 對象處理的。我們可以通過構造函數 MessageChannel() 可以創建一個消息通道,該實列會有2個屬性,分別為 port1 和 port2; 如下代碼所示:
var msg = new MessageChannel(); console.log(msg);
打印信息如下所示:
如上圖我們可以看到,該對象有 onmessage 和 onmessageerror 兩個屬性是兩個回調方法。我們可以使用 MessagePort.postMessage 方法發送消息的時候,我們就可以通過另一個端口的 onmessage 來監聽該消息。
也就是說消息通道是有兩個口子,那么這兩個口子分別是 port1 和 port2。這兩個口子可以相互發送消息,port1口子發送的消息,我們可以在port2口子中接收到消息。
比如如下代碼:
var msg = new MessageChannel(); var p1 = msg.port1; var p2 = msg.port2; // 使用p1口子監聽消息 p1.onmessage = function(msg) { console.log('接收到的消息:' + msg.data); } // 使用p2口子發送消息 p2.postMessage("hello world");
打印信息如下所示:
如上我們可以看到,MessageChannel對象有兩個口子,分別為 port1 和 port2; 我們在port2上使用 postMessage 發送消息,我們可以在 port1上監聽到該消息。
現在我們把該 MessageChannel 消息通道使用到我們的 service worker 當中來,當我們從窗口向service worker 通信時(或者反正都可以),我們可以在窗口中創建一個新的 MessageChannel 對象,並且通過 postMessage 將其中一個口子傳遞給 serviceworker, 當消息到達后,就可以在service worker 中訪問端口了。如下:
首先我們在我們的 main.js(入口文件)添加如下代碼:
var msgChan = new MessageChannel(); var p1 = msgChan.port1; // 使用p1口子監聽消息 p1.onmessage = function(msg) { console.log('接收到的消息:' + msg.data); } var msg = { name: 'kongzhi', age: 31, value: 2 }; if ("serviceWorker" in navigator && navigator.serviceWorker) { navigator.serviceWorker.controller.postMessage(msg, [msgChan.port2]); }
然后在我們的 service worker.js 中添加如下代碼:
// service worker 代碼 self.addEventListener("message", function(event) { var data = event.data; var port = event.ports[0]; if (data.name === 'kongzhi') { port.postMessage(data.value * 2); } });
然后在頁面上會打印如下信息:
如上代碼我們可以看到,我們在main.js 代碼中創建了一個新的 MessageChannel, 並且在port1中的口子上添加了事件監聽器。如果收到任何消息就會打印出來,然后我們就會使用 navigator.serviceWorker.controller.postMessage 代碼向 service worker發送一條消息。同時將 MessageChannel 第二個口子傳遞過去,這邊使用了一個數組傳遞過去,以便我們在service worker中通過0或者多個端口進行通信。
在service worker.js 中,我們監聽了message事件,當檢測到該事件的時候,我們使用 event.data 獲取到消息的內容,和頁面的端口,並且檢測該消息的 name 屬性 等於 'kongzhi' 這個字符串的話,那么我們就使用第二個口子 port2發送一個消息過去,那么在main.js 中,我們使用第一個口子 port1 來監聽該消息,然后就能接收到消息來了,最后打印信息了,如上所示。
如上demo我們演示了 使用 MessageChannel 來實現兩個口子(port1, port2) 之間通信的問題。那么現在我們使用 MessageChannel 如何在頁面和service worker 之間保持連續通信通道打開。
五:窗口之間的通信
通過以上一些知識點,我們現在再來看看如何在不同的窗口之間進行通信呢?現在我們可以通過使用上面的知識點來實現窗口之間發送消息。
比如我現在頁面上有一個注銷操作,當我們用戶點擊該操作時,該鏈接會把用戶返回到首頁,我們之前會在頁面上增加一個 a 鏈接按鈕,點擊該注銷按鈕的時候,我們會發送一個ajax請求,請求成功后,我們會跳轉到登錄頁面去。
現在我們需要使用service worker 來做同樣的操作,唯一不同的是,假如我們的頁面 打開了多個index.html頁面,比如網址為:
http://localhost:8082/index.html 這樣的,多個標簽頁都打開了該頁面,如果我們點擊注銷按鈕后,所有打開該頁面都會被同時退出到登錄頁面去。也就是說,在支持service worker 的瀏覽器下,支持多個窗口同時退出。
首先我們需要在我們的 main.js 添加如下代碼:
$(function(){ if ("serviceWorker" in navigator && navigator.serviceWorker) { console.log(navigator.serviceWorker.controller); $('#logout').click(function(e) { e.preventDefault(); navigator.serviceWorker.controller.postMessage({ action: "logout" }); }); navigator.serviceWorker.addEventListener("message", function(event) { var data = event.data; if (data.action === "navigate") { window.location.href = data.url; } }); } });
如上代碼,當我們點擊 注銷按鈕 id 為 logout 的時候,我們會使用 service worker中的postMessage中的方法:
navigator.serviceWorker.controller.postMessage 發送一個消息過去。然后我們sw.js 代碼中會監聽該消息,比如如下代碼:
// service worker 代碼 self.addEventListener("message", function(event) { var data = event.data; if (data.action === 'logout') { self.clients.matchAll().then(function(clients) { clients.forEach(function(client) { console.log(client.url); if (client.url.includes("http://localhost:8082/index.html")) { client.postMessage({ action: "navigate", url: 'http://www.baidu.com' }) } }) }); } });
然后會獲取到 消息內容 event.data; 然后會判斷該 action 是否等於 'logout' 這個字符串,如果相等的話,監聽器就會獲取當前打開的所有的 WindowClient, 逐個遍歷,並且檢查窗口是否包含 "http://localhost:8082/index.html", 如果包含的話,就向這個窗口發送消息,其中我們的鍵action包含了一個"navigate"字符串,可以隨便取名字。
然后在我們的main.js 會有如下監聽事件代碼,如下所示:
navigator.serviceWorker.addEventListener("message", function(event) { var data = event.data; if (data.action === "navigate") { window.location.href = data.url; } });
如果監聽到該消息,就重置向到 登錄頁面去,我這邊直接使用 百度 首頁打比方。當然當我們點擊注銷按鈕的時候,我們需要發送ajax請求,請求成功后,我們再使用如上的操作代碼。如上代碼,就可以使所有打開該頁面,都會重置到登錄頁面去。
六:從sync事件向頁面傳遞消息

var addStore = function(id, name, age) { var obj = { id: id, name: name, age: age }; addToObjectStore("store", obj); renderHTMLFunc(obj); // 先判斷瀏覽器支付支持sync事件 if ("serviceWorker" in navigator && "SyncManager" in window) { navigator.serviceWorker.ready.then(function(registration) { registration.sync.register("sync-store").then(function() { console.log("后台同步已觸發"); }).catch(function(err){ console.log('后台同步觸發失敗', err); }) }); } else { $.getJSON("http://localhost:8082/public/json/index.json", obj, function(data) { updateDisplay(data); }); } }; $("#submit").click(function(e) { addStore(1, 'kongzhi111', '28'); });
然后會調用 registration.sync.register("sync-store") 注冊一個同步事件,然后會在我們的 sw.js 下會監聽該事件;
如下代碼:
self.addEventListener("sync", function(event) { if (event.tag === "sync-store") { console.log('sync-store') event.waitUntil(syncStores()); } });
如上我們調用了 syncStores 這個函數,我們來看下該函數的代碼如下:
var syncStores = function() { return getStore().then(function(reservations) { console.log(reservations); return Promise.all( reservations.map(function(reservation){ var reservationUrl = createStoreUrl(reservation); return fetch(reservationUrl).then(function(response) { return response.json(); }).then(function(newResponse) { return updateInObjectStore("store", 1, newResponse).then(function(){ }) }) }) ) }); };
如上代碼,我們可以看到在我們的 最后一句代碼 return updateInObjectStore("store", 1, newResponse).then(function() { }) 中,最后調用了 updateInObjectStore 更新 indexedDB數據庫操作,但是我們如何把更新后的數據發送給DOM操作呢?我們之前學習了 postMessage() 這個,使頁面能和service worker 進行通信操作,我們把該技術運用起來。
因此我們需要把上面的sw.js 中的 syncStores 函數 代碼改成如下所示的:
// 新增的代碼: var postStoreDetails = function(data) { self.clients.matchAll({ includeUncontrolled: true }).then(function(clients) { clients.forEach(function(client) { client.postMessage({ action: 'update-store', data: data }) }); }); }; var syncStores = function() { return getStore().then(function(reservations) { console.log(reservations); return Promise.all( reservations.map(function(reservation){ var reservationUrl = createStoreUrl(reservation); return fetch(reservationUrl).then(function(response) { return response.json(); }).then(function(newResponse) { return updateInObjectStore("store", 1, newResponse).then(function() { // 新增的代碼如下: postStoreDetails(newResponse); }) }) }) ) }); };
如上我們在 updateInObjectStore 中的回調中添加了 postStoreDetails 這個函數代碼,然后把新的對象傳遞給函數,該函數如上代碼,會使用postMessage事件發送消息過去,然后我們需要在我們的 myAccount.js 中js操作頁面去使用 message 事件去監聽該消息,代碼如下所示:
function updateDisplay(d) { console.log(d); }; if ("serviceWorker" in navigator && navigator.serviceWorker) { navigator.serviceWorker.addEventListener("message", function(event) { var data = event.data; if (data.action === 'update-store') { console.log('函數終於被調用了'); updateDisplay(data); } }); }
最后我們點擊下該按鈕,會打印如下信息了;如下圖所示:
現在我們就可以拿到新增后或更新后的數據,在頁面DOM上進行操作數據了。