cookie、localStorage、sessionStorage、IndexedDB——四种浏览器端的存储方案。选哪个,取决于数据量大小、生命周期跨度和是否需要发给服务端。
cookie
cookie 最初是为「浏览器和服务器之间维持会话」而设计的,后来被借用做本地存储,但设计包袱一直没有卸掉。
// cookie 是追加式操作,不是 setter
document.cookie = 'a=100;'; // 写入 a=100
document.cookie = 'b=200;'; // 追加 b=200,不会覆盖 a
// 结果:'a=100; b=200'
// 同名 key 覆盖
document.cookie = 'a=999;'; // 覆盖 a=999
// 结果:'a=999; b=200'
// 设置过期时间(不设置则会话结束即过期)
document.cookie = 'token=abc; max-age=3600; path=/; Secure; SameSite=Strict';| 特性 | 说明 |
|---|---|
| 容量 | 约 4KB(不同浏览器略有差异) |
| 发送 | 每次 HTTP 请求自动携带(同域),增加请求体积 |
| 作用域 | 可设 path 和 domain 控制范围,子域名可共享 |
| 安全属性 | HttpOnly(禁止 JS 读取)、Secure(仅 HTTPS)、SameSite(防 XSRF) |
| API | document.cookie——极其简陋,没有 get/set/remove 方法 |
cookie 在今天是身份认证令牌的最佳载体——配合 HttpOnly + Secure + SameSite=Strict,而不是数据存储工具。
SameSite的三个值:Strict(完全禁止跨站,最安全)、Lax(默认值,允许顶级导航如点击链接时跨站携带)、None(允许跨站,但必须同时设置Secure)。
localStorage 与 sessionStorage
HTML5 专门为本地存储设计了这两个 API,不随 HTTP 请求发送,接口简单:
// 统一的 API
localStorage.setItem('theme', 'dark');
localStorage.getItem('theme'); // 'dark'
localStorage.removeItem('theme');
localStorage.clear(); // 清空当前域名的所有数据
// 也可以用对象属性语法(但不推荐,因为可能与内置属性冲突)
localStorage.theme = 'dark';
// sessionStorage 用法完全一样
sessionStorage.setItem('draft', 'hello');| 维度 | localStorage | sessionStorage |
|---|---|---|
| 容量 | 约 5MB(每个域名) | 约 5MB(每个域名) |
| 生命周期 | 永久存储,除非手动删除或代码清除 | 标签页关闭即清除 |
| 作用域 | 同源下所有标签页共享 | 仅当前标签页(同源不同标签页互不共享) |
| 页面刷新 | 保留 | 保留 |
| 新标签页 | 可访问 | 不可访问 |
| 跨域 iframe | 遵循同源规则 | 遵循同源规则 |
容易混淆的点:用 target="_blank" 或 window.open 打开的新标签页,某些浏览器会复制当前页的 sessionStorage 到新页面,但这是实现细节,不要依赖。规范上 sessionStorage 是标签页级别的。
存储空间溢出
空间用尽时 setItem 抛 QuotaExceededError:
try {
localStorage.setItem('key', value);
} catch (e) {
if (e.name === 'QuotaExceededError') {
console.warn('localStorage 已满');
// 处理策略:LRU 淘汰、提示用户清理、切换 IndexedDB
}
}只能存字符串
localStorage 和 sessionStorage 的键和值都只能是字符串:
// 存对象需要序列化
localStorage.setItem('user', JSON.stringify({ name: 'zhangsan' }));
const user = JSON.parse(localStorage.getItem('user'));
// 注意 JSON.stringify 的陷阱
JSON.stringify(undefined); // undefined —— 存入后取出来是字符串 'undefined'
JSON.stringify(NaN); // 'null' —— NaN 变 null
JSON.stringify(Infinity); // 'null' —— Infinity 变 null三者对比
| 维度 | cookie | localStorage | sessionStorage |
|---|---|---|---|
| 容量 | ~4KB | ~5MB | ~5MB |
| 发送服务端 | ✅ 每次请求 | ❌ | ❌ |
| 生命周期 | 可设过期 | 永久 | 标签页关闭 |
| 作用域 | path+domain | 同源共享 | 单标签页 |
| API | 差 | 简洁 | 简洁 |
| 跨标签页 | 是 | 是 | 否 |
选型思路
是否需要自动发送到服务端?
├── 是 → cookie(身份 token,配合 HttpOnly+Secure+SameSite)
└── 否:
├── 数据超过 5MB?→ IndexedDB
├── 需要结构化查询?→ IndexedDB
└── 否:
├── 仅当前标签页有效?→ sessionStorage(表单草稿、临时状态)
└── 否 → localStorage(主题、偏好、缓存数据)
IndexedDB
对于超过 5MB 的结构化数据、离线应用缓存、复杂查询需求,IndexedDB 是唯一选项。它是一个浏览器内建的非关系型数据库,支持:
- 事务(transaction)
- 索引(index)
- 异步查询(不会阻塞主线程)
- 存储 JavaScript 对象(包括 File、Blob)
// 打开数据库
const request = indexedDB.open('MyDB', 2);
request.onupgradeneeded = (e) => {
const db = e.target.result;
// 创建对象存储和索引
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('name', 'name', { unique: false });
};
request.onsuccess = (e) => {
const db = e.target.result;
// 读写操作...
const tx = db.transaction('users', 'readwrite');
const store = tx.objectStore('users');
store.add({ id: 1, name: 'zhangsan' });
};原生 IndexedDB API 基于事件回调,比较繁琐。实际开发常用封装库:
idb(轻量 Promise 封装)、Dexie.js(功能全面)、localForage(自动降级到 localStorage,适合简单 KV 场景)。