上个星期我在生产环境里排查一个奇怪的缓存问题:接口明明加了 caches.default,数据也写进去了,但 cf-cache-status 一路飘着 MISS。最开始怀疑是 Worker 脚本部署没生效,后来把缓存键打印出来才发现,问题出在 caches.default.match(request) 这句话上——它默认拿整个请求对象当缓存键,而请求 URL 里的查询参数,尤其是 utm_source、fbclid 这类追踪参数,把同一个资源拆成了无数个不同的键。改成用 new Request() 手动构造一个规范化后的缓存键之后,命中率直接从前一天的 30% 拉到 96%。这篇教程就把这套“用 new Request() 构造 Cache Key”的做法从头到尾拆开讲,包括默认键的坑、URL 规范化流程、键控策略选型、完整接入代码,以及我踩过的几个比较隐蔽的坑。适合已经会用 Workers 的 Cache API、但命中率始终上不去的开发者参考。
1. 默认缓存键的坑:URL 里每个字符都在影响命中率
1.1 一个查询参数就把键拆得粉碎
很多人第一次用 Workers 的 Cache API,写的都是最直觉的代码:
js复制const cache = caches.default;
let response = await cache.match(request);
if (!response) {
response = await fetch(request);
await cache.put(request, response.clone());
}
这段代码看起来没问题,但放到真实流量里,命中率大概率很难看。原因在于 cache.match(request) 和 cache.put(request, response) 里的 request 对象,会被当成缓存键的一部分。也就是说,如果 URL 带有任意一个变化的查询参数,缓存就分裂了。
举个例子。用户访问的商品页是:
code复制https://shop.example.com/product?id=1001&utm_source=newsletter
另一个用户从搜索引擎点进来,URL 变成了:
code复制https://shop.example.com/product?utm_source=google&id=1001
这两个 URL 在服务端看来,商品数据完全一样,但在 Cache API 眼里是两个完全不同的键。更极端的情况是,只要 URL 里带了一个随机时间戳或者一次性 ref 参数,那么每个请求都会生成一个新键,缓存形同虚设。
这类参数最常见的就是各种渠道追踪参数:
utm_source、utm_medium、utm_campaign、utm_term、utm_contentfbclid、gclid、msclkidref、source、from、spm
它们对业务响应内容没有任何影响,只用于统计来源。但它们确实会出现在 URL 里,导致同一个页面被拆成几十个甚至上百个缓存副本。
1.2 为什么不能完全依赖 match 的 ignoreSearch 选项
很多熟悉浏览器 Cache API 的人会想到 cache.match(request, { ignoreSearch: true }),把查询参数整体忽略掉。这个思路没错,但它有两个现实问题。
第一,ignoreSearch 的语义是一刀切,不管查询参数有多少、有多重要,全部忽略。可实际业务里,有不少参数是必须参与缓存键的,比如分页参数 page、列表排序参数 sort、商品规格参数 sku。如果一股脑全忽略,就会出现“第一页的数据返回给第二页的请求”这种串数据事故。
第二,不同实现环境下对 ignoreSearch 等匹配选项的支持并不完全一致,行为也没有你想象的那么精细,它不能只排除某个指定参数,也不能对参数进行排序后再比较。也就是说,ignoreSearch 是粗粒度的“忽视”,而 new Request() 方案是细粒度的“改造”。
你应该把缓存键看作一把“钥匙”,钥匙的齿形就是我们自己定义的:该保留的查询参数保留,不该保留的剔除,参数顺序固定。这把钥匙应该由 new Request() 显式构造出来,而不是让真实请求里的 URL 替你做决定。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 用 new Request() 构造自定义缓存键:URL 规范化实操
2.1 核心思路:先把 URL 洗干净,再生成 Request
new Request() 构造函数的第一个参数既可以传一个现有 Request 对象,也可以传一个 URL 字符串。它的作用就是让我们能拿到一个“干净”的请求对象,专门用来作为缓存键。我自己常用的写法是这样的:
js复制const TRACKING_PARAMS = new Set([
'utm_source',
'utm_medium',
'utm_campaign',
'utm_term',
'utm_content',
'fbclid',
'gclid',
'ref',
'source'
]);
function buildCacheKey(requ
