浏览器缓存
一、核心概念:三层缓存体系
-
浏览器缓存通常分为三层,你需要清楚它们的分工: 缓存类型 | 控制方 | 特点 | 缺点 Memory Cache | 浏览器 | 内存缓存,读取最快,关闭 Tab 即失 | 容量极小,不可控 Disk Cache | HTTP Header | 强缓存/协商缓存,存硬盘 | 不可编程,只能靠 HTTP 头 Service Worker Cache | 开发者 | 可编程缓存,完全控制请求/响应 | 需 HTTPS,代码复杂
-
Cache API 就是第三层,它允许你像操作数据库一样操作缓存。
二、Service Worker:背后的“幕后黑手”
- 什么是 Service Worker
- 它是一个独立于网页主线程的 JavaScript 脚本。
- 角色:充当 Web 应用与网络之间的代理服务器。
- 能力:拦截所有 fetch请求,决定是从缓存读、网络读,还是自己造一个响应。
- 限制:必须在 HTTPS 或 localhost 下运行。
- Service Worker 的生命周期 (核心考点)
- 这是面试必问的,理解它才能处理好缓存更新。
- Registering (注册):主线程注册 SW。
- Installing (安装):SW 首次被下载并执行。通常在这里预缓存(Cache Static Assets)。
- Waiting (等待):等待旧版本的 SW 关闭。
- Activating (激活):新 SW 接管页面。通常在这里清理旧缓存。
- Activated (激活后):开始拦截 fetch事件。
- 这是面试必问的,理解它才能处理好缓存更新。
- 它是一个独立于网页主线程的 JavaScript 脚本。
三、Cache API 核心操作
- Cache API 是一个 Key-Value 存储,Key 是 Request,Value 是 Response。
- 打开缓存
// 打开一个名为 'v1' 的缓存空间 const cache = await caches.open('v1'); - 添加资源 (Add / AddAll)
// 缓存单个文件 await cache.add('/styles.css'); // 批量缓存(常用于安装阶段) await cache.addAll([ '/', '/index.html', '/app.js', '/logo.png' ]); - 匹配与读取
// 查找缓存中是否有匹配的请求 const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; // 返回缓存 } - 删除
// 删除整个缓存空间 await caches.delete('old-v1');
- 打开缓存
四、实战:Fetch 配合 Service Worker 实现离线缓存
- 最经典的 App Shell 架构
- 步骤 1:主线程注册 Service Worker
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered: ', registration);
});
});
}
- 步骤 2:编写 Service Worker 脚本 (sw.js)
// sw.js
const CACHE_NAME = 'app-shell-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.svg'
];
// 1. 安装阶段:预缓存 App Shell
self.addEventListener('install', event => {
console.log('Installing Service Worker...');
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Caching app shell');
return cache.addAll(urlsToCache);
})
// 强制跳过等待,直接进入 activate
.then(() => self.skipWaiting())
);
});
// 2. 激活阶段:清理旧缓存
self.addEventListener('activate', event => {
console.log('Activating Service Worker...');
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cache => {
if (cache !== CACHE_NAME) {
console.log('Deleting old cache:', cache);
return caches.delete(cache);
}
})
);
}).then(() => self.clients.claim()) // 立即接管所有页面
);
});
// 3. 拦截 Fetch 请求 (最核心)
self.addEventListener('fetch', event => {
// 如果是导航请求(HTML页面),采用 Network First 策略
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => caches.match('/offline.html'))
);
return;
}
// 其他资源(CSS/JS/图片)采用 Cache First 策略
event.respondWith(
caches.match(event.request)
.then(response => {
// 如果缓存中有,直接返回
if (response) {
return response;
}
// 否则去网络请求,并缓存结果
return fetch(event.request).then(networkResponse => {
// 克隆响应(因为响应流只能读一次)
const responseToCache = networkResponse.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return networkResponse;
});
})
);
});
五、缓存策略详解
- 不同的资源需要不同的策略,这是架构能力的体现。 策略名称 逻辑 适用场景 Cache First 先查缓存,无则网络。 静态资源 (图片、CSS、JS)。最快,离线可用。 Network First 先请求网络,失败则缓存。 HTML 页面、API 数据。保证最新,网络不行才兜底。 Stale-While-Revalidate 先返回缓存,后台悄悄更新缓存。 新闻列表、用户头像。速度快,数据略旧但很快更新。 Network Only 只用网络。 支付接口、实时数据(股票)。 Cache Only 只用缓存。 很少用,除非是完全离线的 App。
六、Fetch 与 Cache API 的配合细节
- 为什么需要 clone()?
- 在 Service Worker 中,Response 是一个 Readable Stream。
- 规则:流只能被消费一次。
- 如果你在 fetch中读了一次流,又想把它放进缓存,就会报错。
- 解决:event.respondWith(fetch(req).then(res => res.clone()))。
- 在 Service Worker 中,Response 是一个 Readable Stream。
- 如何更新缓存?
- 版本号法:修改 CACHE_NAME = 'v2'。
- 在 activate事件中删除所有不等于 v2的旧缓存。
- 刷新页面后,新的 SW 会接管并缓存新资源。
七、调试技巧
- 打开 Chrome DevTools -> Application -> Service Workers。
- 查看 Cache Storage,可以看到具体缓存了哪些文件。
- 勾选 Update on reload,方便开发时每次刷新都更新 SW。
- 使用 Offline 开关模拟断网环境,测试离线缓存是否生效。
八、总结
- Fetch + Service Worker + Cache API 构成了现代 Web 的“超级武器”:
- Fetch 负责发起请求。
- Service Worker 作为中间人拦截请求。
- Cache API 负责存储响应。
- 这套机制让你的网站不仅能秒开(Cache First),还能在地铁里、电梯里(离线)继续运行。