如何使用 ServiceWorker 增强应用

如何使用 ServiceWorker 增强应用

发布于

ServiceWorker 是什么

ServiceWorker 是专门的 JavaScript 资源,用作网络浏览器和网络服务器之间的代理。类似于 Web Worker,所有工作都独立于主线程执行。

结合 Cache API,在 ServiceWorker 中拦截应用中所有请求并添加相应的缓存策略, 就可以在离线场景下,使用本地缓存降级支持所有请求响应,从而实现网络应用的离线使用。

请注意以下两点

  • 使用 ServiceWorker 属于渐进式增强,也就是如果不支持 ServiceWorker 或者在 ServiceWorker 中工作出错,应用仅是退化成普通的 Web 应用,基本功能不会中断支持。
  • 为确保安全 ServiceWorker 仅 https 和 localhost 可启用

ServiceWorker 的生命周期1

一个网页如果添加了 ServiceWorker 支持的话,首次访问网页时,页面的加载和 ServiceWorker 下载安装同时进行,页面正常提供基本功能。

在主线程注册 ServiceWorker

window.addEventListener('load', () => {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/service-worker.js').then(() => {
      console.log('Service worker registered!')
    })
  }
})

ServiceWorker 安装

ServiceWorker 会在注册后触发其 install 事件。每个 ServiceWorker 只能调用 install 一次,并且在更新前不会再次触发。

service_worker.js
self.addEventListener('install', (event) => { })

ServiceWorker 激活 activated

如果注册和安装成功,ServiceWorker 就会激活,并且其状态会变为 'activating'。可以在此时开始工作。

self.addEventListener('activate', (event) => {
  event.waitUntil(/** task() */)
})

在安装并激活 ServiceWorker 后,它将接管页面,开始工作,提高可靠性和速度。

更新

在执行导航和功能事件后,浏览器会自动检查更新,也可以手动触发更新:

navigator.serviceWorker.register('/service_worker.js').then(reg => {
  // sometime later…
  reg.update()
})

拦截网络请求

ServiceWorker 支持 fetch 事件,监听主线程发出的任何请求,先忽略这里 caches.open() 的调用,下文再介绍

service_worker.js
const cacheName = 'ImageCacheName'

self.addEventListener('fetch', async (event) => {
  if (event.request.destination === 'image') {
    // 拦截所有图片请求,有缓存则直接返回,无则请求后缓存并返回
    const cacheStore = await caches.open(cacheName)
    const cachedImage = await cacheStore.match(event.request)
    if (cachedImage) {
      return cachedImage
    }
    const response = await fetch(event.request.url)
    cacheStore.put(event.request, fetchedResponse.clone())
    return response
  }
})

event.request 类型声明

  • url 当前网络请求的网址
  • method HTTP method
  • mode 请求模式 值 'navigate' 通常用于区分对 HTML 文档的请求与其他请求
  • destination 请求的内容类型,并避免使用所请求资源的文件扩展名

此时,network 面板可以看到,响应图片请求的不再是 server,而是 ServiceWorker(需要后面刷新页面才能看到)

sw-image-network

Cache API

上面在 ServiceWorker 中缓存的使用与 HTTP 缓存并不冲突,Cache 接口是一种完全独立于 HTTP 缓存的缓存机制。可以认为是比 HTTP 应用层缓存更高一层的 JavaScript API 级别的磁盘缓存(不同于代码级别实现的运行时内存缓存)

Cache API 为 Request / Response 对象提供缓存机制,它与 ServiceWorker 的生命周期 API 的一部分提供,但它同时也是暴露在 window 作用域下的,不必一定要配合 ServiceWorker 使用。

关于 ServiceWorker 缓存的一些重要 API 方法包括:

  • CacheStorage.open 创建新的 Cache 实例,或者通过 Window.cachesWorkerGlobalScope.caches 访问
  • Cache.addCache.put 将网络响应存储在缓存中。
  • Cache.match 在 Cache 实例中查找缓存的响应。
  • Cache.delete 从 Cache 实例中移除缓存的响应。

缓存策略

下面介绍两种常用的缓存策略以支持应用不同资源的处理

缓存优先,回退到网络

上面示例采用的是缓存优先策略,有缓存则优先使用缓存,无缓存才发起请求。

这种策略适用于所有静态资源,CSS、JavaScript、图片、字体等等,虽然此类资源一般也会应用 HTTP 缓存策略,但在 ServiceWorker 中缓存后,可以进一步提高速度,以及获得离线使用能力。

网络优先,回退到缓存

此种策略下,资源请求一律在 ServiceWorker 转发为网络请求并更新到缓存响应,一但网络离线时,便直接使用缓存,这是实现应用离线使用的关键。

这种策略很适合 html 文档请求或者资源类 API

仅缓存

这种策略会将所有请求都导向缓存,就需要预缓存所有资源。

service_worker.js
self.addEventListener('install', (event) => {
  event.waitUntil(caches.open('StaticCacheStore').then((cache) => {
    return cache.addAll([
      '/1.js',
      '/2.css',
      '/3.jpg'
    ])
  }))
})

组合不同的缓存策略可以为应用实现丝滑的在线和离线使用支持,比如这个博客中的静态资源采用缓存优先,页面文档等采用网络优先,可以关闭网络后尝试刷新,页面仍可以正常加载查看

Workerbox

Workerbox 是 Chrome 提供的一组 ServiceWorker 生命周期和缓存策略的封装,简化应用的 ServiceWorker 增强使用

  • workbox-routing,用于请求匹配。
  • workbox-strategies,适用于缓存策略。
  • workbox-precaching(用于预缓存)。
  • workbox-expiration,用于管理缓存。
  • workbox-window,用于注册 Service Worker 并处理更新。

以及 worker-build 直接在开发构建过程中生成 ServiceWorker,如你使用 webpack 作为开发构建工具,还可直接选用已提供的 workbox-webpack-plugin GenerateSWInjectManifest

示例:NextJS

service_worker.ts
import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate } from 'workbox-strategies'

declare const self: ServiceWorkerGlobalScope

// Precache files
precacheAndRoute(self.__WB_MANIFEST)

// Cache pages
registerRoute(
  ({ request }) => request.mode === 'navigate',
  new StaleWhileRevalidate({
    cacheName: 'pages-cache',
  })
)

// Cache static resources
registerRoute(
  ({ request }) =>
    request.destination === 'style' ||
    request.destination === 'script' ||
    request.destination === 'image',
  new StaleWhileRevalidate({
    cacheName: 'static-resources-cache',
  })
)
next.config.js
const { InjectManifest } = require("workbox-webpack-plugin")

/**
 * @type {import('next/dist/next-server/server/config').NextConfig}
 **/
module.exports = {
  // ...
  webpack: (config, options) => {
    config.plugin.push(new InjectManifest({
      swSrc: './worker/service_worker.ts',
      swDest: '/service_worker.js',
      exclude: [
        /^build-manifest\.json$/i,
        /^react-loadable-manifest\.json$/i,
        /\/_error\.js$/i,
        /\.js\.map$/i,
        /\.map$/, /_next\/static\/.*/
      ],
      include: [/_next\/app-build-manifest\.json/],
    }))
    return config
  },
}

最后可以看到,网络处于关闭状态,此时刷新页面,网络请求全部由 ServiceWorker 代理发起,虽然全部失败,但可以回退到缓存, ServiceWorker 仍能给到主线程响应,页面也得以正常加载展示,真正实现了离线使用

sw-offline

Footnotes

  1. Service Worker 生命周期

最后更新于