一、为什么需要 JS 客户端存储?
HTTP 协议的无状态特性导致浏览器与服务器每次通信都无法保留上下文,而客户端存储正是解决这一痛点的关键:
- 状态保持:记住用户登录状态、购物车数据、表单填写进度
- 性能优化:缓存接口数据、静态资源,减少网络请求(尤其弱网场景)
- 离线能力:支持 PWA 离线访问、本地文档编辑等场景
- 个性化体验:存储用户偏好(主题、字体大小)、浏览历史
核心需求本质是:在浏览器端安全、高效地存储不同生命周期、不同容量级别的数据。
二、JS 客户端存储全景图
JS 客户端存储主要分为 5 大类,覆盖从 1KB 到 GB 级的存储需求,适用场景各有侧重:
存储方案 | 容量限制 | 生命周期 | 同源策略 | 访问权限 | 核心特性 |
Cookie | 4KB | 可配置(Expires/Max-Age) | 是 | 前后端均可访问 | 随 HTTP 请求自动携带,支持跨域配置 |
localStorage | 5-10MB | 永久(手动清除) | 是 | 仅前端访问 | 同步操作,字符串键值对 |
sessionStorage | 5-10MB | 会话级(标签页关闭失效) | 是 | 仅前端访问 | 同标签页共享,刷新不丢失 |
IndexedDB | 无明确限制(取决于磁盘) | 永久 | 是 | 仅前端访问 | 异步操作,支持结构化数据、事务 |
Cache API | 无明确限制 | 永久(手动清除) | 是 | 仅前端访问 | 专门缓存 HTTP 请求 / 响应 |
三、逐个击破:每种存储方案的实战指南
1. Cookie:前后端共享的 “轻量存储”
核心原理
Cookie 是服务器发送给浏览器的小型文本数据,浏览器会在后续请求中自动携带(通过Cookie请求头),常用于身份验证、会话跟踪。
关键 API 与配置
// 设置Cookie(核心参数:键、值、过期时间、域名、路径、安全属性)
function setCookie(key, value, options = {}) {
let cookieStr = `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
// 过期时间:Expires(日期字符串)或 Max-Age(秒数)
if (options.expireDays) {
const date = new Date();
date.setTime(date.getTime() + options.expireDays * 24 * 60 * 60 * 1000);
cookieStr += `; Expires=${date.toUTCString()}`;
}
// 核心安全属性(必设!)
cookieStr += `; Path=${options.path || '/'}`; // 生效路径
cookieStr += options.domain ? `; Domain=${options.domain}` : '';
cookieStr += options.secure ? '; Secure' : ''; // 仅HTTPS传输
cookieStr += '; SameSite=Lax'; // 防御CSRF攻击
cookieStr += options.httpOnly ? '; HttpOnly' : ''; // 禁止JS访问(防XSS)
document.cookie = cookieStr;
}
// 读取Cookie
function getCookie(key) {
const cookies = document.cookie.split('; ');
for (const cookie of cookies) {
const [k, v] = cookie.split('=');
if (decodeURIComponent(k) === key) {
return decodeURIComponent(v);
}
}
return null;
}
// 删除Cookie(设置过期时间为过去)
function removeCookie(key) {
setCookie(key, '', { expireDays: -1 });
}
适用场景与坑点
✅ 适用:身份令牌(JWT)、会话 ID、记住登录状态(7 天免登录)
❌ 不适用:大量数据(4KB 限制)、敏感数据(即使有 HttpOnly,仍可能被 CSRF 利用)
⚠️ 坑点:
- 每次 HTTP 请求都会携带所有 Cookie,增加带宽消耗(建议核心 Cookie 仅保留 1-2 个)
- 跨域请求默认不携带 Cookie,需配置withCredentials: true(前端)+ Access-Control-Allow-Credentials: true(后端)
2. Web Storage:前端专属的 “键值对存储”
localStorage和sessionStorage统称 Web Storage,API 完全一致,核心差异在生命周期和共享范围。
核心 API(通用)
// 存储数据(自动转为字符串,复杂数据需序列化)
localStorage.setItem('username', 'zhangsan');
localStorage.setItem('userInfo', JSON.stringify({ id: 1, age: 20 }));
// 读取数据
const username = localStorage.getItem('username');
const userInfo = JSON.parse(localStorage.getItem('userInfo'));
// 删除数据
localStorage.removeItem('username');
localStorage.clear(); // 清空所有
// 遍历数据
for (let i = 0; i ++) {
const key = localStorage.key(i);
console.log(key, localStorage.getItem(key));
}
两者核心差异
特性 | localStorage | sessionStorage |
生命周期 | 永久(除非手动清除) | 标签页关闭后失效 |
共享范围 | 同源所有标签页 | 仅当前标签页 |
跨窗口通信 | 支持(storage 事件) | 不支持 |
进阶封装:带过期时间的 Web Storage
原生 Web Storage 不支持过期时间,封装工具类解决这一痛点:
class StorageUtil {
constructor(storage = localStorage) {
this.storage = storage;
}
// 存储数据(expire:过期时间,单位秒,0表示永久)
set(key, value, expire = 0) {
const data = {
value,
expire: expire ? Date.now() + expire * 1000 : 0,
timestamp: Date.now()
};
this.storage.setItem(key, JSON.stringify(data));
}
// 读取数据(自动清理过期数据)
get(key) {
const json = this.storage.getItem(key);
if (!json) return null;
try {
const data = JSON.parse(json);
// 检查过期
if (data.expire && data.expire {
this.remove(key);
return null;
}
return data.value;
} catch (e) {
this.remove(key); // 数据格式错误时清理
return null;
}
}
remove(key) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
// 监听storage变化(仅localStorage有效)
onStorageChange(callback) {
window.addEventListener('storage', (e) => {
callback({
key: e.key,
oldValue: e.oldValue ? JSON.parse(e.oldValue)?.value : null,
newValue: e.newValue ? JSON.parse(e.newValue)?.value : null,
storageArea: e.storageArea
});
});
}
}
// 实例化
const localStore = new StorageUtil(localStorage);
const sessionStore = new StorageUtil(sessionStorage);
// 使用示例
localStore.set('token', 'xxx', 86400); // 24小时过期
console.log(localStore.get('token'));
适用场景与坑点
✅ 适用:
- localStorage:用户偏好设置、表单缓存、非敏感接口数据缓存
- sessionStorage:临时表单数据、页面间临时传值(同标签页)
❌ 不适用:
- 大量数据(5-10MB 限制)
- 敏感数据(易被 XSS 攻击窃取)
- 跨标签页实时通信(sessionStorage 不支持,localStorage 需监听 storage 事件)
⚠️ 坑点:
- 同步操作:批量读写会阻塞主线程(建议单次存储不超过 100KB)
- 字符串序列化:存储undefined会转为"undefined",存储循环引用对象会报错
- 同源限制