浏览器存储方案
cookie、localStorage、sessionStorage、IndexedDB 是浏览器提供的四类持久化方案,各有各的容量、作用域和生命周期。本文逐一拆解它们的特性、API 和适用场景,最后给出选型建议与安全提醒。
一、cookie:服务端留下的通行证
cookie 最初用于在客户端记录会话状态,随每个 HTTP 请求自动发送到服务器。
1.1 核心特性
| 特性 | 说明 |
|---|---|
| 单条大小 | 约 4KB(单域名总和也受限,通常约 4KB × 数量上限) |
| 数量上限 | 每域名几十条(各浏览器约 50-180 条) |
| 有效期 | 由 expires / max-age 控制,可长期存活 |
| 作用域 | 按 path 和 domain 限制 |
| 传输 | 每次请求自动带上,是流量开销 |
| 读写 | 服务端可写,JS 通过 document.cookie 读写 |
1.2 读写方式
// 写入(追加式,一次一条)
document.cookie = "username=zhangsan; path=/; max-age=86400";
document.cookie = "theme=dark; path=/; expires=Fri, 31 Dec 2027 23:59:59 GMT";
// 读取:返回所有 cookie 拼接的字符串
console.log(document.cookie); // "username=zhangsan; theme=dark"
// 解析某个字段
function getCookie(name) {
const match = document.cookie.match(new RegExp("(^|;\\s*)" + name + "=([^;]*)"));
return match ? decodeURIComponent(match[2]) : null;
}
console.log(getCookie("theme")); // "dark"
// 删除:把 max-age 设为 0 或过期时间
document.cookie = "theme=; path=/; max-age=0";1.3 关键属性
| 属性 | 作用 | 注意 |
|---|---|---|
path=/ | 限定 URL 路径生效范围 | 不设置默认当前路径 |
domain=example.com | 限定域名(含子域名) | 不能设为不相关的域名 |
expires / max-age | 有效期 | 会话 cookie 不设置则关浏览器即失效 |
HttpOnly | 禁止 JS 读取 | 只能服务端通过 Set-Cookie 设置,防 XSS 窃取 |
Secure | 仅 HTTPS 传输 | 需服务端设置 |
SameSite | 跨站请求策略 | Lax(默认)/ Strict / None |
cookie 的 JS 读写能力较弱(没有 get/set/remove 方法,只能操作整串),且会随请求传输,不适合存大量业务数据。
二、localStorage 与 sessionStorage
两者都属于 Web Storage,API 完全一致,区别只在生命周期与作用域:
// 写
localStorage.setItem("key", "value");
// 读
localStorage.getItem("key"); // "value" 或 null
// 删
localStorage.removeItem("key");
// 清空
localStorage.clear();
// 遍历
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
console.log(key, localStorage.getItem(key));
}| 对比项 | localStorage | sessionStorage |
|---|---|---|
| 生命周期 | 持久化,手动删除才消失 | 关闭标签页/浏览器即清除 |
| 作用域 | 同源(协议+域名+端口)共享 | 同源且同一标签页 |
| 数据隔离 | 所有标签页共享 | 各标签页独立 |
| 容量 | 约 5-10MB | 约 5MB |
| 存储类型 | 只能存字符串 | 只能存字符串 |
对象需序列化后存储:
const user = { name: "张三", age: 18 };
localStorage.setItem("user", JSON.stringify(user));
const raw = localStorage.getItem("user");
const parsed = raw ? JSON.parse(raw) : null;
console.log(parsed.name); // "张三"storage 事件:跨标签页同步
同一源下其他标签页修改 localStorage 时,会触发 storage 事件(当前页面自身不触发):
window.addEventListener("storage", (event) => {
console.log("改变的键:", event.key);
console.log("旧值:", event.oldValue);
console.log("新值:", event.newValue);
console.log("来源 URL:", event.url);
});| event 成员 | 含义 |
|---|---|
key | 被修改的键(clear() 时为 null) |
oldValue / newValue | 修改前后的值 |
url | 触发修改的页面地址 |
典型应用:多标签页"登录态同步""购物车同步",一个标签页登出,其他标签页收到事件立即刷新 UI。
三、IndexedDB:浏览器里的数据库
IndexedDB 是浏览器内置的事务型 NoSQL 数据库,适合存储结构化数据和大量数据(视频、图片、离线数据包)。
3.1 核心特点
| 特点 | 说明 |
|---|---|
| 容量 | 大(数百 MB 到数 GB,按磁盘剩余空间) |
| 数据模型 | 对象仓库(object store)+ 索引(index) |
| 读写方式 | 全部异步,基于事件/回调(或 Promise 封装) |
| 事务 | 支持事务,出错可回滚 |
| 索引 | 支持对任意字段建索引做快速查询 |
| 同源限制 | 严格按同源隔离 |
3.2 简单使用流程
// 1. 打开数据库
const request = indexedDB.open("my-db", 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 2. 建对象仓库 + 索引(只能在版本升级回调里建)
const store = db.createObjectStore("users", { keyPath: "id" });
store.createIndex("byName", "name", { unique: false });
};
request.onsuccess = (event) => {
const db = event.target.result;
// 3. 在事务中写入
const tx = db.transaction("users", "readwrite");
tx.objectStore("users").add({ id: 1, name: "张三", age: 18 });
tx.oncomplete = () => console.log("写入完成");
};
request.onerror = (event) => {
console.error("打开数据库失败", event.target.error);
};读取与查询:
function getById(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction("users", "readonly");
const request = tx.objectStore("users").get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 按索引查询
function findByName(db, name) {
return new Promise((resolve, reject) => {
const tx = db.transaction("users", "readonly");
const index = tx.objectStore("users").index("byName");
const request = index.getAll(name);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}| 流程步骤 | API |
|---|---|
| 打开/升级数据库 | indexedDB.open(name, version) |
| 建表/建索引 | createObjectStore、createIndex(upgradeneeded 中) |
| 读写 | transaction + objectStore + add/get/put/delete |
| 查询 | 主键 get,索引 index(...).getAll(),游标 openCursor() |
IndexedDB 原生 API 偏底层(回调+事件),工程上常用 idb、Dexie.js 等 Promise 封装库简化写法。
四、四大存储方案对比
| 对比项 | cookie | localStorage | sessionStorage | IndexedDB |
|---|---|---|---|---|
| 容量 | 约 4KB/条 | 约 5-10MB | 约 5MB | 数百 MB 起 |
| 持久性 | 按 expires 设置 | 永久 | 关标签页消失 | 永久 |
| 作用域 | 域名 + path | 同源 | 同源 + 同标签页 | 同源 |
| 同步/异步 | 同步 | 同步 | 同步 | 异步 |
| 随请求发送 | 是 | 否 | 否 | 否 |
| 数据类型 | 字符串 | 字符串 | 字符串 | 结构化对象 |
| API 复杂度 | 简单(整串操作) | 简单 | 简单 | 复杂 |
| 适用场景 | 会话标识、登录态 | 偏好设置、草稿 | 表单临时数据 | 离线数据、大文件 |
五、存储选型建议
按场景选择存储:
- 登录态 / 会话标识:cookie(配合 HttpOnly + SameSite),或短时 token 放 sessionStorage。
- 用户偏好(主题、语言、布局):localStorage,简单同步读写。
- 临时草稿 / 页面状态:sessionStorage,关标签页自动清理,不污染长期存储。
- 离线数据 / 缓存列表 / 大对象:IndexedDB,容量大且异步不阻塞 UI。
- 登录 token:优先考虑内存 + httpOnly cookie;存 localStorage 需搭配严谨的 XSS 防护。
一句话口诀:小数据同步读写选 localStorage,临时数据选 sessionStorage,会话标识选 cookie,大数据结构化数据选 IndexedDB。
六、隐私与安全注意
存储是安全重灾区,几个必须遵守的原则:
| 风险 | 说明 | 防护 |
|---|---|---|
| XSS 窃取 | 攻击脚本读到 localStorage 里的 token | 不存敏感凭证;对用户输入转义;HttpOnly cookie 让 JS 读不到 |
| 敏感数据入存储 | 密码、身份证、银行卡明文入库 | 一律不入浏览器存储,只放内存变量 |
| 第三方脚本 | 页面里的第三方 SDK 能读到同源存储 | 谨慎引入不可信脚本 |
| 用户清空 | 用户可随时清除所有存储 | 关键数据必须服务端留存,本地只是缓存 |
| 过期遗漏 | cookie/token 忘记续期或清除 | 统一管理有效期与登出清理逻辑 |
// 反例:把密码存进 localStorage
localStorage.setItem("password", userInputPassword); // 绝不这样做
// 正例:只存与安全无关的偏好
localStorage.setItem("theme", "dark");写入前对 key 做白名单校验、value 做大小与类型检查,也能避免异常数据污染存储空间:
const KEY_WHITELIST = new Set(["theme", "locale", "fontSize"]);
function safeSet(key, value) {
if (!KEY_WHITELIST.has(key)) return;
if (typeof value !== "string") value = JSON.stringify(value);
if (value.length > 1024 * 1024) return; // 单值限 1MB
localStorage.setItem(key, value);
}存储选型与安全是同一件事的两面:按数据敏感度决定存哪里,按数据体量决定用哪种方案。会话凭证交给服务端 cookie 与安全响应头,用户体验数据放 localStorage,离线大数据交给 IndexedDB,各归其位才能既顺畅又安全。