閱讀目錄html
一:頁面窗口向 service worker 通訊node
Service Worker 沒有直接操做頁面DOM的權限。可是能夠經過postMessage方法和web頁面進行通訊。讓頁面操做DOM。而且這種操做是雙向的。webpack
頁面向service worker 發送消息,首先咱們要獲取當前控制頁面的 service worker。可使用 navigator.serviceWorker.controller 來獲取這個service worker. 以後咱們就可使用 service worker 中的 postMessage() 方法,該方法接收的第一個參數爲消息自己,該參數能夠是任何值,能夠是js對象,字符串、對象、數組、布爾型等。git
好比以下代碼是 頁面向service worker 發送了一條簡單對象的消息:github
navigator.serviceWorker.controller.postMessage({ 'userName': 'kongzhi', 'age': 31, 'sex': 'men', 'marriage': 'single' });
消息一旦發佈,service worker 就能夠經過監聽 message 事件來捕獲它。以下代碼:web
self.addEventListener("message", function(event) { console.log(event.data); });
在代碼演示以前,咱們來看下咱們項目中的目錄結構以下:ajax
|----- 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 代碼添加以下代碼:json
// 頁面向 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上進行操做數據了。