Service Worker 与页面通信

Service Worker 没有直接操作页面 DOM 的权限,但是可以通过 postMessage 方法和 Web 页面进行通信,让页面操作 DOM。而且这种通信可以是双向的,类似于 iframe 之间的通信。下面就给大家介绍 postMessage 在项目中的一些使用场景。注意下面的前提是浏览器支持 Service Worker。

下文的 service-worker.js 文件,简称为 sw.js

如何使用 postMessage 方法发送信息

1、在 sw.js 中向接管页面发信息,可以采用 client.postMessage() 方法,示例代码如下:

  1. self.clients.matchAll()
  2. .then(function (clients) {
  3. if (clients && clients.length) {
  4. clients.forEach(function (client) {
  5. // 发送字符串'sw.update'
  6. client.postMessage('sw.update');
  7. })
  8. }
  9. })

2、在主页面给 Service Worker 发消息,可以采用 navigator.serviceWorker.controller.postMessage() 方法,示例代码如下:

  1. // 点击指定 DOM 时就给Service Worker 发送消息
  2. document.getElementById('app-refresh').addEventListener('click', function () {
  3. navigator.serviceWorker.controller && navigator.serviceWorker.controller.postMessage('sw.updatedone');
  4. });

如何接收 postMessage 发送的信息

若要接收消息,当然我们需要绑定 message 的监听事件

1、在 sw.js 中接收主页面发来的信息,示例代码如下,通过 event.data 来读取数据:

  1. self.addEventListener('message', function (event) {
  2. console.log(event.data); // 输出:'sw.updatedone'
  3. });

2、在页面中接收 sw.js 发来的信息,示例代码如下,通过 event.data 来读取数据:

  1. navigator.serviceWorker.addEventListener('message', function (event) {
  2. if (e.data === 'sw.update') {
  3. // 此处可以操作页面的 DOM 元素啦
  4. }
  5. });

实现 sw.js 的检测更新机制

我们利用这种通信,为您在导出项目中做了一些简单的 sw.js 缓存更新,在上一节中的缓存更新及处理中有相应的阐述,这里具体展开一些实现,以及您后期可进行的升级:

sw.js 文件发现更新后,在 activate 事件最后 postMessage 事件(代码默认在导出项目中的 config/sw.tmpl.js 文件中)

  1. // 如果非首次安装 Service Worker 或缓存中原先有缓存的静态资源,我们需要通知接管页面,sw.js有更新,提示用户点击刷新页面
  2. if (!firstRegister) {
  3. return self.clients.matchAll()
  4. .then(function (clients) {
  5. if (clients && clients.length) {
  6. clients.forEach(function (client) {
  7. client.postMessage('sw.update');
  8. })
  9. }
  10. });
  11. }

在页面中,接收到消息 Service Worker 的缓存更新消息后,在页面增加提示,如下图所示(代码默认在导出项目中的 src/sw-register.js 文件中)

  1. // src/sw-register.js 中注册,重载相关代码
  2. navigator.serviceWorker && navigator.serviceWorker.register('/service-worker.js')
  3. .then(function () {
  4. // 监听 message 事件,并处理
  5. navigator.serviceWorker.addEventListener('message', function (e) {
  6. // Service Worker 如果更新成功会 postMessage 给页面,内容为 'sw.update'
  7. if (e.data === 'sw.update') {
  8. // 开发者可以使用默认提供的用户提示,引导用户刷新
  9. // 也可以自定义处理函数,这里建议引导用户 reload 处理,详细查看项目具体文件
  10. // location.reload();
  11. }
  12. });
  13. });

版本更新提示引导

可能存在的问题及解决方案

1、问题

结合上面的更新提示机制,当我们在浏览器中打开多个相同的页面时,若 sw.js 文件更新成功,多个窗口均会弹出引导用户更新的提示条,当用户点击当前页面的 “点击刷新” 时,我们会重载当前页面,当切换至其他页面时,提示条仍然可见,并且还需用户点击刷新才能更新老页面,如果不刷新就可能存在老页面使用新缓存的问题,在新版本上线时,有一定的风险。使用者可以根据情况选择是否要做进一步升级。

2、解决方法

解决上述问题的方法也并不复杂,需要利用浏览器的 visibilitychange 事件,这是浏览器新添加的一个事件,当浏览器的某个标签页切换到后台,或从后台切换到前台时就会触发该消息,现在主流的浏览器都支持该事件了,例如 Chrome, Firefox, IE10 等。当切换某个页面时,就会触发该事件,可通过判断 “有刷新提示条 & 用户点击刷新” 来决定是否重载页面。

  1. // 可以监听的事件名称
  2. var visibilityChangeEvent = '';
  3. if (typeof document.hidden !== 'undefined' ) {
  4. visibilityChangeEvent = 'visibilitychange';
  5. }
  6. else if (typeof document.webkitHidden !== 'undefined') {
  7. visibilityChangeEvent = 'wekitvisibilitychange';
  8. }
  9. else if (typeof document.mozHidden !== 'undefined') {
  10. visibilityChangeEvent = 'mozvisibilitychange';
  11. }
  12. // 如果支持该事件,就绑定并添加处理函数
  13. if (visibilityChangeEvent) {
  14. var onVisibilityChange = function () {
  15. // 在进入页面和离开页面均会触发该事件,所以我们这里需要判断是进入页面的情况才做处理
  16. if (!(document.hidden || document.wekitHidden || document.mozHidden)) {
  17. // 刷新提示条显示类名,判断是否有刷新条,这里只是示例
  18. var hasRefreshTip = document.getElementsByClassName('app-refresh-show').length;
  19. // 刷新提示是否被用户点击刷新,这里仅示例
  20. var userClickRefresh = document.getElementsByClassName('app-refresh-click').length;
  21. // 有刷新提示条 && 某个页面点击过刷新
  22. if (hasRefreshTip && userClickRefresh) {
  23. // 这里的location.reload只能刷新当前打开的页面,后台的页面并不起作用
  24. location.reload();
  25. }
  26. }
  27. }
  28. document.addEventListener(visibilityChangeEvent, onVisibilityChange);
  29. }

通过上面示例代码我们看到如果刷新页面,还需要关键的一个判断条件 —— 某个页面用户已点击过刷新。如果其他页面没点击过刷新,我们也不应该重载更新。其实,这里使用 双向通信 就可以解决这个问题了。当某个页面用户点击刷新后,给 Service Worker 发送一个 sw.updatedone 的消息,Service Worker 接收到该消息以后,可以给接管的后台页面发送该消息,后台页面接收到信息后,可以对 DOM 做相应的操作,如给特定标签增加一个指定类名等,用于页面激活后判断用户是否已点击过刷新。这样就简单的解决了

注意,不能在后台页面接收到 sw.updatedone 消息后就直接 reload, 会不起作用,浏览器只能 reload 当前的页面。

扩展

查看资料时,发现 Polymer 示例 使用了 MessageChannel 的方式 postMessage,大家也可以了解下。

MessageChannel 接口是信道通信 API 的一个接口,它允许我们创建一个新的信道并通过信道的两个 MessagePort 属性来传递数据

简单来说,MessageChannel 创建了一个通信的管道,这个管道有两个口子,每个口子都可以通过 postMessage 发送数据,而一个口子只要绑定了 onmessage 回调方法,就可以接收从另一个口子传过来的数据。

一个简单的例子:

  1. var ch = new MessageChannel();
  2. var p1 = ch.port1;
  3. var p2 = ch.port2;
  4. p1.onmessage = function (e) {
  5. console.log("port1 receive " + e.data);
  6. };
  7. p2.onmessage = function (e) {
  8. console.log("port2 receive " + e.data);
  9. };
  10. p1.postMessage("你好世界");
  11. p2.postMessage("世界你好");
  12. // 输出
  13. // port1 receive 世界你好
  14. // port2 receive 你好世界

MessageChannel 用法很简单,但是功能却不可小觑。例如当我们使用多个 Web Worker 之间实现通信的时候,MessageChannel 就可以派上用场。

小结

本文主要介绍所需的一些基础知识和示例,开发者可以根据自身项目需求进行相应的定制。