为了加快页面渲染,我们常使用先缓存后网络(StaleWhileRevalidate)策略将本地缓存作为响应快速返回给用户,同时从网络中获取最新资源并更新缓存,最后通知页面进行更新。由于 Service Worker 与页面运行在不同的线程环境中,故需要一种机制来保证缓存更新后页面能够及时得到通知。在 Workbox 中,我们可以使用 workbox-broadcast-cache-update模块来实现这一需求,接下来就让我们一起来探究该模块的使用

# 基本使用

workbox.routing.registerRoute(
  '/articles',
  new workbox.strategies.StaleWhileRevalidate({
    plugins: [
      new workbox.broadcastUpdate.Plugin({
        channelName: 'workbox',
        deferNoticationTimeout: 1000,
        headersToCheck: ['Content-Length', 'ETag', 'Last-Modified']
      })
    ]
  })
);

上例中,当请求 /articles 的缓存更新后,只要新响应头信息中 Content-LengthETagLast-Modified 的值有任何一个与旧响应头信息中相关属性的值不一致,便会向频道 workbox 广播缓存更新消息。其中 workbox.broadcastUpdate.Plugin 构造函数的参数为含有以下属性的对象:

  • channelName:频道名称(默认值为 workbox)。
  • headersToCheck:出于效率的考量,Workbox 通过比对前后两个响应的头信息来判断响应是否更新,我们可通过该属性来设置需比对的头信息(默认值为 content-lengthetaglast-modified)。
  • deferNoticationTimeout:当请求为导航请求,且相关缓存有所更新时,Workbox 会延迟广播直到页面准备妥当(页面可通过调用 navigator.serviceWorker.controller.postMessage 发送 {type: 'WINDOW_READY', meta: 'workbox-window'} 消息来告知 Workbox),同时也为了避免无限制地等待,我们可通过该属性以要求 Workbox 在等待指定时间后,无论是否收到页面通知,都将立即广播更新消息(默认值为 1000,单位为毫秒)

由于 workbox.broadcastUpdate.Plugin 内部使用了 workbox.broadcastUpdate.BroadcastCacheUpdate来处理缓存更新广播,因此在自定义的请求策略中,可直接使用它来处理缓存更新广播,比如:

const broadcastUpdate = new workbox.broadcastUpdate.BroadcastCacheUpdate({
  channelName: 'workbox',
  deferNoticationTimeout: 1000,
  headersToCheck: ['Content-Length', 'ETag', 'Last-Modified']
});

const cacheName = 'cacheName';
const url = 'http:/127.0.0.1:8080/articles';
const cache = await caches.open(cacheName);
const oldResponse = await cache.match(url);
const newResponse = await fetch(url);

broadcastUpdate.notifyIfUpdated({
  oldResponse,
  newResponse,
  url,
  cacheName
});

上例中,我们通过调用 workbox.broadcastUpdate.BroadcastCacheUpdate 实例的 notifyIfUpdated 方法,以便在缓存更新后广播相关信息。该方法的参数为含有以下属性的对象:

  • oldResponse:已经缓存的请求响应。
  • newResponse:新的将要被缓存的请求响应。
  • url:请求的 URL(字符串,非 URL 类型)。
  • cacheName:缓存名称。
  • event:触发请求的 FetchEvent 对象,该属性为可选属性。

上文对 Service Worker 中的处理进行了介绍,此处需要牢记:无论是使用 workbox.broadcastUpdate.Plugin还是 workbox.broadcastUpdate.BroadcastCacheUpdate,由于它们都是根据响应头的差异来判断缓存是否需要更新,因此如果我们将它们作用在不透明响应上,更新广播将永远不会触发。

完成了 Service Worker 中的设置,接下来就需要在页面中监听此消息,相关代码如下:

if ('BroadcastChannel' in window) {
  const workboxChannel = new BroadcastChannel('workbox');
  workboxChannel.addEventListener('message',  event => {
    console.log('Receive message from ServiceWorker:', event.data);
  });
} else {
  navigator.serviceWorker.addEventListener('message', event => {
    console.log('Receive message from ServiceWorker:', event.data);
  });
}

上例中,如果浏览器支持 BroadcastChannel API,则监听 BroadcastChannel 的 message 事件,否则监听 navigator.serviceWorker 的 message 事件,回调参数 event.data 为含有以下属性的对象:

  • type:消息类型,值为常量 CACHE_UPDATED
  • meta:元属性,值为常量 workbox-broadcast-cache-update
  • payload:缓存相关信息,值为含有以下属性的对象:
  • cacheName:缓存名称。
  • updatedUrl:已更新的缓存地址(字符串,非 URL 类型)

# 消息总线

上文提到了 BroadcastChannel,通过它我们可以实现 Service Worker 与页面的相互通信,当然也可使用 postMessageMessageChannel 来实现此功能。究竟这三种技术有何区别?它们各自的适用场景又该如何?本节将为一一为大家进行介绍。

# postMessage

通过 postMessage 可实现不同窗口(比如:iframe、WebWorker 或 ServiceWorker)间的相互通信。同时,由于该方法允许来自不同源的脚本进行有效且安全地通信,它也常作为跨域通信的有效解决方案。此方法的使用如下:

otherWindow.postMessage(message, targetOrigin, transferList);
  • otherWindow:其他窗口的一个引用,比如 iframecontentWindow属性、执行 window.open 所返回的窗口对象、已命名或数值索引的 window.framesWorkerServiceWorker实例。
  • message:需要发送给 otherWindow 的数据,其值为可被结构化克隆算法序列化的所有类型。
  • targetOrigin:通过该参数可控制消息能够发送给哪些窗口;只有目标窗口的协议、Host 地址及端口号与该参数的值完全相同,此窗口才能接收信息;当值为 * 时,则表明任何窗口都可以接收信息。
  • transferList:可选参数,Transferable 对象数组,这些对象的所有权将转移给消息的接收方,并且在所有权转移之后,消息的发送方将不能再操作该对象,否则将抛出异常。

于 Worker 或 ServiceWorker 与注册它的页面遵循同源策略,因此它们的实例方法 postMessage 的参数与上述有所差异,依次为 message 和 transferList,类型则与上述相关参数相同。

目标窗口可通过监听 message 事件来接收消息:

addEventListener('message', event => {
  // doSomething...
});

回调参数 event 为 MessageEvent 对象,主要包含以下属性:

  • data:从其他窗口发送的消息对象。
  • origin:消息发送窗口的源。
  • source:消息发送窗口的引用。

# MessageChannel

除了 postMessage,我们也可以使用 MessageChannel 来实现不同窗口间的相互通信,它与 postMessage 的主要差异有:

  • MessageChannel 的通信双方必须遵循同源策略,不能进行跨域通信。
  • MessageChannel 无需维护通信双方实体的引用。
  • MessageChannel 的使用如下所示:
// index.html
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = event => {
  // doSomething...
};
navigator.serviceWorker.controller.postMessage(
  'Message from UI thread',
  [messageChannel.port2]
);

//sw.js
self.addEventListener('message', event => {
  // doSomething...
  event.ports[0].postMessage('Message back from Service Worker');
});

上例中,我们在页面中创建了信道实例 messageChannel,然后通过信道的两个端口 messageChannel.port1 和 messageChannel.port2 完成不同窗口的通信。端口的使用规则如下:

  • 创建信道的窗口(此处为 index.html)使用端口 port1,并设置此端口的 onmessage 属性来接收另一端口发送的消息;
  • 调用 postMessage(此处为 navigator.serviceWorker.controller.postMessage)方法将端口 port2 作为参数 transferList 的值,以消息的形式发送给另一个窗口(此处为 sw.js);
  • sw.js 中监听 message 事件,并通过 event.ports[0] 来获得从 index.html 传递过来的信道端口,然后便可调用端口的 postMessage 方法来发送消息给 index.html

其中:

  • event 参数为 MessageEvent 对象,主要属性上文已进行说明,此处不再重述。
  • postMessage 方法的参数依次为 message 和 transferList,具体类型上文已进行说明,此处不再重述。

# BroadcastChannel

虽然利用 MessageChannel,我们无需维护通信双方实体的引用便可完成双方的通信,但它依旧存在以下问题:

  • 由于通信双方必须持有同一信道的不同端口,所以创建信道的一方必须通过某种方式将端口传递给另一方,这在无形之中增加了代码的复杂度。
  • 由于同一通道只有两个端口,如果通信实体大于两个,那么 MessageChannel 将无法处理。

我们可以使用 BroadcastChannel 来解决上述问题,比如:

//index.html
const broadcastChannel = new BroadcastChannel('workbox');
broadcastChannel.addEventListener('message',  event => {
  //...doSomething
});
broadcastChannel.postMessage('Message from UI thread');

//sw.js
const broadcastChannel = new BroadcastChannel('workbox');
broadcastChannel.addEventListener('message',  event => {
  //...doSomething
});
broadcastChannel.postMessage('Message back from Service Worker');

上例中,我们在 index.html 和 sw.js 中创建了具有相同名称 workbox 的广播信道实例 broadcastChannel,然后通过调用实例方法 postMessage 来发送消息,并监听实例的 message 事件来接收消息。只要保证信道具有相同的名称,通信的任何一方无需再向另一方传递任何信息,便能接收到发送方发送的消息。基于此,该机制常作为不同窗口通信的首选方案,但它依旧存在以下限制:

  • BroadcastChannel 的通信双方必须遵循同源策略,不能进行跨域通信。
  • 不同于 MessageChannel,如果消息接收方在发送方发送消息之后才监听 message 事件,那么接收方将无法获得之前发送的消息。

其中:

  • event 参数为 MessageEvent 对象,主要属性上文已进行说明,此处不再重述。
  • postMessage 方法的参数只有 message,具体类型上文已进行说明,此处不再重述。

# 总结

本章我们首先对 workbox-broadcast-cache-update 模块进行了介绍,通过该模块,我们可以在请求缓存发生更新后,页面主线程能够及时得到通知;然后,我们对不同窗口通信的常见技术进行了介绍:

  • postMessage:主要用于跨域通信,但通信双方需要各自维护通信另一方实体的引用;
  • MessageChannel:无需维护通信双方实体的引用,但不能处理跨域通信,通信双方需要各自持有信道的一个端口,也由于一个信道只有两个端口,因此无法处理两个以上通信实体的相互通信;主要用于一对一,且消息接收方在发送方发送消息之后才设置 onmessage 参数时,依旧需要接收到之前发送的消息的场景。
  • BroadcastChannel:使用方式最为简单,且可以支持任意窗口(大于等于二)的相互通信,但不能处理跨域通信,并且如果消息接收方在发送方发送消息之后才监听 message 事件,那么接收方将无法获得之前发送的消息。

至此,我们完成了 Workbox 中缓存处理相关所有内容的学习,下一章,我们将对 Workbox 的后台同步进行介绍

阅读全文