阅读目录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上进行操做数据了。