Service Worker 与页面通信
Service Worker 没有直接操作页面 DOM 的权限,但是可以通过 postMessage 方法和 Web 页面进行通信,让页面操作 DOM。而且这种通信可以是双向的,类似于 iframe 之间的通信。下面就给大家介绍 postMessage 在项目中的一些使用场景。注意下面的前提是浏览器支持 Service Worker。
下文的 service-worker.js
文件,简称为 sw.js
。
如何使用 postMessage 方法发送信息
1、在 sw.js
中向接管页面发信息,可以采用 client.postMessage() 方法,示例代码如下:
self.clients.matchAll()
.then(function (clients) {
if (clients && clients.length) {
clients.forEach(function (client) {
// 发送字符串'sw.update'
client.postMessage('sw.update');
})
}
})
2、在主页面给 Service Worker 发消息,可以采用 navigator.serviceWorker.controller.postMessage()
方法,示例代码如下:
// 点击指定 DOM 时就给Service Worker 发送消息
document.getElementById('app-refresh').addEventListener('click', function () {
navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage('sw.updatedone');
});
如何接收 postMessage 发送的信息
若要接收消息,当然我们需要绑定 message 的监听事件
1、在 sw.js
中接收主页面发来的信息,示例代码如下,通过 event.data 来读取数据:
self.addEventListener('message', function (event) {
console.log(event.data); // 输出:'sw.updatedone'
});
2、在页面中接收 sw.js
发来的信息,示例代码如下,通过 event.data 来读取数据:
navigator.serviceWorker.addEventListener('message', function (event) {
if (e.data === 'sw.update') {
// 此处可以操作页面的 DOM 元素啦
}
});
实现 sw.js 的检测更新机制
我们利用这种通信,为您在导出项目中做了一些简单的 sw.js
缓存更新,在上一节中的缓存更新及处理中有相应的阐述,这里具体展开一些实现,以及您后期可进行的升级:
sw.js
文件发现更新后,在 activate 事件最后 postMessage 事件(代码默认在导出项目中的 config/sw.tmpl.js
文件中)
// 如果非首次安装 Service Worker 或缓存中原先有缓存的静态资源,我们需要通知接管页面,sw.js有更新,提示用户点击刷新页面
if (!firstRegister) {
return self.clients.matchAll()
.then(function (clients) {
if (clients && clients.length) {
clients.forEach(function (client) {
client.postMessage('sw.update');
})
}
});
}
在页面中,接收到消息 Service Worker 的缓存更新消息后,在页面增加提示,如下图所示(代码默认在导出项目中的 src/sw-register.js
文件中)
// src/sw-register.js 中注册,重载相关代码
navigator.serviceWorker && navigator.serviceWorker.register('/service-worker.js')
.then(function () {
// 监听 message 事件,并处理
navigator.serviceWorker.addEventListener('message', function (e) {
// Service Worker 如果更新成功会 postMessage 给页面,内容为 'sw.update'
if (e.data === 'sw.update') {
// 开发者可以使用默认提供的用户提示,引导用户刷新
// 也可以自定义处理函数,这里建议引导用户 reload 处理,详细查看项目具体文件
// location.reload();
}
});
});
可能存在的问题及解决方案
1、问题
结合上面的更新提示机制,当我们在浏览器中打开多个相同的页面时,若 sw.js
文件更新成功,多个窗口均会弹出引导用户更新的提示条,当用户点击当前页面的 “点击刷新” 时,我们会重载当前页面,当切换至其他页面时,提示条仍然可见,并且还需用户点击刷新才能更新老页面,如果不刷新就可能存在老页面使用新缓存的问题,在新版本上线时,有一定的风险。使用者可以根据情况选择是否要做进一步升级。
2、解决方法
解决上述问题的方法也并不复杂,需要利用浏览器的 visibilitychange
事件,这是浏览器新添加的一个事件,当浏览器的某个标签页切换到后台,或从后台切换到前台时就会触发该消息,现在主流的浏览器都支持该事件了,例如 Chrome, Firefox, IE10 等。当切换某个页面时,就会触发该事件,可通过判断 “有刷新提示条 & 用户点击刷新” 来决定是否重载页面。
// 可以监听的事件名称
var visibilityChangeEvent = '';
if (typeof document.hidden !== 'undefined' ) {
visibilityChangeEvent = 'visibilitychange';
}
else if (typeof document.webkitHidden !== 'undefined') {
visibilityChangeEvent = 'wekitvisibilitychange';
}
else if (typeof document.mozHidden !== 'undefined') {
visibilityChangeEvent = 'mozvisibilitychange';
}
// 如果支持该事件,就绑定并添加处理函数
if (visibilityChangeEvent) {
var onVisibilityChange = function () {
// 在进入页面和离开页面均会触发该事件,所以我们这里需要判断是进入页面的情况才做处理
if (!(document.hidden || document.wekitHidden || document.mozHidden)) {
// 刷新提示条显示类名,判断是否有刷新条,这里只是示例
var hasRefreshTip = document.getElementsByClassName('app-refresh-show').length;
// 刷新提示是否被用户点击刷新,这里仅示例
var userClickRefresh = document.getElementsByClassName('app-refresh-click').length;
// 有刷新提示条 && 某个页面点击过刷新
if (hasRefreshTip && userClickRefresh) {
// 这里的location.reload只能刷新当前打开的页面,后台的页面并不起作用
location.reload();
}
}
}
document.addEventListener(visibilityChangeEvent, onVisibilityChange);
}
通过上面示例代码我们看到如果刷新页面,还需要关键的一个判断条件 —— 某个页面用户已点击过刷新。如果其他页面没点击过刷新,我们也不应该重载更新。其实,这里使用 双向通信
就可以解决这个问题了。当某个页面用户点击刷新后,给 Service Worker 发送一个 sw.updatedone
的消息,Service Worker 接收到该消息以后,可以给接管的后台页面发送该消息,后台页面接收到信息后,可以对 DOM 做相应的操作,如给特定标签增加一个指定类名等,用于页面激活后判断用户是否已点击过刷新。这样就简单的解决了
注意,不能在后台页面接收到 sw.updatedone
消息后就直接 reload, 会不起作用,浏览器只能 reload 当前的页面。
扩展
查看资料时,发现 Polymer 示例 使用了 MessageChannel 的方式 postMessage,大家也可以了解下。
MessageChannel 接口是信道通信 API 的一个接口,它允许我们创建一个新的信道并通过信道的两个 MessagePort 属性来传递数据
简单来说,MessageChannel 创建了一个通信的管道,这个管道有两个口子,每个口子都可以通过 postMessage 发送数据,而一个口子只要绑定了 onmessage 回调方法,就可以接收从另一个口子传过来的数据。
一个简单的例子:
var ch = new MessageChannel();
var p1 = ch.port1;
var p2 = ch.port2;
p1.onmessage = function (e) {
console.log("port1 receive " + e.data);
};
p2.onmessage = function (e) {
console.log("port2 receive " + e.data);
};
p1.postMessage("你好世界");
p2.postMessage("世界你好");
// 输出
// port1 receive 世界你好
// port2 receive 你好世界
MessageChannel 用法很简单,但是功能却不可小觑。例如当我们使用多个 Web Worker 之间实现通信的时候,MessageChannel 就可以派上用场。
小结
本文主要介绍所需的一些基础知识和示例,开发者可以根据自身项目需求进行相应的定制。