技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTHTML

是谁还在学 Service Worker

所属馆藏
HTML
文件格式
Markdown
原始路径
HTML/10-是谁还在学 Service Worker
本文目录13 个章节
  1. 前言
  2. Service Worker 是什么?
  3. 注意点
  4. 极简化 Demo
  5. 生命周期和更新
  6. 缓存策略
  7. Workbox 百宝箱
  8. Service Worker 与 Web Worker 傻傻分不清楚
  9. Chrome Extension 的 Service Worker 又是什么?
  10. 我能用 Service Worker 做什么?
  11. 上手注意事项
  12. 总结
  13. 参考资料

是谁还在学 Service Worker

Category(分类): HTML Status: 未知

本文尽量保留原文的叙述顺序和主题,对 Service Worker、Web Worker、Chrome Extension Service Worker、缓存策略和 Workbox 进行了合并整理。原文中的代码复制残留、过时或不准确的生命周期描述已经删除或修正。

前言

最近给平台做性能优化,恶补了一波 Service Worker,发现它的应用场景远不止性能优化和缓存。如果想做一些离线、后台事件和通知相关的功能,Service Worker 确实可以为浏览器端提供更多能力。

不过 Service Worker 的上手成本并不低。如果有人问你:

  • Service Worker 和 Web Worker 有什么关系?
  • Service Worker 和浏览器插件里的 background.js 是一回事吗?
  • Service Worker 是不是一个始终运行的后台线程?
  • Service Worker 能不能拦截所有请求?

这些问题很容易混淆。本文将尽量用直观的方式介绍 Service Worker 能做什么,以及它与相关概念之间的区别。

Service Worker 是什么?

Service Worker 是一种事件驱动的 Worker。它可以在满足安全上下文、作用域和页面控制条件时,作为 Web 应用、浏览器与网络之间的代理层,处理缓存、离线、Push 和部分后台事件。

Service Worker 可以理解成一个由浏览器管理的“中介”:

  • 用户暂时没有网络时,中介可以返回之前缓存的页面或资源;
  • 页面请求资源时,中介可以按照既定策略选择缓存或网络;
  • 网络请求成功后,中介可以更新缓存,供下次访问使用;
  • 收到 Push 事件时,中介可以展示通知;
  • 在支持 Background Sync 的浏览器中,中介可以在合适的时机重试短时任务。

不过,Service Worker 并不是一个始终运行的后台进程。它空闲时可能被浏览器终止,需要处理 fetchpushsync 等事件时再重新启动。

注意点

1. 需要安全上下文

Service Worker 正式环境通常必须使用 HTTPS。本地开发时,http://localhost 通常被浏览器视为安全上下文。

正式环境:HTTPS
本地开发:localhost 通常可以使用 HTTP

2. 不能直接操作 DOM

Service Worker 运行在独立的 Worker 上下文中,不能访问:

  • window
  • document
  • 页面 DOM;
  • 页面中的 parent

它可以访问 selfnavigatorlocation 等 Worker 环境对象,但 Service Worker 中的 location 表示 Service Worker 脚本自身的地址,不是页面当前地址。

3. 通过事件和页面通信

Service Worker 与页面之间通常使用:

  • postMessage()
  • message 事件;
  • clients.matchAll()
  • MessageChannel

Service Worker 不能直接修改页面内容,需要把数据发送回页面,再由页面脚本操作 DOM。

4. 只能控制作用域内、已受控页面的请求

Service Worker 不是整个浏览器的全局代理。只有位于注册作用域内并且已经被当前 Service Worker 控制的页面,其请求才会触发对应的 fetch 事件。

首次注册后,当前页面通常需要刷新一次,或者由激活后的 Service Worker 使用 clients.claim() 立即接管。

极简化 Demo

下面的 Demo 展示一个最基本的注册、安装、预缓存和运行时缓存流程。

HTML 文件:index.html

页面加载时注册 Service Worker:

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Service Worker Demo</title>
  </head>
  <body>
    <h1>Service Worker Demo</h1>

    <script>
      if ('serviceWorker' in navigator) {
        window.addEventListener('load', async () => {
          try {
            const registration = await navigator.serviceWorker.register(
              '/service-worker.js'
            )

            console.log(
              'Service Worker registered with scope:',
              registration.scope
            )
          } catch (error) {
            console.error('Service Worker registration failed:', error)
          }
        })
      }
    </script>
  </body>
</html>

navigator.serviceWorker 存在只表示当前环境提供注册接口,注册仍可能因为 HTTPS、脚本路径、作用域或服务器响应错误而失败。

原文的兼容性截图来自较早版本的浏览器数据,只能作为历史参考,具体支持情况应以当前 MDN、Can I Use 或 Baseline 数据为准。

Service Worker 早期浏览器兼容性截图,仅作历史参考

Service Worker 文件:service-worker.js

const CACHE_NAME = 'my-cache-v1'
const PRECACHE_URLS = [
  '/',
  '/index.html'
]

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(PRECACHE_URLS)
    })
  )
})

self.addEventListener('fetch', (event) => {
  const request = event.request
  const url = new URL(request.url)

  // Cache Storage 通常只处理 GET 请求
  if (request.method !== 'GET') {
    return
  }

  // 这个 Demo 只缓存同源资源
  if (url.origin !== self.location.origin) {
    return
  }

  event.respondWith((async () => {
    const cache = await caches.open(CACHE_NAME)
    const cachedResponse = await cache.match(request)

    if (cachedResponse) {
      return cachedResponse
    }

    try {
      const networkResponse = await fetch(request)

      if (networkResponse.ok) {
        event.waitUntil(
          cache.put(request, networkResponse.clone())
        )
      }

      return networkResponse
    } catch {
      return Response.error()
    }
  })())
})

这个示例采用简单的 Cache First 策略:

  1. 先读取缓存;
  2. 有缓存则直接返回;
  3. 没有缓存则请求网络;
  4. 网络成功后写入缓存。

生产项目还需要增加离线页面、缓存版本清理、缓存过期、请求分类和更新提示。HTML 通常不建议永久使用 Cache First,否则可能长期读取旧页面。

生命周期和更新

Service Worker 的生命周期通常包括:

installing
    ↓
installed / waiting
    ↓
activating
    ↓
activated
    ↓
redundant

install

通常用于预缓存资源。传入 event.waitUntil() 的 Promise fulfilled 后,安装才算成功;如果 Promise rejected,安装可能失败。

waiting

如果旧版本仍控制着页面,新版本安装成功后通常会等待旧版本释放客户端。

activate

通常用于清理旧缓存、迁移数据和执行初始化:

const CURRENT_CACHES = new Set([
  'my-cache-v1',
  'my-runtime-v1'
])

self.addEventListener('activate', (event) => {
  event.waitUntil((async () => {
    const cacheNames = await caches.keys()

    await Promise.all(
      cacheNames
        .filter((name) => name.startsWith('my-'))
        .filter((name) => !CURRENT_CACHES.has(name))
        .map((name) => caches.delete(name))
    )
  })())
})

只删除自己应用的缓存,不要无条件删除当前源下的所有缓存,因为同一个源可能有多个应用或功能使用 Cache Storage。

skipWaiting()clients.claim()

self.addEventListener('install', () => {
  self.skipWaiting()
})

self.addEventListener('activate', (event) => {
  event.waitUntil(self.clients.claim())
})
  • skipWaiting() 让新版本跳过 waiting 阶段,尽快进入激活流程;
  • clients.claim() 让已激活的 Service Worker 接管当前作用域内尚未受控的页面;
  • clients.claim() 不会更新缓存;
  • 两者可能导致旧页面、新 Service Worker 和新缓存混合运行,应确认版本兼容后再使用。

手动检查更新

const registration = await navigator.serviceWorker.ready
await registration.update()

更新 Service Worker 时应尽量保持脚本 URL 稳定,通过脚本内容变化和缓存版本号完成更新,而不是不断改成 /sw-v2.js/sw-v3.js

缓存策略

下面是常见策略:

  1. Cache First:有缓存时返回缓存,没有时请求并缓存;
  2. Cache Only:只读取缓存,缓存没有则失败;
  3. Network First:先请求网络,失败或超时时回退缓存;
  4. Network Only:不使用 Cache Storage,始终请求网络;
  5. Stale While Revalidate:有缓存时立即返回,同时请求网络更新缓存,供下一次使用。

Cache Only

async function cacheOnly(request) {
  const cache = await caches.open('cache-only-v1')
  const response = await cache.match(request)

  if (!response) {
    throw new Error('Cache miss')
  }

  return response
}

Network Only

async function networkOnly(request) {
  return fetch(request)
}

适合登录、支付和必须获取实时结果的请求。非 GET 请求也通常不应写入 Cache Storage。

Network First

async function networkFirst(request) {
  const cache = await caches.open('network-first-v1')

  try {
    const response = await fetch(request)

    if (response.ok) {
      await cache.put(request, response.clone())
      return response
    }

    throw new Error(`HTTP ${response.status}`)
  } catch {
    const cachedResponse = await cache.match(request)

    if (cachedResponse) {
      return cachedResponse
    }

    throw new Error('Network and cache are both unavailable')
  }
}

fetch() 在收到 HTTP 404 或 500 时通常不会 reject,因此如果希望 HTTP 错误也回退缓存,需要主动检查 response.ok

Network First with timeout

function fetchWithTimeout(request, timeout = 3000) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), timeout)

  return fetch(request, { signal: controller.signal })
    .finally(() => clearTimeout(timer))
}

async function networkFirstWithTimeout(request) {
  const cache = await caches.open('network-timeout-v1')

  try {
    const response = await fetchWithTimeout(request)

    if (response.ok) {
      await cache.put(request, response.clone())
    }

    return response
  } catch {
    const cachedResponse = await cache.match(request)

    if (cachedResponse) {
      return cachedResponse
    }

    throw new Error('Network timeout and cache miss')
  }
}

超时策略要谨慎使用。请求被 AbortController 取消后,网络请求并不一定在服务器端停止,应用还应处理重试和重复提交问题。

Stale While Revalidate

async function staleWhileRevalidate(event) {
  const request = event.request
  const cache = await caches.open('stale-v1')
  const cachedResponse = await cache.match(request)

  const networkPromise = fetch(request).then(async (response) => {
    if (response.ok) {
      await cache.put(request, response.clone())
    }

    return response
  })

  event.waitUntil(networkPromise.catch(() => undefined))

  return cachedResponse || networkPromise
}

这种策略适合头像、图片和允许短暂过期的非关键内容。对于余额、订单、权限等数据,不应直接返回旧缓存。

Workbox 百宝箱

Workbox 是一组用于简化 Service Worker 路由和缓存的模块。每个模块负责 Service Worker 开发中的一个具体方面,可以减少重复的底层代码。

workbox-routing

用于匹配请求和路由:

import { registerRoute } from 'workbox-routing'

workbox-strategies

提供常用缓存策略:

  • CacheFirst
  • NetworkFirst
  • StaleWhileRevalidate
  • NetworkOnly
  • CacheOnly

workbox-precaching

用于预缓存构建产物。使用构建工具时,通常由构建流程生成资源清单:

import { precacheAndRoute } from 'workbox-precaching'

precacheAndRoute(self.__WB_MANIFEST)

workbox-expiration

用于限制缓存条目数和缓存时间:

import { ExpirationPlugin } from 'workbox-expiration'

workbox-window

这是在页面窗口上下文中使用的模块,可以帮助注册 Service Worker、监听更新和处理生命周期交互。它不是运行在 Service Worker 内部的缓存策略模块。

Workbox 示例

现代项目应通过 npm 和构建工具使用当前版本 Workbox:

import { registerRoute } from 'workbox-routing'
import { CacheFirst, NetworkFirst } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'

registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images-v1',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 7 * 24 * 60 * 60
      })
    ]
  })
)

registerRoute(
  ({ request }) => request.mode === 'navigate',
  new NetworkFirst({
    cacheName: 'pages-v1',
    networkTimeoutSeconds: 3
  })
)

Workbox 仍然需要开发者根据资源类型设计缓存策略。它不是自动适配所有项目的黑盒,也不能替代缓存版本、离线页面和错误降级设计。

Workbox 路由匹配示意图

Service Worker 与 Web Worker 傻傻分不清楚

不少同学使用过 Web Worker。两者都带有 Worker,它们是一回事吗?

相同点

  • 都运行在不同于页面主线程的 Worker 执行上下文中;
  • 都不能直接访问页面 DOM;
  • 都可以通过 postMessage()message 事件通信;
  • 都不能依赖 windowdocument 操作页面。

“线程池”“进程是否相同”等属于浏览器实现细节,不适合作为规范层面的结论。

不同点

可解决的问题不同

用户 A:

页面操作起来太卡,点击和滚动不流畅。

解决方案:使用 Web Worker,将高计算量任务移出页面主线程。

用户 B:

页面重复打开时加载很慢,或者没有网络时无法访问。

解决方案:使用 Service Worker 控制作用域内的请求、实现缓存和离线回退。

生命周期不同

  • Web Worker:通常由页面通过 new Worker() 创建,可以由页面调用 terminate() 销毁;
  • Service Worker:通过 register() 注册,由浏览器按生命周期和事件管理,空闲时可以被终止,需要事件时再启动;
  • Web Worker 通常只服务于创建它的页面;
  • Service Worker 可以控制作用域内的多个页面。

能力不同

能力Web WorkerService Worker
计算任务适合可以执行事件任务,但主要用途不是计算
拦截页面请求不负责可以处理受控页面作用域内的 fetch
离线缓存需要自行实现,不能接管页面导航常用能力
Push不负责可以监听 push
Background Sync不负责在支持的浏览器中可以监听 sync
DOM不能访问不能访问

Chrome Extension 的 Service Worker 又是什么?

Chrome Extension Manifest V3 使用 Service Worker 处理扩展的后台逻辑。它与普通网页 Service Worker 都采用 Worker 上下文和事件驱动机制,但运行环境、权限和可用 API 不同。

Chrome Extension Service Worker

主要用于:

  • 管理扩展生命周期;
  • 处理扩展事件;
  • 与弹出页面、选项页、内容脚本通信;
  • 使用 Chrome Extension API;
  • 保存和管理扩展状态。

扩展 Service Worker 同样可能在空闲时被浏览器终止,不能依赖内存中的全局变量保存持久状态。

普通 Web Service Worker

主要用于:

  • 缓存静态资源;
  • 提供离线访问;
  • 拦截受控页面请求;
  • 接收 Web Push;
  • 配合 Background Sync。

扩展 Service Worker 与普通 Web Service Worker 不是完全无关,也不是完全相同。它们共享 Worker 的基础执行模型,但属于不同的平台环境。

我能用 Service Worker 做什么?

1. 请求拦截与缓存管理

这是 Service Worker 最常见的用途。可以根据资源类型选择不同策略:

  • 静态资源:Cache First;
  • HTML:Network First;
  • 图片和头像:Stale While Revalidate;
  • 登录和支付请求:Network Only;
  • 必须离线的应用壳:Cache Only。

2. 推送通知

Push 通知需要完整链路:

  1. 用户授权通知权限;
  2. 页面创建 Push subscription;
  3. 将订阅信息发送到后端;
  4. 后端通过 Push 服务发送消息;
  5. Service Worker 监听 push 事件并显示通知。

Service Worker 本身不能主动向所有用户发送通知,也不能保证通知一定送达。

3. 后台同步

Background Sync 可以在网络恢复后重试短时、可重试的任务,例如离线消息队列。

self.addEventListener('sync', (event) => {
  if (event.tag === 'send-outbox') {
    event.waitUntil(flushOutbox())
  }
})

Background Sync 不是所有浏览器都支持,也不保证网络恢复后立即执行。应用必须提供页面重新打开后的降级重试方案。

4. 离线支持

PWA 可以通过 Service Worker 缓存应用壳、页面和部分资源,使用户在离线或弱网状态下继续使用部分功能。

离线视频也可以使用缓存,但需要额外考虑:

  • 视频文件大小和存储配额;
  • Range 请求;
  • 浏览器缓存能力;
  • 资源版权;
  • 用户是否允许占用本地空间。

上手注意事项

1. 调试是个大坑

开发时应熟悉浏览器 DevTools 的 Application 面板,可以查看:

  • Service Worker 注册状态;
  • 当前作用域;
  • waiting 和 active Worker;
  • Cache Storage;
  • IndexedDB;
  • Update on reload;
  • Skip waiting;
  • Unregister;
  • Clear storage。

2. 注销不会自动删除缓存

注销 Service Worker 和删除缓存是两个独立操作:

const registrations = await navigator.serviceWorker.getRegistrations()

await Promise.all(
  registrations.map((registration) => registration.unregister())
)

const cacheNames = await caches.keys()
await Promise.all(
  cacheNames
    .filter((name) => name.startsWith('my-'))
    .map((name) => caches.delete(name))
)

不一定必须“在注销前”清除缓存,但如果希望彻底移除应用数据,就必须显式删除相关缓存。

3. 缓存使用不当可能是负优化

缓存并不一定总能提升体验。需要考虑:

  • 首次安装预缓存会增加首次加载成本;
  • 过大的缓存会占用存储空间;
  • Cache First 可能导致内容更新不及时;
  • 缓存接口数据可能显示过期信息;
  • 错误的缓存策略可能影响 LCP 和页面可用性;
  • 关键请求不应被错误的缓存响应替代。

缓存策略应根据业务数据的实时性、离线需求和用户可接受的延迟来设计。

4. 缓存中不要保存敏感数据

不要把密码、访问令牌、支付信息等敏感内容直接写入 Cache Storage。退出登录时也应清理与用户相关的缓存和 IndexedDB 数据。

总结

本文保留了原文中关于 Service Worker、Web Worker、扩展 Service Worker、Workbox 和缓存策略的主要内容,并补充了几个必须掌握的限制:

  1. Service Worker 需要安全上下文,正式环境通常使用 HTTPS,localhost 可用于本地开发;
  2. Service Worker 不是永久运行的后台进程,空闲时可能被浏览器终止;
  3. 只有作用域内、已受控页面的请求才会触发 fetch
  4. install 适合预缓存,activate 适合清理旧缓存;
  5. event.waitUntil() 用于等待异步事件任务,event.respondWith() 用于提供请求响应;
  6. Web Worker 主要解决计算阻塞,Service Worker 主要处理缓存、离线和后台事件;
  7. Chrome Extension Service Worker 与普通 Web Service Worker 共享基础模型,但运行环境和权限不同;
  8. Workbox 可以简化缓存代码,但仍需要正确配置路由、策略、版本和过期时间;
  9. Push、Background Sync 和离线视频都有浏览器支持、权限、容量和可靠性限制;
  10. 缓存用得好可以改善重复访问体验,用错则可能造成数据过期、更新延迟和性能下降。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS