跳转到内容

存储 · API

CacheStorage 与 caches 全局对象

发布于

CacheStorage 以 caches 全局对象的形式暴露在窗口、worker 和 Service Worker 中,是该源下具名 Cache 对象的注册表;每个 Cache 是一组 Request/Response 对,Service Worker 可以在离线时用它们应答 fetch 事件。它与 HTTP 缓存相互独立:数据只能通过脚本进出,HTTP 新鲜度头部一概不生效。

caches.open(cacheName)
caches.has(cacheName)
caches.delete(cacheName)
caches.keys()
caches.match(request)
caches.match(request, options)

每个方法都返回 Promise。open() 在具名缓存不存在时创建它,所以在名称里升级版本号(shell-v4)就足以开启一个全新的存储。CacheStorage 上的 match() 按创建顺序搜索全部缓存,返回第一个命中或 undefined。主线程自 Chrome 43、Firefox 41、Safari 11.1 起可用(BCD api.CacheStorage),Service Worker 内自 Chrome 40 起可用。

方法 参数 类型 含义
open、has、delete cacheName string 缓存名称。区分大小写,作用域为源;同一源上的两个 Service Worker 共享同一命名空间。
match request Request 或 string 要查找的请求。字符串会按当前基准 URL 转成 Request。
match options.ignoreSearch boolean 比较 URL 时忽略查询串。默认 false。
match options.ignoreMethod boolean 允许匹配非 GET 请求。默认 false,因此 POST 不会匹配。
match options.ignoreVary boolean 忽略已存响应的 Vary 头。默认 false。
match options.cacheName string 只搜索指定缓存。Chromium 的 CacheStorage.match() 只支持 ignoreSearch 和 cacheName(BCD api.CacheStorage.match)。

写入方法在 Cache 上而不在 CacheStorage 上:cache.put(request, response) 存入你已经持有的一对对象,cache.add(request) 与 cache.addAll(requests) 先抓取再存入,cache.delete(request) 删除一条。

异常 位置 触发条件
TypeError cache.put() 请求 URL 的 scheme 不是 http: 或 https:,响应状态为 206 Partial Content,或响应带 Vary: *。Promise 被拒绝。
TypeError cache.add()、cache.addAll() 抓取到的响应不是 ok(状态码不在 200 到 299 之间)。不透明响应状态为 0,因此一律落入此列;要有意存入不透明响应,改用 fetch() 加 put()。
QuotaExceededError DOMException 任何写入 该源配额已用尽。Promise 被拒绝,已存条目不受影响。
SecurityError DOMException 任何 caches 方法 源是不透明源(没有 allow-same-origin 的沙箱 <iframe>),或该站点的存储被禁用。

Cache Storage 不会让条目过期。以 Cache-Control: max-age=60 存入的响应一年后原样返回;新鲜度是 Service Worker 的职责。

CacheStorage 与 Cache 自 Chrome 43、Firefox 41、Safari 11.1 起在所有引擎中可用(BCD api.CacheStorage、api.Cache),窗口、专用 worker 和 Service Worker 一视同仁,且仅限安全源。cache.add() 自 Chrome 46 起要求 HTTPS。唯一缺失该 API 的场景是禁用了站点存储的 WebView 或浏览器,此时每个方法都以 SecurityError 拒绝。

Cache Storage 与 IndexedDB、OPFS 共用该源的单一配额,数值由 navigator.storage.estimate() 报告。不透明响应按填充后的体积计费,避免页面借配额 API 测出跨源资源大小:在 Chromium 中每条不透明响应无论实际多大都计入约 7 MB 用量(Understanding storage quota,developer.chrome.com)。跨源资源用 cors 模式请求,或在加载它的标签上加 crossorigin,响应就不再是不透明的,按真实大小计费。

三个示例分别覆盖安装期预缓存、由 fetch 填充的运行时缓存,以及页面接触 caches 之前需要的特性检测。

带版本的预缓存与 activate 时清理

Section titled “带版本的预缓存与 activate 时清理”

缓存名携带版本号。install 填充新缓存;activate 删除所有名称不同的缓存,于是上一次部署的文件被回收而不是堆积。

const CACHE = 'shell-v4';
const ASSETS = ['/', '/app.css', '/app.js', '/offline.html'];
self.addEventListener('install', (event) => {
event.waitUntil(caches.open(CACHE).then((cache) => cache.addAll(ASSETS)));
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(names.filter((name) => name !== CACHE).map((name) => caches.delete(name)))
)
);
});

addAll() 是原子的:四个请求里有一个返回 404,整个调用就以 TypeError 拒绝,install 失败。对应用壳而言这正是你想要的行为。调用 caches.match(event.request) 的 fetch 处理函数会继续从旧缓存应答,直到新 worker 激活。

在一个处理函数里存入并返回响应

Section titled “在一个处理函数里存入并返回响应”

Response 的 body 只能读一次。同一响应还要交给页面时,先克隆再 put();不要在 respondWith() 里等待 put() 完成,那会让页面为一次磁盘写入而等待。

self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') return;
event.respondWith(
caches.match(event.request).then((hit) => {
if (hit) return hit;
return fetch(event.request).then((response) => {
if (response.ok) {
const copy = response.clone();
event.waitUntil(caches.open('runtime-v1').then((cache) => cache.put(event.request, copy)));
}
return response;
});
})
);
});

response.ok 守卫把 404 和不透明响应挡在缓存之外;没有它,离线用户会把昨天的错误页当作命中拿到。

不安全源上没有 caches,早于「语法」一节所列版本的浏览器也没有。运行在页面里(Service Worker 之外)的代码应先检测再使用。

async function cachedOrNetwork(url) {
if (!('caches' in self)) {
return fetch(url); // 没有 Cache Storage:纯网络,没有离线副本
}
const hit = await caches.match(url);
return hit ?? fetch(url);
}

同一个函数在 worker 和窗口里都能用,因为两者都暴露 self。

规范

规范状态
无。