本地存储方案对比
背景:为什么要关注浏览器存储
HTTP 协议是无状态的——服务器不会记住上一次请求是谁发的。为了在客户端保留用户身份、缓存数据、维持交互状态,浏览器提供了多种存储机制。面试中经常被问到的就是:cookie、localStorage、sessionStorage、IndexedDB 四者有什么区别,各自适合什么场景。
这篇文章按照"逐个拆解 → 横向对比 → 选型建议"的思路展开,覆盖面试高频追问点。
Cookie
本质与用途
Cookie 最初被设计用来解决 HTTP 无状态问题。服务器通过 Set-Cookie 响应头将数据写入浏览器,浏览器在后续同源请求中自动通过 Cookie 请求头携带这些数据。
核心特征:每次同源 HTTP 请求都会自动携带,这既是它的能力,也是它的负担。
关键属性字段
Set-Cookie: token=abc123; Domain=.example.com; Path=/; Expires=Thu, 01 Jan 2027 00:00:00 GMT; HttpOnly; Secure; SameSite=Lax
逐个拆解:
| 属性 | 作用 | 说明 |
|---|---|---|
| Domain | 指定 cookie 可被发送到哪些域 | 设置为 .example.com 则子域名也能读取 |
| Path | 限制 cookie 在哪些路径下可用 | 默认为当前路径 |
| Expires / Max-Age | 控制过期时间 | 不设置则为会话级 cookie,关闭浏览器即失效 |
| HttpOnly | 禁止 JavaScript 通过 document.cookie 读取 | 防御 XSS 窃取 cookie |
| Secure | 仅在 HTTPS 连接下发送 | 防止中间人截获明文 cookie |
| SameSite | 控制跨站请求是否携带 cookie | Strict 完全不带;Lax GET 导航会带;None 都带(需配合 Secure) |
SameSite 深入
这是面试高频追问点。Chrome 80+ 将 SameSite 默认值从 None 改为 Lax,直接影响了跨站场景:
- Strict:任何跨站请求都不带 cookie。从第三方链接跳转过来也不带,用户体验差(登录态丢失)。
- Lax:跨站的 GET 导航(链接跳转、预加载)会带,但 POST 表单、iframe、AJAX 不带。适合大多数场景。
- None:跨站也带,但必须同时设置
Secure。用于需要跨站传 cookie 的场景,如第三方登录、嵌入式 widget。
容量限制
- 单个 cookie 大小上限约 4KB
- 每个域名下 cookie 数量上限通常为 50 个(各浏览器略有差异)
- cookie 会随每次请求发送,体积过大直接影响网络性能
操作 API
// 写入
document.cookie = 'name=value; max-age=3600; path=/';
// 读取(只能拿到 key=value,拿不到属性)
console.log(document.cookie); // "name=value; other=xxx"
// 删除(设置过期时间为过去)
document.cookie = 'name=; max-age=0';
API 设计非常原始——没有直接的 get/set/delete 方法,读取时得自己解析字符串。这也是为什么现代前端存储更倾向使用 Web Storage API。
localStorage
核心特性
localStorage 提供了简单的键值对持久化存储:
- 持久化:数据不会过期,除非手动清除或用户清理浏览器数据
- 同源共享:同一源(协议 + 域名 + 端口)下的所有标签页、窗口共享同一份数据
- 容量:通常为 5MB(不同浏览器略有差异,部分移动端浏览器为 2.5MB)
- 同步 API:操作会阻塞主线程
API 使用
// 写入
localStorage.setItem('user', JSON.stringify({ name: '南祎', role: 'dev' }));
// 读取
const user = JSON.parse(localStorage.getItem('user'));
// 删除单个
localStorage.removeItem('user');
// 清空当前源下所有数据
localStorage.clear();
// 获取存储的键值对数量
console.log(localStorage.length);
注意事项
- 只能存字符串:存对象需要
JSON.stringify,取出需要JSON.parse。存储undefined会变成字符串"undefined"。 - 同步阻塞:大量数据读写可能阻塞渲染,不适合存储大数据。
- 无过期机制:需要自行实现过期逻辑(存入时带时间戳,读取时判断)。
- storage 事件:同源的其他标签页可以监听
storage事件,实现跨标签页通信。
// 标签页 A 写入
localStorage.setItem('theme', 'dark');
// 标签页 B 监听变化
window.addEventListener('storage', (event) => {
console.log(event.key); // 'theme'
console.log(event.oldValue); // null 或旧值
console.log(event.newValue); // 'dark'
});
这个特性常被用作简易的跨标签页通信方案。
sessionStorage
核心特性
sessionStorage 的 API 与 localStorage 完全一致,区别在于生命周期和作用域:
- 会话级别:关闭标签页/窗口后数据清除
- 标签页隔离:即使同源,不同标签页之间也不共享数据
- 刷新保留:页面刷新不会丢失数据(区别于内存变量)
- 容量:同样约 5MB
标签页隔离的细节
这是面试区分度的关键点:
// 标签页 A
sessionStorage.setItem('step', '2');
// 标签页 B(即使同源)
sessionStorage.getItem('step'); // null —— 读不到 A 的数据
但有一个特殊情况:通过 window.open 或 <a target="_blank"> 打开的新标签页,会复制当前标签页的 sessionStorage 作为初始值,之后两者独立互不影响。
典型使用场景
- 表单多步骤填写:防止用户刷新丢失已填内容,但关闭后不需要保留
- 单次会话的临时状态:如当前滚动位置、展开/折叠状态
- 防止同一用户在多个标签页重复提交
IndexedDB
为什么需要 IndexedDB
当存储需求超出简单键值对的范围——需要存储大量结构化数据、支持索引查询、处理二进制文件(图片、音频)——localStorage 的 5MB 限制和纯字符串存储就力不从心了。
IndexedDB 是浏览器内置的事务型数据库,可以理解为浏览器里的 NoSQL 数据库。
核心概念
- Database:一个源可以有多个数据库,通过名称区分
- Object Store:类似关系数据库的"表",存储 JavaScript 对象
- Key:每条记录的唯一标识,可以是 keyPath(对象内某个字段)或自增 key
- Index:在非主键字段上建索引,加速查询
- Transaction:所有读写操作必须在事务中进行,保证数据一致性
- Cursor:遍历大量数据的迭代器
基本操作示例
// 打开/创建数据库
const request = indexedDB.open('MyApp', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建 Object Store,指定主键
const store = db.createObjectStore('users', { keyPath: 'id' });
// 创建索引
store.createIndex('nameIndex', 'name', { unique: false });
};
request.onsuccess = (event) => {
const db = event.target.result;
// 写入数据
const transaction = db.transaction('users', 'readwrite');
const store = transaction.objectStore('users');
store.add({ id: 1, name: '南祎', age: 28 });
// 读取数据
const getRequest = store.get(1);
getRequest.onsuccess = () => {
console.log(getRequest.result); // { id: 1, name: '南祎', age: 28 }
};
// 通过索引查询
const index = store.index('nameIndex');
const indexRequest = index.get('南祎');
indexRequest.onsuccess = () => {
console.log(indexRequest.result);
};
};
关键特性
| 特性 | 说明 |
|---|---|
| 容量 | 通常为磁盘可用空间的 50%,远超其他方案 |
| 数据类型 | 支持结构化克隆算法能处理的所有类型(对象、数组、Blob、File、ArrayBuffer 等) |
| 异步 API | 不阻塞主线程(基于事件回调,也可用 Promise 封装) |
| 事务支持 | 读写操作在事务内执行,保证一致性 |
| 同源策略 | 遵循同源限制 |
| Web Worker | 可在 Worker 中访问 |
实际使用建议
原生 API 偏底层,回调嵌套多。生产中通常使用封装库:
- idb:轻量 Promise 封装,API 简洁
- Dexie.js:功能丰富,支持链式查询、版本管理
- localForage:统一的 localStorage 风格 API,底层自动选择 IndexedDB / WebSQL / localStorage
四种方案对比
| 维度 | Cookie | localStorage | sessionStorage | IndexedDB |
|---|---|---|---|---|
| 容量 | ~4KB/个 | ~5MB | ~5MB | 数百MB甚至更多 |
| 生命周期 | 可设置过期时间 | 永久(手动清除) | 标签页关闭即清除 | 永久(手动清除) |
| 作用域 | 同源 + Path + Domain 控制 | 同源所有标签页共享 | 仅当前标签页 | 同源所有标签页共享 |
| 是否随请求发送 | ✅ 自动携带 | ❌ | ❌ | ❌ |
| API 风格 | 字符串拼接,原始 | 同步,简单键值对 | 同步,简单键值对 | 异步,事务型 |
| 数据类型 | 字符串 | 字符串 | 字符串 | 结构化数据、二进制 |
| Web Worker 访问 | ❌ | ❌ | ❌ | ✅ |
| 典型用途 | 身份认证、会话管理 | 用户偏好、缓存数据 | 临时表单状态 | 离线应用、大量结构化数据 |
存储方案选型建议
选型的核心思路是根据数据特征匹配存储能力:
- 需要服务端识别身份 → Cookie(配合 HttpOnly + Secure + SameSite)
- 少量配置数据,需要跨标签页持久化 → localStorage(如主题偏好、语言设置、token 缓存)
- 单次会话临时数据,标签页之间互不干扰 → sessionStorage(如表单草稿、支付流程状态)
- 大量结构化数据或离线场景 → IndexedDB(如离线邮件、本地数据缓存、大文件暂存)
一个常见的组合方案:
认证 token → Cookie(HttpOnly,防 XSS 读取)
用户偏好设置 → localStorage(持久化,跨标签页同步)
表单填写进度 → sessionStorage(标签页关闭即弃)
离线数据/缓存 → IndexedDB(容量大,支持索引查询)
安全注意事项
XSS 防护
- Cookie 设置
HttpOnly后,JavaScript 无法读取,降低被 XSS 窃取的风险 - localStorage / sessionStorage 没有类似机制——一旦页面被 XSS 注入,存储的所有数据都可以被读取
- 敏感信息(token、密钥)不应存在 localStorage 中,优先使用 HttpOnly Cookie
CSRF 防护
- Cookie 会自动携带,天然面临 CSRF 攻击风险
SameSite=Lax/Strict是目前最有效的防御手段- localStorage 中的 token 需要手动通过 Header 发送,天然免疫 CSRF
数据加密
- 浏览器存储的数据在本地是明文的,任何能打开 DevTools 的人都能看到
- 不要在客户端存储密码、密钥等高度敏感信息
- 如果必须存储敏感数据,应该在存入前加密,读取时解密
存储配额与异常处理
// localStorage 写入可能因配额满而抛异常
try {
localStorage.setItem('key', largeData);
} catch (error) {
if (error.name === 'QuotaExceededError') {
// 清理旧数据或降级处理
console.warn('存储空间已满');
}
}
面试高频追问
Q: cookie 和 localStorage 最本质的区别是什么?
Cookie 会随 HTTP 请求自动发送到服务器,localStorage 不会。这决定了 cookie 适合身份认证场景,而 localStorage 适合纯客户端数据存储。
Q: 为什么不把 token 存在 localStorage 里?
localStorage 没有 HttpOnly 保护,XSS 攻击可以直接用 localStorage.getItem 读走 token。而 HttpOnly Cookie 即使页面被注入恶意脚本也无法被 JS 读取。但也有观点认为,如果已经被 XSS 了,攻击者可以直接以用户身份发请求,HttpOnly 也只是增加了攻击成本而非根本解决问题。
Q: sessionStorage 在新标签页打开同一个 URL 能共享数据吗?
直接在地址栏输入 URL 打开新标签页——不能共享。但通过 window.open 或带 target="_blank" 的链接打开——会复制一份当时的 sessionStorage 作为初始值,之后两者独立。
Q: 如何实现 localStorage 的过期机制?
localStorage 本身不支持过期时间,需要自行封装:
function setWithExpiry(key, value, ttlMs) {
const item = { value, expiry: Date.now() + ttlMs };
localStorage.setItem(key, JSON.stringify(item));
}
function getWithExpiry(key) {
const raw = localStorage.getItem(key);
if (!raw) return null;
const item = JSON.parse(raw);
if (Date.now() > item.expiry) {
localStorage.removeItem(key);
return null;
}
return item.value;
}
Q: IndexedDB 相比 localStorage 的核心优势在哪?
三个维度:容量(数百 MB vs 5MB)、数据类型(结构化对象 + 二进制 vs 纯字符串)、查询能力(索引 + 游标 vs 只能按 key 取值)。另外 IndexedDB 是异步的,不会阻塞主线程。
总结
- Cookie 是唯一能自动随请求发送的存储方案,适合身份认证,但容量小、API 原始,安全属性(HttpOnly/Secure/SameSite)必须配置正确
- localStorage 提供简单持久化键值存储,跨标签页共享,适合用户偏好等少量数据
- sessionStorage 与 localStorage API 一致但生命周期限于标签页,适合临时状态
- IndexedDB 是浏览器端的事务型数据库,容量大、支持结构化数据和索引,适合离线应用和大数据量场景
- 安全上,敏感 token 优先用 HttpOnly Cookie 存储,客户端存储要做好 XSS 防护和异常处理