Service Worker 缓存:离线优先进阶

FreeGuideOnline 最新 2026-06-15

为什么需要 Service Worker 缓存?

在移动优先、网络环境多变的今天,仅靠浏览器默认的 HTTP 缓存已经无法满足“秒开”、“离线可用”的体验要求。Service Worker 缓存让你以 JavaScript 代码精确控制网络请求的拦截、响应和存储,是实现离线优先、提升性能、节省流量的关键技术。

本教程带你从零掌握 Service Worker 缓存的核心概念、常用策略和工程化实践,真正实现“离线优先进阶”。

什么是 Service Worker?

Service Worker 是独立于网页运行的脚本,充当浏览器与网络之间的代理。它可以拦截页面发出的所有网络请求,根据需要返回缓存内容、发出网络请求或进行自定义处理。其特点包括:

  • 基于 HTTPS(本地开发可用 localhost)
  • 独立线程,不阻塞 UI
  • 事件驱动,激活后可以持久运行
  • 拥有自己的存储空间,可访问 Cache API 和 IndexedDB

缓存功能是 Service Worker 最核心的能力,通过 Cache API 实现资源的持久化存储。

缓存的核心 API:Cache Storage

Service Worker 处理缓存主要依赖两个接口:

  • CacheStorage:全局的 caches 对象,用来管理多个命名缓存空间(类似目录)。
  • Cache:单个缓存空间,存放 Request / Response 键值对。

常用方法:

// 打开或创建一个缓存空间
const cache = await caches.open('my-cache-v1');

// 将请求/响应对加入缓存
await cache.add('/app.js');                      // 请求并缓存
await cache.addAll(['/app.js', '/style.css']);   // 批量添加
await cache.put(request, response);              // 手动存入

// 从缓存匹配请求
const cachedResponse = await cache.match(request);

// 全局匹配:在所有缓存空间中查找
const response = await caches.match(request);

// 删除缓存项或整个空间
await cache.delete(request);
await caches.delete('my-cache-v1');

Response 对象只能被读取一次,如果需要同时返回给页面和存入缓存,必须使用 response.clone()

Service Worker 生命周期与缓存管理

Service Worker 的生命周期直接决定了缓存的最佳操作时机。

1) 安装阶段 (install)

当用户首次访问、或 SW 脚本发生字节变化时,会触发 install 事件。这是预缓存的关键时机,适合缓存应用骨架(App Shell)和关键静态资源。

const PRECACHE = ['/','/css/app.css','/js/app.js'];

self.addEventListener('install', event => {
  event.waitUntil(
    caches.open('v1.0.0')
      .then(cache => cache.addAll(PRECACHE))
      .then(() => self.skipWaiting()) // 可选:立即激活
  );
});

2) 激活阶段 (activate)

install 完成后 SW 不会立即控制页面,而是进入 waiting 状态,直到旧 SW 释放。activate 事件触发后,可以安全地清理旧版本缓存

self.addEventListener('activate', event => {
  const currentCaches = ['v1.0.0'];
  event.waitUntil(
    caches.keys()
      .then(keys => Promise.all(
        keys.filter(key => !currentCaches.includes(key))
            .map(key => caches.delete(key))
      ))
      .then(() => self.clients.claim()) // 让新 SW 立即控制所有客户端
  );
});

3) 请求拦截阶段 (fetch)

SW 接管页面后,每个网络请求都会触发 fetch 事件,在这里可以自由应用缓存策略

self.addEventListener('fetch', event => {
  // 必须用 event.respondWith 返回你的响应
  event.respondWith(/* 自定义策略 */);
});

公共缓存策略详解

理解每种策略的适用场景,是离线进阶的关键。

Cache First(缓存优先)

优先读取缓存,缓存未命中时发起网络请求,适用于不常变的静态资源(CSS、JS 及字体等)。

async function cacheFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;
  const response = await fetch(request);
  const cache = await caches.open('static');
  cache.put(request, response.clone());
  return response;
}

Network First(网络优先)

优先尝试网络,网络失败或超时回退到缓存,适合展示最新内容的 API 或 HTML 页面。

async function networkFirst(request) {
  const cache = await caches.open('dynamic');
  try {
    const response = await fetch(request);
    cache.put(request, response.clone());
    return response;
  } catch (err) {
    const cached = await cache.match(request);
    return cached || caches.match('/offline.html');
  }
}

Stale-While-Revalidate(后台更新)

立即返回缓存(如果有),同时在后台发起网络请求更新缓存,下次访问时展示新内容。兼顾速度与新鲜度。

async function staleWhileRevalidate(request) {
  const cache = await caches.open('dynamic');
  const cached = await cache.match(request);
  const fetchPromise = fetch(request).then(response => {
    cache.put(request, response.clone());
    return response;
  });
  return cached || fetchPromise;
}

Network Only / Cache Only

不常用,主要用于调试或特定资源策略。

实现离线优先:预缓存关键资源

离线优先的核心是在 install 阶段提前将应用“壳”缓存到设备,使得在无网络时页面仍可完整渲染。

  1. 明确 App Shell:确定最小化 HTML、CSS、JS、LOGO 等资源构成可用骨架。
  2. 构建时生成预缓存清单:手动维护清单容易出错,推荐使用 Workbox、webpack 插件自动生成哈希版本化的资源列表。
  3. 在 install 中执行 cache.addAll,保证这些资源永远能被离线访问。

示例清单生成(使用 Workbox):

import {precacheAndRoute} from 'workbox-precaching';
precacheAndRoute(self.__WB_MANIFEST);

提示:永远不要预缓存过多的动态资源,版本变更会导致频繁下载。

运行时缓存动态内容

对于用户数据、API 结果、CDN 图片等运行时资源,更适合在 fetch 事件中按策略动态缓存。

常见实践:

  • API 响应:使用 Network First 或 Stale-While-Revalidate,保证数据准确性。
  • 图片/字体:使用 Cache First,减少重复下载。
  • 用户头像/第三方资源:使用 Stale-While-Revalidate,在速度和更新间平衡。
  • 不可变资源(带哈希文件):使用 Cache First 或仅靠预缓存即可。

配置示例(使用 Workbox 路由):

import {registerRoute} from 'workbox-routing';
import {NetworkFirst, CacheFirst, StaleWhileRevalidate} from 'workbox-strategies';

registerRoute(
  /\/api\/.*/,
  new NetworkFirst({ cacheName: 'api-cache' })
);
registerRoute(
  /\.(?:png|jpg|jpeg|svg|gif)$/,
  new CacheFirst({ cacheName: 'image-cache' })
);

更新 Service Worker 与缓存清理

SW 本身不随页面刷新更新,浏览器会持续检测脚本的字节变化。当你发布新版本时需注意:

  • 要求页面刷新:默认情况下,新 SW 安装后会等待旧 SW 失效(用户关闭所有标签页)。skipWaiting() 结合 clients.claim() 可实现平滑更新,但需要配合页面刷新逻辑。
  • 缓存版本化管理:更改缓存名称(如 v2),在 activate 事件中删除旧版。
  • 提示用户更新:监听 controllerchange 事件或在 SW 内通过 postMessage 通知页面,显示“有新版本,点击刷新”的提示。
// 页面监听更新
navigator.serviceWorker.addEventListener('controllerchange', () => {
  window.location.reload();
});

实战:打造一个离线可用的应用

完整代码骨架:

sw.js

const CACHE_NAME = 'app-v1';
const PRECACHE_URLS = ['/','/main.css','/app.js'];

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

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys => Promise.all(
      keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))
    )).then(() => self.clients.claim())
  );
});

self.addEventListener('fetch', event => {
  if (event.request.method !== 'GET') return;
  event.respondWith(
    caches.match(event.request).then(cached => {
      const fetched = fetch(event.request).then(response => {
        const respClone = response.clone();
        caches.open(CACHE_NAME).then(cache => cache.put(event.request, respClone));
        return response;
      }).catch(() => cached);
      return cached || fetched;
    })
  );
});

注册 Service Worker

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => console.log('SW 注册成功'))
      .catch(err => console.error('SW 注册失败', err));
  });
}

此示例采用 Stale-While-Revalidate 风格混合:有缓存直接返回,同时后台更新;无缓存则网络请求并缓存。

调试与常见坑

  • 使用 Chrome DevTools:Application → Service Workers 面板可查看状态、强制更新、模拟离线。
  • 缓存存储查看:Application → Cache Storage 可浏览精确内容。
  • 更改 SW 后不生效:确认文件无语法错误,勾选 DevTools 中的 “Update on reload”。
  • 过期的缓存页面:通过版本化缓存名解决,切勿改变同一缓存名中资源的含义。
  • 跨域资源:跨域资源需设置 mode: 'cors' 才能成功缓存 Response
  • 存储空间限制:浏览器对每个源有配额,尽早清理无用缓存。
  • 敏感数据:不要在缓存中存储用户私有信息,或在注销时清除相关缓存。

Service Worker 缓存是构建现代 Web 应用的基石之一。理解其生命周期、巧妙组合缓存策略,你就能为用户提供几乎瞬时的加载体验和真正的离线支持。进阶推荐使用 Workbox 等工具降低维护成本,使缓存方案更健壮、更易扩展。