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

显示模式

登录
ARCHIVE DOCUMENTHTML

Web Worker 与 Service Worker 详解

所属分类
HTML
文件格式
Markdown
原始路径
HTML/06-web worker与service worker详解
本文目录11 个章节
  1. 目录
  2. 一、为什么需要 Worker
  3. 二、Web Worker 与 Service Worker 的关系
  4. 三、Web Worker
  5. 四、Service Worker
  6. 五、常见缓存策略
  7. 六、使用 Workbox
  8. 七、Web Worker 与 Service Worker 对比
  9. 八、常见问题和调试建议
  10. 九、总结
  11. 参考资料

Web Worker 与 Service Worker 详解

Category(分类): HTML Status: 未知

本文对原文进行了重新整理和勘误。示例代码使用现代 Web Worker、Service Worker 和 Vite/Webpack 兼容的写法,删除了网页复制残留、失效配置和不准确的生命周期描述。

目录

一、为什么需要 Worker

浏览器页面通常由主线程负责多项工作,包括:

  • 执行页面 JavaScript;
  • 处理用户输入和事件;
  • 计算样式和布局;
  • 更新 DOM;
  • 绘制页面。

如果主线程长时间执行排序、加密、压缩、图像处理等计算,就可能无法及时处理输入和绘制任务,页面会出现卡顿。将适合的计算任务交给 Worker,可以让页面主线程有更多时间处理交互和渲染。

需要注意两点:

  1. JavaScript 通常是“每个执行上下文单线程”,并不代表整个浏览器只有一个 JavaScript 线程;
  2. Worker 不能消除计算成本,只是将计算转移到其他执行上下文。创建 Worker、传输数据和调度线程本身也有成本,小任务不一定适合使用 Worker。

二、Web Worker 与 Service Worker 的关系

Web Worker 和 Service Worker 都属于 Worker 类型的执行环境,都不能直接访问页面 DOM,但设计目标不同:

对比项Web WorkerService Worker
主要目的把计算任务移出页面主线程代理网络请求、实现缓存和离线能力
启动方式页面主动调用 new Worker()页面调用 navigator.serviceWorker.register() 注册
作用范围通常只服务于创建它的页面可以控制作用域内的多个页面
生命周期通常与创建它的页面相关由浏览器按事件启动、暂停和终止
是否能访问 DOM不能不能
是否能拦截页面请求不能作为通用网络代理可以拦截受控页面作用域内的 Fetch 请求
常见 APIpostMessage()、terminate()、importScripts()install、activate、fetch、push、sync
常见用途大量计算、图像处理、数据分析离线缓存、PWA、通知和后台同步

Service Worker 可以看作一种具有特殊生命周期和事件模型的 Worker,但它不是 Web Worker 的简单替代品,也不是一个始终运行的后台线程。

三、Web Worker

3.1 什么是 Web Worker

Web Worker 是浏览器提供的后台 JavaScript 执行环境。浏览器通常会为它安排独立线程,但具体线程调度属于浏览器实现细节。

Dedicated Worker(专用 Worker)只能被创建它的页面或 Worker 访问。它适合执行与页面相关、但不需要直接操作 DOM 的计算任务。

Worker 创建后不会因为主线程继续执行而自动停止,但它也不是永久运行的进程:

  • 主线程可以调用 terminate() 立即终止它;
  • Worker 自身可以调用 self.close() 结束自己;
  • 页面关闭后,关联的 Worker 通常也会结束;
  • 浏览器可能对后台页面进行冻结或资源回收。

3.2 Web Worker 的限制

不能直接操作 DOM

Worker 中不能访问 window、document 和页面 DOM,也不能直接修改页面元素:

// Worker 中不可用
window.document.querySelector('#app')

正确方式是由 Worker 计算数据,再通过消息发送给页面,由页面更新 DOM。

不能使用大部分 Window API

Worker 没有页面的 window 对象,但可以使用部分专用全局对象和 API,例如:

  • self;
  • navigator;
  • location(表示 Worker 脚本地址);
  • fetch();
  • XMLHttpRequest(经典 Worker 中可用,但通常优先使用 fetch());
  • WebSocket;
  • IndexedDB;
  • Web Crypto。

alert()、confirm() 等依赖页面窗口的 API 不能在 Worker 中使用。

Worker 脚本需要满足 URL 和安全策略

直接使用 new Worker('worker.js') 时,脚本通常需要与创建它的页面同源。实际项目中还可能使用构建工具生成的资源、blob: URL 或模块 Worker。

不要直接依赖 file:// 打开 HTML 文件来测试 Worker。应通过本地 HTTP 服务器运行项目。

不能依赖普通全局变量保存持久状态

Worker 的全局变量只在当前 Worker 实例存活期间有效。需要持久保存的数据应放在页面端、IndexedDB 或其他合适的存储中。

主线程和 Worker 之间不能直接共享普通 JavaScript 对象

通信需要通过 postMessage(),数据默认使用结构化克隆算法复制。函数、DOM 节点等对象不能直接传递。

3.3 创建和通信

主线程代码

if (typeof Worker !== 'undefined') {
  const worker = new Worker('./worker.js')

  worker.postMessage({
    type: 'CALCULATE',
    numbers: [1, 2, 3, 4, 5]
  })

  worker.addEventListener('message', (event) => {
    console.log('收到 Worker 结果:', event.data)
  })

  worker.addEventListener('error', (event) => {
    console.error('Worker 发生错误:', event.message)
  })
}

worker.js

self.addEventListener('message', (event) => {
  const { type, numbers } = event.data || {}

  if (type !== 'CALCULATE' || !Array.isArray(numbers)) {
    return
  }

  const result = numbers.reduce((sum, value) => sum + value, 0)

  self.postMessage({
    type: 'RESULT',
    result
  })
})

也可以使用属性形式监听事件:

self.onmessage = (event) => {
  self.postMessage(`收到:${event.data}`)
}

页面和 Worker 两端都可以使用 addEventListener('message', ...) 或 onmessage。推荐统一约定消息格式,例如使用 type 字段区分不同命令。

3.4 终止和错误处理

主线程终止 Worker

const worker = new Worker('./worker.js')

// terminate() 会立即终止 Worker,不会等待当前任务完成
worker.terminate()

Worker 终止自身

self.close()

terminate() 和 close() 都会停止 Worker,但不会自动保存未完成的任务。需要保证数据完整性时,应在终止前设计明确的完成、取消和错误协议。

监听错误

const worker = new Worker('./worker.js')

worker.addEventListener('error', (event) => {
  console.error('Worker 错误:', event.message)
})

worker.addEventListener('messageerror', (event) => {
  console.error('Worker 消息无法反序列化:', event)
})

Worker 内部也可以监听错误,但很多错误会通过 Worker 实例的 error 事件传递给主线程,因此主线程应始终设置错误处理逻辑。

3.5 结构化克隆与可转移对象

postMessage() 默认使用结构化克隆算法传递数据。例如对象和数组可以直接发送:

worker.postMessage({
  name: 'demo',
  values: [1, 2, 3]
})

大数据复制可能消耗时间和内存。对于 ArrayBuffer、MessagePort、ImageBitmap 等可转移对象,可以使用第二个参数转移所有权,避免复制:

const buffer = new ArrayBuffer(1024)

worker.postMessage(buffer, [buffer])

// buffer 的所有权已经转移,主线程不能再使用它

转移对象后,发送方不再拥有该对象。使用 Transferable 时要确保后续代码不会继续访问已经转移的对象。

3.6 使用模块 Worker

现代项目通常推荐模块 Worker。模块 Worker 可以使用 import,并支持更清晰的模块组织。

主线程

const worker = new Worker(
  new URL('./math.worker.js', import.meta.url),
  { type: 'module' }
)

worker.postMessage({
  numbers: [10, 20, 30]
})

worker.onmessage = ({ data }) => {
  console.log('计算结果:', data)
}

math.worker.js

import { sum } from './math.js'

self.onmessage = ({ data }) => {
  self.postMessage(sum(data.numbers))
}
// math.js
export function sum(numbers) {
  return numbers.reduce((total, value) => total + value, 0)
}

importScripts() 只适用于经典 Worker,不适用于模块 Worker:

// Classic Worker 中可以使用
importScripts('./helper.js')

// Module Worker 中应使用 import
import { helper } from './helper.js'

3.7 在 Vite、Vue 和 Nuxt 中使用

现代 Vite、Webpack 5 和 Nuxt 项目一般不需要安装 worker-loader,可以直接使用:

const worker = new Worker(
  new URL('./workers/task.worker.js', import.meta.url),
  { type: 'module' }
)

Vite 也支持 ?worker 方式:

import TaskWorker from './workers/task.worker.js?worker'

const worker = new TaskWorker()

在 Vue 组件中,Worker 应在客户端创建,并在组件卸载时终止:

<script setup>
import { onBeforeUnmount, onMounted } from 'vue'

let worker

onMounted(() => {
  worker = new Worker(
    new URL('./workers/task.worker.js', import.meta.url),
    { type: 'module' }
  )

  worker.postMessage({ type: 'START' })
})

onBeforeUnmount(() => {
  worker?.terminate()
})
</script>

在 Nuxt 等 SSR 框架中,window、Worker 等浏览器 API 不能在服务端执行。可以使用 onMounted()、客户端插件或 import.meta.client 确保代码只在浏览器运行。

3.8 Web Worker 的适用场景

适合使用 Web Worker 的场景包括:

  1. 大量数据的排序、过滤、聚合和统计;
  2. 加密、解密、哈希和压缩;
  3. 复杂的文本解析、CSV/JSON 处理;
  4. 图像像素处理和部分音视频计算;
  5. 复杂算法、路径规划和数据预计算;
  6. 使用 OffscreenCanvas 进行支持的 Canvas 绘制任务。

需要注意:

  • Worker 不能直接操作普通 DOM;
  • 普通 Canvas 不能直接交给 Worker 使用,通常需要 OffscreenCanvas;
  • Worker 不能直接创建页面中的 <img> 元素,但可以请求图像数据或创建 ImageBitmap;
  • 轻量计算不一定值得创建 Worker,应考虑数据传输和启动成本。

四、Service Worker

4.1 什么是 Service Worker

Service Worker 是一种事件驱动的 Worker。它位于 Web 应用、浏览器和网络之间,可以在页面处于其控制范围且满足条件时处理网络请求。

它主要用于:

  • 预缓存应用壳和静态资源;
  • 根据策略从缓存或网络返回响应;
  • 提供离线页面和弱网体验;
  • 接收 Push 事件并显示通知;
  • 配合 Background Sync 重试短时网络任务。

Service Worker 不是永久运行的后台进程,也不是普通 Web Worker 的“常驻版本”。浏览器可以终止空闲的 Service Worker,需要处理事件时再重新启动它。

4.2 Service Worker 的能力和限制

Service Worker 可以访问:

  • self;
  • caches;
  • fetch();
  • clients;
  • registration;
  • indexedDB;
  • push、sync、notificationclick 等事件。

Service Worker 不能直接访问:

  • window;
  • document;
  • 页面 DOM;
  • 页面当前 URL 对应的 window.location。

Service Worker 中的 location 是 Worker 脚本本身的地址。如果需要查看或操作页面,应使用 clients.matchAll()、postMessage() 等机制与页面通信。

Service Worker 可以拦截请求,但不是任意网络请求的全局代理。只有被当前 Service Worker 控制、并且位于注册作用域内的页面请求,才会触发对应的 fetch 事件。

4.3 安全上下文和作用域

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

Service Worker 的默认作用域是脚本所在目录及其子目录:

脚本地址默认作用域
/sw.js/
/app/sw.js/app/
/static/sw.js/static/

下面的注册方式只能让脚本控制 /app/:

navigator.serviceWorker.register('/app/sw.js')

如果要让位于子目录的脚本控制更上层的路径,服务端需要返回:

Service-Worker-Allowed: /

作用域越大,Service Worker 能拦截的请求越多,应避免不必要地设置过大的作用域。

4.4 注册 Service Worker

页面端注册:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js', {
        scope: '/'
      })

      console.log('Service Worker 注册成功:', registration.scope)
    } catch (error) {
      console.error('Service Worker 注册失败:', error)
    }
  })
}

register() 返回一个 Promise。注册成功后,浏览器还要完成安装和激活,当前页面也不一定马上受其控制。

等待一个已经激活的注册对象:

const registration = await navigator.serviceWorker.ready
console.log('Service Worker 已准备就绪:', registration.scope)

注册路径应保持稳定。更新 Service Worker 时,通常保持 /sw.js 不变,让浏览器重新检查脚本内容,而不是不断改成 /sw-v2.js、/sw-v3.js。

4.5 生命周期

Service Worker 的生命周期状态通常为:

installing
    ↓
installed / waiting
    ↓
activating
    ↓
activated
    ↓
redundant

Service Worker 生命周期示意图

fetch、push、sync 和 message 是激活后的功能事件,不是生命周期状态。

installing

浏览器获取到新脚本后创建新的 Worker 实例,并触发 install。通常在这里执行预缓存。

waiting

如果旧版本仍控制着页面,新版本安装成功后通常会处于 waiting 状态,等待旧版本不再控制客户端。

activating

旧版本退出控制后,新版本进入激活阶段。通常在这里删除旧缓存、迁移数据或执行初始化任务。

activated

激活后的 Service Worker 可以处理被控制页面的 fetch、message、push 等事件。

redundant

安装失败或被更新版本替代的 Worker 会进入 redundant 状态。

Service Worker 实例可以在空闲时被浏览器终止,但这不会自动删除它的注册信息或缓存数据。缓存数据仍可能被用户清理或浏览器因存储压力回收。

4.6 安装与预缓存

在 sw.js 中预缓存应用壳和离线页面:

const APP_PREFIX = 'demo-app-'
const STATIC_CACHE = `${APP_PREFIX}static-v1`

const PRECACHE_URLS = [
  '/',
  '/offline.html',
  '/app.js',
  '/styles.css'
]

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

event.waitUntil() 会延长 install 事件的生命周期。如果 Promise rejected,安装通常会失败,新版本不会进入可用状态。

cache.addAll() 中只要有一个资源请求失败,整个操作就可能失败。因此预缓存列表应只包含稳定存在、确实需要离线使用的资源。

不建议在安装阶段缓存大量接口数据或所有图片。变化频繁的内容应在运行时根据请求逐步缓存。

4.7 激活与清理旧缓存

const APP_PREFIX = 'demo-app-'
const STATIC_CACHE = `${APP_PREFIX}static-v1`
const RUNTIME_CACHE = `${APP_PREFIX}runtime-v1`
const CURRENT_CACHES = new Set([
  STATIC_CACHE,
  RUNTIME_CACHE
])

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

    await Promise.all(
      cacheNames
        .filter((name) =>
          name.startsWith(APP_PREFIX) && !CURRENT_CACHES.has(name)
        )
        .map((name) => caches.delete(name))
    )
  })())
})

删除缓存时不要无条件执行 caches.keys().then(keys => keys.map(caches.delete)),因为同一源下可能有多个应用或多个功能使用 Cache Storage。应使用应用专属前缀。

4.8 拦截请求

一个基础的运行时缓存示例:

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

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

  // 当前示例只缓存同源资源
  if (url.origin !== self.location.origin) {
    return
  }

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

    if (cachedResponse) {
      return cachedResponse
    }

    const networkResponse = await fetch(request)

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

    return networkResponse
  })())
})

需要注意:

  • respondWith() 必须提供 Response 或 Promise;
  • Request 或 Response 被读取后通常需要使用 clone();
  • Cache Storage 不适合直接缓存 POST、PUT 等请求;
  • fetch() 收到 HTTP 404、500 时通常不会 reject,是否回退缓存需要自行判断 response.ok;
  • 跨域响应涉及 CORS 和 opaque response,不应简单套用同源缓存逻辑;
  • event.waitUntil() 用于保证后台缓存写入等异步任务有机会完成。

页面导航通常需要单独使用 Network First,并提供离线页面:

self.addEventListener('fetch', (event) => {
  if (event.request.mode !== 'navigate') {
    return
  }

  event.respondWith((async () => {
    try {
      const response = await fetch(event.request)

      if (response.ok) {
        const cache = await caches.open(RUNTIME_CACHE)
        event.waitUntil(cache.put(event.request, response.clone()))
      }

      return response
    } catch {
      const cache = await caches.open(STATIC_CACHE)
      return (await cache.match('/offline.html')) || Response.error()
    }
  })())
})

4.9 更新策略

浏览器会在注册、导航和部分功能事件等时机检查 Service Worker 脚本是否更新。脚本 URL 不变但文件内容发生变化时,也可以安装新版本。

更新过程通常是:

旧版本 activated
        ↓ 检查到 sw.js 内容变化
新版本 install
        ↓
新版本 waiting
        ↓ 旧版本不再控制客户端
新版本 activate

skipWaiting()

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

skipWaiting() 会让新版本尽快跳过 waiting,但可能造成旧页面代码、新 Service Worker 和新缓存混合使用。只有确认版本兼容时才应使用。

clients.claim()

self.addEventListener('activate', (event) => {
  event.waitUntil(self.clients.claim())
})

clients.claim() 会让已激活的 Service Worker 接管当前作用域内尚未受控的页面。它不会更新缓存,也不能代替版本兼容性设计。

主动检查更新

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

也可以通过 registration.updatefound 和 Worker 的 statechange 监听更新过程,并在发现新版本后提示用户刷新。

4.10 页面与 Service Worker 通信

页面发送消息:

navigator.serviceWorker.controller?.postMessage({
  type: 'CLEAR_RUNTIME_CACHE'
})

navigator.serviceWorker.addEventListener('message', (event) => {
  console.log('来自 Service Worker:', event.data)
})

Service Worker 接收消息:

self.addEventListener('message', async (event) => {
  if (event.data?.type !== 'CLEAR_RUNTIME_CACHE') {
    return
  }

  await caches.delete(RUNTIME_CACHE)
  event.source?.postMessage({
    type: 'RUNTIME_CACHE_CLEARED'
  })
})

Service Worker 不能直接操作页面 DOM。需要改变页面内容时,应将数据发送回页面,由页面脚本执行 DOM 操作。

4.11 Push 与通知

Notification API 和 Push API 是相关但不同的能力:

  • Notification API:创建和显示通知;
  • Push API:接收推送消息;
  • Service Worker:在后台接收 push 事件并调用 showNotification()。

请求通知权限应尽量放在用户点击按钮等用户手势中:

async function requestNotificationPermission() {
  if (!('Notification' in window)) {
    return false
  }

  const permission = await Notification.requestPermission()
  return permission === 'granted'
}

Service Worker 中处理 Push:

self.addEventListener('push', (event) => {
  const data = event.data?.json() || {
    title: '新消息',
    body: '你收到了一条新消息'
  }

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192.png'
    })
  )
})

真正的 Web Push 还需要:

  1. 页面创建 Push subscription;
  2. 将订阅信息发送到后端;
  3. 后端使用 VAPID 等方式通过 Push 服务发送消息;
  4. 浏览器唤醒 Service Worker 并触发 push 事件。

通知点击处理:

self.addEventListener('notificationclick', (event) => {
  event.notification.close()

  event.waitUntil((async () => {
    const clients = await self.clients.matchAll({
      type: 'window',
      includeUncontrolled: true
    })

    const client = clients.find((item) => item.url === self.registration.scope)

    if (client) {
      await client.focus()
    } else {
      await self.clients.openWindow(self.registration.scope)
    }
  })())
})

推送和通知都受浏览器权限、操作系统设置、浏览器策略和网络环境影响,不能保证一定送达或立即显示。

4.12 Background Sync

Background Sync 可以将短时、可重试的任务延迟到网络恢复后执行,例如发送离线消息队列。

页面端注册任务:

async function registerOutboxSync() {
  const registration = await navigator.serviceWorker.ready

  if (!('sync' in registration)) {
    // 降级:在下次打开页面时主动重试
    return false
  }

  await registration.sync.register('send-outbox')
  return true
}

Service Worker 端处理:

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

async function flushOutbox() {
  // 从 IndexedDB 读取待发送数据
  // 发送成功后删除队列数据
  // 发送失败时抛出异常,以便后续重试
}

Background Sync 不是所有浏览器都支持,也不保证网络恢复后立即执行。它适合短任务,不适合大文件下载或必须严格按时完成的任务。应用必须提供普通页面重试等降级方案。

五、常见缓存策略

策略处理流程适合场景
Cache Only只从缓存读取已预缓存的应用壳
Network Only只从网络读取登录、支付、强实时接口
Cache First先缓存,未命中再访问网络带版本指纹的静态资源、图片、字体
Network First先访问网络,失败后读取缓存HTML、文章、需要较新数据的内容
Stale While Revalidate立即返回缓存,同时请求网络更新头像、图片、非关键内容
Network Race缓存和网络竞争特定性能场景,需要自行处理失败情况

“渐进式缓存”通常是运行时逐步缓存资源的开发模式,不是浏览器内置的独立缓存策略。“Network Race”也不是 Service Worker 的内置策略。

选择策略时应考虑:

  • 内容是否允许过期;
  • 是否必须支持离线;
  • 请求是否包含用户私密数据;
  • 资源是否有文件指纹;
  • 网络错误和 HTTP 错误如何处理;
  • 缓存未命中时返回什么降级内容。

六、使用 Workbox

Workbox 是用于构建 Service Worker 的工具库,提供预缓存、运行时路由、缓存过期和常用策略实现。

现代项目应使用当前版本的 Workbox,并通过 npm 和构建工具集成,不建议继续复制旧版 workbox-sw.js CDN 示例或安装很早期的 worker-loader 方案。

模块化 Workbox 示例:

import { registerRoute } from 'workbox-routing'
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } 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
  })
)

registerRoute(
  ({ request }) =>
    request.destination === 'style' || request.destination === 'script',
  new StaleWhileRevalidate({
    cacheName: 'assets-v1'
  })
)

networkTimeoutSeconds 是正确的配置名,不能写成 networkTimetoutSeconds。Workbox 仍然需要根据资源类型配置路由,不能对所有请求使用同一种策略。

七、Web Worker 与 Service Worker 对比

可以从下面几个问题快速判断应该使用哪一种:

需要处理大量计算,但页面仍需要保持流畅

使用 Web Worker:

const worker = new Worker(
  new URL('./calculate.worker.js', import.meta.url),
  { type: 'module' }
)

需要拦截请求、实现离线页面

使用 Service Worker:

navigator.serviceWorker.register('/sw.js')

需要在多个标签页之间共享计算任务

可以考虑 Shared Worker,但需要检查浏览器支持情况和业务复杂度。Shared Worker 与 Service Worker 的生命周期、通信方式和用途都不同,不能直接互换。

需要后台推送通知

使用 Push API + Service Worker,并准备后端推送服务和权限降级方案。

八、常见问题和调试建议

1. 为什么 Worker 代码不能访问 window

Worker 没有页面 Window 全局对象。应使用 self,并通过消息让页面执行 DOM 操作。

2. 为什么 Service Worker 注册成功但 fetch 没有触发

常见原因包括:

  • 当前页面不在 Service Worker 作用域内;
  • 页面尚未被该 Worker 控制,首次注册后没有刷新;
  • 脚本返回了 HTML 错误页而不是 JavaScript;
  • 页面不是 HTTPS 或 localhost;
  • 旧 Service Worker 仍处于控制状态;
  • 请求被代码中的方法、来源或路径过滤条件排除了。

3. 为什么缓存没有永久保存

Cache Storage 不是永久存储。用户清理网站数据、浏览器存储空间不足或存储配额变化,都可能导致缓存被删除。应用必须能够处理缓存未命中。

4. 为什么 Service Worker 更新后页面仍然使用旧资源

可能是以下原因:

  • 新 Worker 处于 waiting 状态;
  • HTML 使用了过于激进的 Cache First;
  • 旧页面仍被旧 Worker 控制;
  • 静态资源文件名没有版本指纹;
  • 开发者工具缓存或浏览器 HTTP 缓存仍然生效。

5. 如何调试 Service Worker

Chrome DevTools 的 Application 面板可以查看:

  • Service Workers 的注册、状态和作用域;
  • Cache Storage 的内容;
  • IndexedDB 数据;
  • Update on reload;
  • Skip waiting;
  • Unregister;
  • Clear storage。

这些选项主要用于开发调试,不能代替正式环境的版本更新方案。

6. 如何避免资源泄漏

  • 页面销毁时调用 worker.terminate();
  • 不要无限创建 Worker;
  • Service Worker 缓存使用版本号;
  • 在 activate 中删除旧缓存;
  • 限制图片和接口缓存的数量与有效期;
  • 不要将访问令牌、密码等敏感数据放进缓存。

九、总结

  1. Web Worker 主要解决页面主线程计算阻塞问题;
  2. Service Worker 主要解决网络代理、缓存、离线和后台事件问题;
  3. 两者都不能直接访问页面 DOM;
  4. Worker 通过 postMessage() 通信,复杂数据可以使用 Transferable 减少复制;
  5. Service Worker 需要 HTTPS 或 localhost,并且受注册作用域和页面控制状态限制;
  6. Service Worker 不是永久运行进程,不能依赖内存全局变量保存持久数据;
  7. 缓存策略应根据资源实时性、隐私和离线需求选择;
  8. skipWaiting() 和 clients.claim() 应谨慎组合,避免新旧页面和缓存版本混用;
  9. Push、Background Sync 和 Periodic Background Sync 都需要特性检测和降级方案;
  10. 新的 Vue、Vite、Webpack 和 Nuxt 项目通常不需要 worker-loader;
  11. 发布前应测试首次访问、刷新、更新、离线、缓存清理、权限拒绝和多标签页场景。

参考资料

807 DOCUMENTS · 12 COLLECTIONS
ARCHIVE SEARCH807 篇文章