Web 存储与 Service Worker
本节目标
- 对比 cookie / localStorage / sessionStorage / IndexedDB 四种存储
- 用一个浏览器可打开的 HTML,亲手体验四种存储的读写与生命周期
- 理解 Service Worker 作为「网络代理」能做什么(离线、缓存优先)
存储方案对比:
| 方案 | 容量 | 持久性 | 作用域 | 典型场景 |
|---|---|---|---|---|
| Cookie | ~4KB | 可设过期 | 域名 | 会话/认证(自动携带) |
| localStorage | ~5MB | 永久(手动清) | 源 | 偏好设置、缓存数据 |
| sessionStorage | ~5MB | 关标签页即清 | 标签页 | 临时表单草稿 |
| IndexedDB | 大(GB 级) | 永久 | 源 | 离线数据、大对象 |
Cookie 要点:HttpOnly(防 XSS 读)、Secure(仅 HTTPS 携带)、SameSite(防 CSRF:Lax/Strict/None)。每次请求自动携带 → 别往里塞大数据。
下面这个 HTML,在浏览器打开就能亲手读写四种存储,直观感受「谁关页面还在、谁刷新还在」:
<!-- 运行环境:任意现代浏览器 -->
<!-- 步骤:存为 brw-l13.html,用浏览器打开,按 F12 看 Console;再试「刷新」和「关掉重开标签页」对比差异 -->
<!-- ⚠️ 建议用本地服务打开(如 npx serve 或 VS Code Live Server)。直接用 file:// 打开时,
cookie 可能被浏览器忽略、IndexedDB 在部分浏览器下也会受限——它们都依赖「源(origin)」 -->
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><title>四种存储演示</title></head>
<body>
<h3>打开控制台看输出;刷新页面 / 关掉重开对比谁还在</h3>
<script>
// 1) localStorage:关页面、刷新都在(同源永久)
localStorage.setItem('ls', '我关掉浏览器还在')
console.log('localStorage:', localStorage.getItem('ls'))
// 2) sessionStorage:只在这个标签页会话内,关标签页就没
sessionStorage.setItem('ss', '我关标签页就消失')
console.log('sessionStorage:', sessionStorage.getItem('ss'))
// 3) cookie:随请求自动带,可设过期;这里用 document.cookie 写(非 HttpOnly)
document.cookie = 'ck=hello; max-age=60' // 60 秒后过期
console.log('cookie:', document.cookie)
// 4) IndexedDB:异步、容量大,适合存结构化/大数据
const req = indexedDB.open('demoDB', 1)
req.onupgradeneeded = (e) => {
const db = e.target.result
if (!db.objectStoreNames.contains('kv')) db.createObjectStore('kv')
}
req.onsuccess = (e) => {
const db = e.target.result
const tx = db.transaction('kv', 'readwrite')
tx.objectStore('kv').put('我存在IndexedDB', 'key1')
tx.oncomplete = () => {
db.transaction('kv').objectStore('kv').get('key1').onsuccess = (ev) =>
console.log('IndexedDB:', ev.target.result)
}
}
</script>
</body>
</html>Service Worker(SW):运行在独立线程的网络代理,能拦截 fetch 请求并决定「走缓存 / 走网络 / 混合」。
// 注册 + 安装缓存 + 拦截 fetch(浏览器环境,放 sw.js + 主线程注册)
// 主线程:
navigator.serviceWorker.register('/sw.js')
// sw.js:
self.addEventListener('install', (e) => {
e.waitUntil(caches.open('v1').then((c) => c.addAll(['/', '/app.js'])))
})
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then((hit) => hit || fetch(e.request)) // 缓存优先
)
})SW 关键点:必须 HTTPS(或 localhost);首次访问注册后刷新一次才接管;更新要 skipWaiting + clients.claim 并清理旧缓存。PWA = SW(离线/缓存优先)+ Manifest(可安装/图标)+ 推送,核心价值是离线可用、秒开、类原生体验。
名词解释
localStorage:同源下的「永久键值仓库」,容量约 5MB,关浏览器数据仍在,除非代码或用户手动清。方法:setItem(k,v)、getItem(k)、removeItem(k)、clear()。适合存「不敏感的用户偏好」(如主题、语言)。注意:它是同步 API,存太大或频繁读写会阻塞主线程。
sessionStorage:和 localStorage 几乎一样,但生命周期绑定于「标签页会话」——关掉这个标签页数据就没了(刷新不丢,新开标签不算同一会话)。方法同 localStorage。适合存「本次访问的临时草稿」。
IndexedDB:浏览器内的「嵌入式数据库」,异步、容量大(GB 级)、支持事务和索引,适合存结构化数据和离线内容。方法:open(name, ver) 打开、objectStore 操作表、transaction 起事务、put/get 读写。它是 PWA 离线能力的存储底座。
Service Worker(SW):浏览器里的「网络代理线程」,能拦截页面所有 fetch。方法:register() 注册、self.addEventListener('fetch', ...) 拦截、caches.open/addAll/match 管理缓存、skipWaiting()+clients.claim() 控制更新时机。它让「离线可用」「缓存优先」成为可能。
课后练习
练习 1:用户登录态 token 应该存 cookie、localStorage 还是 sessionStorage?各有什么权衡?
答案:若需要「每次请求自动带凭据给后端」且防 XSS 偷读,用 HttpOnly + Secure + SameSite 的 cookie 最稳(JS 读不到,天然抗 XSS)。若图方便放
localStorage,要自己手动塞进请求头,且一旦页面有 XSS 就会被偷走 token。sessionStorage 关标签页就清,不适合「记住登录」。结论:敏感凭证优先 HttpOnly cookie;纯前端态(如 UI 主题)才放 localStorage。
练习 2:Service Worker 为什么说「首次访问后需要刷新一次才生效」?
答案:SW 的注册和激活是异步的——首次访问时 SW 还在
install/activate,此时页面已经被旧的(或没有)SW 控制着;等 SW 激活后,必须等「下一次导航/刷新」浏览器才会让新 SW 接管当前页面。这就是为什么 SW 改动后常看到「刷新后生效」,或用clients.claim()主动让新 SW 立即控制已有页面。
总结
存储这一节,我愿你建立的认知是:没有「最好的存储」,只有「最合场景的存储」。Cookie 为「认证自动携带」而生但容量小;localStorage 简单永久却同步会卡;sessionStorage 临时但一关就没;IndexedDB 强大异步但 API 繁琐。选错存储,要么撑不下数据,要么悄悄拖慢主线程,要么把敏感 token 暴露在 XSS 面前。而 Service Worker 则是另一个维度的能力跃迁——它让网页从「必须联网才能用」变成「能离线、能缓存优先」。但 SW 也有代价:HTTPS 强制、更新心智负担、调试坑多。我的建议是:日常状态用对存储,离线/秒开需求才上 SW+PWA,别为了「听起来高级」而滥用。