Skip to content
导航

Service Worker

Service worker 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。

这个 API 旨在创建有效的离线体验,可以用来做这些事情:

  • 后台数据同步
  • 响应来自其他源的资源请求
  • 在多个页面间共享和更新数据
  • 在浏览器内实现开发用的脚手架能力,如模块编译、依赖管理
  • 性能优化,如预取资源

Service worker 是一个注册在指定源和路径下的事件驱动的 worker。

它被设计为完全异步的,不能在其中使用同步的XHR,也不能使用同步的API如localStorage,可能是因为无法保证返回时的数据符合开发者预期,容易造成时序问题。

由于 service worker 可以用作代理服务器,所以它可以实现中间人攻击。出于安全考虑,浏览器只允许在HTTPS页面或本地页面(localhost)注册、使用 service worker。

register

在浏览器中使用navigator.serviceWorker.register()方法下载脚本URL并安装为一个service worker

html
<script>
navigator.serviceWorker.register("service-worker.js", {
  scope: "./", //service worker注册范围,指定源和路径
}).then((registration) => {
  //下载完成,准备开始安装
}).catch(err => {
  //下载出错
})
</script>

生命周期

Service worker 是一个可以被多页面共享的worker,不同于SharedWorker是在所有端口断开后关闭,Service worker可以在页面关闭后独立运行。

Service worker 有三种生命周期,从register事件回调中读取:

  • installing
    service worker脚本下载完成,开始执行脚本中的 install 事件
  • wating
    service worker脚本更新下载完成,等待旧的脚本不再被页面使用,以替换旧脚本完成更新
  • active
    当前激活的 service worker
html
<script>
navigator.serviceWorker.register("service-worker.js", {
  scope: "./",
}).then((registration) => {
  let serviceWorker

  if (registration.installing) {
    serviceWorker = registration.installing
  } else if (registration.waiting) {
    serviceWorker = registration.waiting
  } else if (registration.active) {
    serviceWorker = registration.active
  }
})
</script>

同个service worker脚本在浏览器不同页面中可能处于不同生命周期,例如:

  • 在页面A安装service worker脚本,生命周期处于installing
  • 在页面B安装service worker脚本的新版本,生命周期处于installing
  • 在新页面C打开网页B,生命周期处于waiting
  • 关闭页面A,刷新页面B或C,生命周期处于active

状态

有五种状态:

  • installinginstalled
    开始执行install事件回调、回调执行完成
  • activatingactivated
    开始执行activate事件回调、回调执行完成
  • redundant
    当前 service worker 实例已冗余,已被注销
html
<script>
navigator.serviceWorker.register("service-worker.js", {
  scope: "./",
}).then((registration) => {
  let serviceWorker

  if (registration.installing) {
    serviceWorker = registration.installing
  } else if (registration.waiting) {
    serviceWorker = registration.waiting
  } else if (registration.active) {
    serviceWorker = registration.active
  }

  // 五种状态
  if (serviceWorker) {
    console.log(serviceWorker.state)
    serviceWorker.addEventListener("statechange", (e) => {
      console.log(e.target.state)
    });
  }
})
</script>

ready

这个属性返回一个Promise,当 serviceWorker 处于 active 时执行回调,即 state 值为 activatingactivated 执行。

它不会失败,只会成功或者不执行回调。

html
<script>
navigator.serviceWorker.ready.then((registration) => {
  console.log(`A service worker is active: ${registration.active}`)
})
</script>

适用于不生产 service worker,只消费 service worker 的页面。
当然也能在注册 service worker 的页面作为一种简化的API使用。

与 service worker 通信

与service worker和与shared worker通信类似,先要找到控制当前页面的service worker实例

html
<script>
navigator.serviceWorker.addEventListener("message", (e)=>{
  console.log('serviceWorker message', e.data)
})
navigator.serviceWorker.ready.then((registration)=>{
  registration.active.postMessage("I'm main page.")
})
</script>

和页面间通信一样,可以从event.source获得消息来源回复消息

js
//service-worker.js
self.addEventListener('message', (event) => {
  console.log('received message:', event.data)
  event.source.postMessage('I\'m service worker')
})

service worker也可以主动向页面发送消息,这段代码需要在页面完成监听message后运行

js
// service worker 主动向控制的已打开的所有页面发送消息
self.clients.matchAll().then(function(clients) {
  clients.forEach(function(client) {
    client.postMessage('The service worker just started up.');
  });
});

fetch事件

service worker 控制的页面,每次发起请求都会触发fetch事件,service worker 可以读取请求。

service worker 可以通过事件对象上的方法 event.responseWith() 阻止浏览器的默认 fetch 操作,并自定义响应。

js
self.addEventListener('fetch', (event) => {
  event.respondWith(
    new Response('service worker') //页面上的任何HTTP(s)请求都会得到自定义的响应,内容为字符串
  )
})

浏览器暴露此事件及API,为 service worker 提供了代理服务器的能力,也可以实现中间人攻击(修改、伪造响应)。

DEMO

参考资料