登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

Service Worker 怎样只缓存同源静态资源

来源:17golang原创

时间:2026-10-23 11:02:56 189浏览 收藏

Service Worker 只缓存同源静态资源,关键不是把每一次 fetch 都交给缓存,而是在 fetch 事件里先判断请求的 origin,再用 Request.destination 限定资源类型。命中缓存就直接返回;未命中时请求网络,并且只把状态正常、确实属于静态资源的响应副本放进缓存。

官方地址:https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API

要点速览
  • 同源判断使用 new URL(event.request.url).origin === self.location.origin,不要只比较字符串前缀。
  • 用 Request.destination 允许 style、script、image、font 等静态资源,排除 document、fetch 和 empty。
  • 缓存 API 不会自动按 HTTP 缓存头更新条目,缓存名要版本化,并在 activate 中清理旧版本。

先确认 Service Worker 为什么没有接管请求

遇到“缓存没有生效”时,第一步不是修改 cache.put(),而是确认 Service Worker 能否运行。它属于 Worker,不能访问 DOM;注册文件必须来自安全上下文,通常是 HTTPS,开发时的 http://localhost 也被浏览器视为安全上下文。注册脚本的目录还决定默认作用域,脚本放在过深的目录时,页面可能根本不在它的控制范围内。

注册代码可以保持很小,把路径和作用域写清楚:

if ('serviceWorker' in navigator) {
  // 注册路径应与实际脚本位置一致,scope 决定它能控制哪些页面。
  navigator.serviceWorker.register('/sw.js', { scope: '/' })
    .then((registration) => {
      console.log('Service Worker registered:', registration.scope);
    })
    .catch((error) => {
      // 注册失败先检查 HTTPS、脚本路径和作用域,而不是假设缓存逻辑有问题。
      console.error('Service Worker registration failed:', error);
    });
}

首次注册完成后,当前页面不一定马上由新 Worker 控制;页面刷新一次或使用 clients.claim() 都可能影响观察结果。这个现象常被误判成“同源条件没有命中”。

Service Worker 中请求 origin、作用域和静态资源类型筛选关系的原创结构说明图
图1:同源与静态资源筛选的结构说明图,不是运行截图。

用 origin 和 destination 把缓存范围收窄

真正的筛选逻辑放在 fetch 事件里。new URL(request.url).origin 能把协议、主机和端口作为整体比较;相比用 startsWith('/assets/'),它不会因为路径前缀相似就把外部地址误纳入。

Request.destination 由浏览器描述请求将要用于什么目的,例如样式表、脚本、图片、字体或文档。示例只允许静态资源目的,明确跳过页面导航和接口请求:

const CACHE_NAME = 'static-assets-v1';
const STATIC_DESTINATIONS = new Set([
  'style',
  'script',
  'image',
  'font',
  'audio',
  'video'
]);

self.addEventListener('fetch', (event) => {
  const request = event.request;
  const requestURL = new URL(request.url);
  const sameOrigin = requestURL.origin === self.location.origin;

  // 只处理同源静态资源;document、fetch 和空 destination 留给网络层。
  if (!sameOrigin || !STATIC_DESTINATIONS.has(request.destination)) {
    return;
  }

  event.respondWith(cacheFirst(request));
});

这里的“同源”包含协议、主机和端口。即使两个地址看起来都来自同一个站点,只要端口不同,也不能当作同源。接口请求的 destination 通常是 empty,但不要只依赖这一点;origin 与资源用途应当同时判断。

Service Worker fetch、Cache.match、网络响应与 Cache.put 的静态关系原创说明图
图2:缓存命中、网络回源与响应副本的关系说明图,不是运行截图。

缓存优先只保存可以复用的响应

对静态资源可以采用缓存优先:先查当前版本的 Cache,命中就返回;未命中时访问网络,收到成功响应后克隆一份写入缓存,原响应继续返回给页面。因为响应体只能被消费一次,写入缓存和返回页面必须使用不同的副本。

async function cacheFirst(request) {
  const cache = await caches.open(CACHE_NAME);
  const cachedResponse = await cache.match(request);

  if (cachedResponse) {
    // 命中缓存直接返回,避免静态资源每次都访问网络。
    return cachedResponse;
  }

  const networkResponse = await fetch(request);

  // 只缓存成功响应,避免把 404 或服务器错误固定在缓存里。
  if (networkResponse.ok) {
    await cache.put(request, networkResponse.clone());
  }

  // 返回原响应给页面,clone 只用于保存另一份响应体。
  return networkResponse;
}

Cache 的条目不会因为 HTTP 缓存头自动更新,也不会自动过期;应用需要自己决定何时替换或删除它们。若静态文件名不带内容哈希,部署新版本时更要更换缓存名,否则旧脚本或旧样式可能继续被命中。

用版本化缓存处理更新和旧条目

把清理动作放到 activate,保留当前版本,其余名称全部删除。event.waitUntil() 用来延长生命周期,确保异步清理完成前事件不会被过早结束:

self.addEventListener('activate', (event) => {
  const keepCaches = new Set([CACHE_NAME]);

  event.waitUntil(
    caches.keys().then((cacheNames) => Promise.all(
      cacheNames.map((cacheName) => {
        // 只删除不在当前白名单中的旧缓存,避免误删其他用途的缓存。
        if (!keepCaches.has(cacheName)) {
          return caches.delete(cacheName);
        }
        return undefined;
      })
    ))
  );
});

排查时可以按这个顺序观察:注册是否成功、当前页面是否已被控制、请求是否同源、destination 是否在白名单、缓存名是否切换、网络响应是否为 ok。任意一层不满足,后面的 Cache.match 都不会给出预期结果。

相关问题

为什么接口也被 Service Worker 拦截了?

如果只监听了 fetch 却没有检查 destination,接口请求同样会进入处理器。为静态缓存建立明确白名单,并让 empty 等类型直接返回。

跨域 CDN 资源能不能放进这个缓存?

本文的目标是只缓存同源资源,所以跨源请求直接跳过。如果业务确实需要缓存 CDN,应单独确认 CORS、响应类型和缓存策略,不要把它混入同源白名单。

为什么改了代码还读到旧文件?

通常是旧缓存名仍在使用、旧 Worker 仍处于 waiting,或当前页面还没有被新 Worker 控制。提升缓存版本并在 activate 清理旧名,再重新加载受控页面。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>