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

Service Worker Cache 怎样按请求方法过滤缓存

来源:17golang原创

时间:2026-11-03 14:12:35 166浏览 收藏

Service Worker 中按请求方法过滤 Cache,最直接的做法是在 fetch 监听器入口检查 request.method === "GET",只有符合业务白名单的请求才调用 event.respondWith 并读写缓存。不要把 POST 转成 GET 再存,也不要用 ignoreMethod: true 当作允许写入其他方法的开关。方法过滤与资源是否适合缓存,是两层不同的决定。

规范地址:https://w3c.github.io/ServiceWorker/

先确定哪些请求属于缓存接口

假设一个资料阅读器同时请求公共样式、公共脚本和提交收藏的接口。这里是原创设计场景,不是线上事故记录。我的选择是让缓存只负责两个带版本名的公共静态资源,把收藏提交留给正常网络路径。这样离线样式可以复用,提交动作不会误收到一份旧的静态响应。

GET 是必要条件,却不是“可以长期保存”的证明。用户资料、带身份信息的响应和动态业务接口不属于本例的白名单。服务端还应保证下面两个静态路径对不同账号返回相同的公开内容;若同一路径会按会话改变内容,就不满足这个设计前提。

请求本例决定理由
GET 白名单静态资源允许缓存处理内容公开且路径带版本
POST、PUT、PATCH、DELETE本监听器不拦截不复用 GET 缓存
HEAD本监听器不拦截不在本例的 GET 合约内
其他 GET 路径或查询串本监听器不拦截未获得缓存授权

把方法过滤放到 respondWith 之前

下面是 Service Worker 脚本中的 fetch 处理片段,用来替换相同职责的既有监听器。注册、安装和激活属于宿主应用已有流程,不在本例重复增加。把公共资源实际路径替换进白名单,再与服务器的发布版本保持一致。Cache API 与 Service Worker 依赖相应运行环境,通常部署在 HTTPS 安全上下文。

// 此白名单仅适用于服务器保证公开、与登录身份无关的版本化资源。
const CACHE_NAME = "reader-public-static-v3";
const PUBLIC_PATHS = new Set([
  "/public-static/reader.v3.css",
  "/public-static/reader.v3.js",
]);

function canUseCache(request) {
  if (request.method !== "GET") return false; // 先限定方法。
  const url = new URL(request.url);
  return url.origin === self.location.origin
    && PUBLIC_PATHS.has(url.pathname)
    && url.search === ""
    && !request.headers.has("Authorization")
    && !request.headers.has("Range")
    && request.cache !== "no-store"
    && request.cache !== "reload";
}

self.addEventListener("fetch", (event) => {
  const request = event.request;
  if (!canUseCache(request)) return; // 此监听器放行,不创建替代请求。
  event.respondWith(readPublicAsset(request)); // 同步登记响应承诺。
});

入口判断不需要等待异步任务,因此 respondWith 能在事件回调期间调用。return 表示这个监听器不提供响应,并不替服务器返回成功;其他 fetch 监听器仍可能处理它。项目若有多个路由,应把相同规则合并到统一入口,避免一处放行、另一处又重新缓存。

请求方法、资源白名单和缓存接口的静态依赖关系
结构说明图:GET、同源路径和附加请求条件共同限定缓存接口;连线表示条件依赖,不是抓包记录或执行流程。

保持读取和写入使用同一请求

readPublicAsset 延续同一个 Request:先从指定 Cache 读取,未命中才请求网络。它采用缓存优先,适合上述路径不变则内容不变的版本化资源。新内容应换新路径;如果希望同路径更新,应另外设计刷新策略,不能期待本例自动重新验证。

// 缓存不可用时仍允许网络读取;错误日志不打印 URL、请求头或响应内容。
async function readPublicAsset(request) {
  let cache = null;
  try {
    cache = await caches.open(CACHE_NAME);
    const hit = await cache.match(request); // 保留默认匹配选项。
    if (hit) return hit;
  } catch {
    console.warn("公共资源缓存读取失败");
  }

  // 网络失败让 Promise 拒绝,不伪装成提交成功或空白 200 响应。
  const response = await fetch(request);
  const cacheControl = response.headers.get("Cache-Control") ?? "";
  const vary = response.headers.get("Vary") ?? "";
  const hasVaryStar = vary.split(",").some((field) => field.trim() === "*");
  const mayStore = response.status === 200
    && response.type === "basic"
    && !response.redirected
    && !/\b(?:no-store|private)\b/i.test(cacheControl)
    && !hasVaryStar;

  if (cache && mayStore) {
    try {
      // 分出一份响应体供 Cache 消费,原响应仍交给浏览器。
      await cache.put(request, response.clone());
    } catch {
      console.warn("公共资源缓存写入失败");
    }
  }
  return response;
}

这个函数应与上一段放在同一个 Service Worker 脚本中。它没有主动扩大存储范围:只接受状态 200、同源 basic、未重定向的响应;分段 206、opaque 及错误响应不保存。Cache API 本身不会替应用遵循 HTTP 缓存头,本例对 no-store、private 的判断是主动增加的业务限制,不是一个完整的 HTTP 缓存实现。

Cache.put 的写入请求方法被规范限定为 GET。调用时传入原 request,保留方法、URL 和请求头,而不是只传 request.url 创建默认 GET。Response.clone 则是为了区分给页面与给缓存的响应体使用方;它不是解决请求方法问题的工具。

分开处理网络失败与存储失败

我更倾向于让存储失败降低离线能力,而不毁掉已经拿到的网络响应。因此代码在 put 失败后仍返回 response。相反,网络本身失败且缓存没有命中时,函数拒绝,浏览器得到相应失败结果;若产品需要离线提示,可以增加专门的资源或页面回退,但不能对所有业务请求返回同一份“成功”。

本例等待 put 完成后才交出网络响应,使写入处于响应承诺的生命周期内,代价是未命中路径会多等一次存储。需要缩短这段等待时,可以专门设计 waitUntil 延长后台任务生命周期;不要简单去掉 await,留下无人处理的 Promise。这里选择代码短、错误边界明确的写法,没有声称它是所有页面的最快方案。

缓存读写、响应副本与页面响应的边界
结构说明图:readPublicAsset 关联指定 Cache 与网络响应,响应副本供写入使用,原响应返回页面;存储错误与网络错误采用不同处理方式。

保留匹配参数并限定缓存生命周期

ignoreMethod 只改变查询时的方法条件,不会赋予 Cache.put 保存 POST 的能力,更不会把请求体纳入一个可靠的业务缓存键。如果给提交请求打开这个选项,相同 URL 的 GET 缓存可能被拿来代替提交响应。对于“同 URL、不同方法”的接口,我会把方法条件固定在入口,不让调用方随意传查询开关。

本例也不设置 ignoreSearch 或 ignoreVary。URL 查询参数可能代表不同内容,Vary 可能要求按请求头区分响应,忽略它们需要额外证明。这里直接拒绝任何查询串,让白名单集合的含义清楚。

缓存名使用 reader-public-static-v3,只读取这一份缓存,不跨所有缓存寻找“碰巧匹配”的响应。更新后可在应用既有激活策略中清理自己拥有的旧版本;不要删除同源下所有 Cache。Cache 条目没有因为 HTTP max-age 到点就自动过期,存储也可能被浏览器回收,因此它不能替代持久业务存储。

按调用方结果排查常见问题

为什么加了 ignoreMethod 仍然 put 失败?它是查询参数,不改变写入约束。先确认写入请求确实是 GET,再区分响应体已消费、响应类型、Vary 和存储等原因。

为什么 HEAD 没走这个缓存?本例显式只接收 GET;需要 HEAD 语义时单独设计,不能通过返回含正文的 GET 响应含糊替代。

为什么 POST 离线仍失败?方法过滤没有提供离线提交队列,也没有重放业务动作。这里的目标是公共静态资源复用,提交请求的离线处理属于另一项接口设计。

GET 白名单是否意味着一定返回缓存?不意味着。缓存未命中、被回收或存储不可用时仍需要网络。本例只限定缓存职责,不能保证永久离线可用。

技术依据:Cache 查询与写入规则见 Service Workers 规范;接口说明可查 https://developer.mozilla.org/en-US/docs/Web/API/Cache、https://developer.mozilla.org/en-US/docs/Web/API/Cache/put 与 https://developer.mozilla.org/en-US/docs/Web/API/Cache/match。代码中的公开资源白名单和失败策略是本文的应用设计选择。

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