跳转至

ServiceWorker入门

什么是 Service Worker

Service Worker 解决了困扰用户多年的难题:如何让 Web 应用程序在离线的情况下工作。

AppCache 虽然也是为了解决这个问题,但它已经是一个**已废弃**的 API,不要使用该特性。

虽然 Service Worker 的语法比 AppCache 更加复杂,但是你可以使用 JavaScript 更加精细地控制 AppCache 的静默行为。有了它,你可以解决目前离线应用的问题,同时也可以做更多的事。 Service Worker 可以使你的应用先访问本地缓存资源,所以在离线状态时,在没有通过网络接收到更多的数据前,仍可以提供基本的功能(一般称之为 Offline First)。

Service Worker 通过 navigator.serviceWorker.register('/sw.js') 来注册,注册成功后将运行在专属的 Service Worker 线程,该线程与主线程相互独立,没有访问 DOM 的能力,并且只能执行异步操作。

参考

本文参考了以下资料,建议优先阅读这些一手资料

调试 Service Worker

  • Firefox:在地址栏输入 about:debugging,点击 “此 Firefox”。
  • Chrome:打开 DevTools(F12),切换到 Application 选项卡。

动手实现一个最简单的 Service Worker

我们只需要创建 3 个文件:

  • index.html —— 用于展示页面内容
  • index.js —— 用于注册 Service Worker
  • sw.js —— Service Worker 本体的代码

查看代码 · Show me the code!

index.html

HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Service Worker Demo</title>
</head>
<body>
  <h1>Service Worker Demo</h1>
  <h2>v1</h2>

  <script src="index.js"></script>
</body>
</html> 

index.js

JavaScript
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(function (reg) {
    if (reg.installing) {
      console.log('Service worker installing')
    } else if (reg.waiting) {
      console.log('Service worker installed')
    } else {
      console.log('Service worker active')
    }

  }).catch(function (error) {
    console.log(error)
    alert('Service worker 注册失败!')
  })
} else {
  alert('浏览器不支持 serviceWorker!')
} 

sw.js

JavaScript
var CACHE_VERSION = 'v1'

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_VERSION).then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/index.js',
      ])
    })
  )
})

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((resp) => {
      return resp || fetch(event.request).then((response) => {
        let responseClone = response.clone()
        caches.open(CACHE_VERSION).then((cache) => {
          cache.put(event.request, responseClone)
        })

        return response
      }).catch(() => {
        return
      })
    })
  )
})

如果你使用上述代码运行成功了的话,可能还会发现一个问题,那就是第一次打开页面之后,无论怎么修改 sw.js 的缓存版本号和 index.html 内容,页面内容都不会改变(除非强制刷新或清除缓存)

解决 Service Worker 更新问题

出现这个问题的原因是,浏览器保存了多个版本的缓存,在没有删除之前缓存的情况下,默认读取旧的缓存。我们在 Service Worker 的 activate 事件中处理删除缓存的操作,并且在 install 事件中使用 self.skipWaiting() 来跳过等待,这样下次刷新页面时就可以加载新的缓存内容了。

sw.js:在 activate 事件中处理删除缓存的操作

JavaScript
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keyList) => {
      return Promise.all(keyList.map((key) => {
        if (key !== CACHE_VERSION) {
          return caches.delete(key)
        }
      }))
    })
  )
})

sw.js:在 install 事件中使用 self.skipWaiting() 来跳过等待

JavaScript
1
2
3
4
5
self.addEventListener('install', (event) => {
  // ... 之前的代码 ...

  return self.skipWaiting()
})

index.js:也可以在注册的时候增加一个事件监听,检测到 Service Worker 更新(controllerchange 事件)就执行刷新页面操作

JavaScript
1
2
3
4
5
6
7
8
9
navigator.serviceWorker.register('/sw.js').then(function (reg) {
  // ... 之前的代码 ...

  navigator.serviceWorker.addEventListener('controllerchange',
    // 当检测到更新时强制刷新当前页面(可能会丢失数据!)
    // 如果不强制刷新,则需要手动刷新才会生效(用户要看到最新内容一共需要刷新两遍)
    function () { window.location.reload(); }
  )
})

查看代码 · Show me the code!

当然,这只是最简单的一种更新方案(强制刷新可能会丢失未保存的用户数据),如果需要更科学的方案,请参阅:How to Fix the Refresh Button When Using Service Workers