localStorage 没有 5MB 保证:Web Storage 的隔离规则、配额失败与选型
Web Storage API 提供浏览器端键/值存储,比 Cookie 直观。它有两个机制,返回不同的 Storage 对象,各自独立运行:
sessionStorage:为每个源(origin)维持独立的存储区,页面会话期间可用。浏览器或标签页关闭后数据清除。新标签打开页面时,会复制顶级会话上下文作为新会话上下文——这一点和 session cookie 的行为不同。打开多个相同 URL 的标签页,各自独立;同一标签页下,同源的 iframe 之间共享。localStorage:跨会话保留,没有过期时间,只能通过 JS、清缓存或手动清数据清除。同一 origin 的所有标签页和窗口共享。
通过 window.sessionStorage / window.localStorage 访问。
Storage 接口
| 成员 | 说明 |
|---|---|
length | 当前存储区中的键数量 |
key(index) | 取第 index 个键名,越界返回 null |
getItem(key) | 读取值,键不存在返回 null |
setItem(key, value) | 写入 |
removeItem(key) | 删除单个键 |
clear() | 清空整个存储区 |
键和值始终是字符串。整数 key 会自动转成字符串,存对象要先 JSON.stringify。取出来的是字符串,不是原来的类型,反序列化失败要自己兜住。
同源隔离:协议也参与
Storage 绑定到源,也就是协议 + 域名 + 端口三元组。http://example.com 和 https://example.com 是两个不同的存储对象,不同协议、不同子域也一样隔离。把站点从 http 切到 https,用户之前存的本地数据不会跟过来。
storage 事件:只发给别的标签页
Storage 对象发生变化(创建、更新、删除键)时,会在文档的 window 上触发 StorageEvent。几个容易记错的点:
- 重复设置相同的键值,不触发事件。
clear()至多触发一次。- 当前页面自己的改动不触发;同域下其他标签页和 iframe 会收到。
- 事件属性:
key、oldValue、newValue、url、storageArea。 - 用
obj.key = value这种属性赋值方式写,不触发该事件,要用setItem。
配额:5MB 不是保证
Web Storage 不随每个请求发送到服务器,服务器也无法通过 HTTP header 操纵它,所以能存的东西比 Cookie 多。多数现代浏览器至少给 5MB(有的更多),但配额由浏览器、设备、站点策略共同决定,不要把 5MB 当成跨浏览器的保证。
几个已知的具体限制:
- Firefox 在崩溃或重启后,每个源限制 10M。
- 用户禁用第三方 cookie 时,第三方 iframe 不允许访问 Web Storage。
- 隐身/隐私模式下,
localStorage被视为sessionStorage,关闭后清空,但存储 API 仍然可用。 - iOS 5.1+ 的移动 Safari 把 localStorage 存在 cache 文件中,系统空间不足时会被清理。
失败的表现
| 情况 | 表现 |
|---|---|
| 配额写满 | setItem 抛 QuotaExceededError |
| 来源不透明或策略禁止 | 访问 localStorage 抛 SecurityError |
| 隐身模式 / 第三方 iframe 被拒 | 可用但生命周期缩短,或直接不可访问 |
| iOS 空间不足 | 数据被系统清理,读出来是 null |
注意一个细节:仅仅断言 localStorage 是否存在也可能抛异常,所以能力探测必须包在 try/catch 里,不能靠 !!window.localStorage。
安全:同源 JS 能读走一切
localStorage 和 sessionStorage 都能被同源 JS 读取。不要把密码、长期访问令牌这类凭据放进去——一旦站点存在 XSS,攻击者直接把它们全读走。认证 Cookie 应当由服务端设置,并带上 Secure、HttpOnly、SameSite;HttpOnly 的 Cookie 无法被 JS 读取。
放哪里:三种容器的取舍
| localStorage | sessionStorage | Cookie / 服务端 | |
|---|---|---|---|
| 生命周期 | 无过期时间,手动清 | 标签页会话结束即失效 | 可设过期时间 |
| 共享范围 | 同源所有标签页/窗口 | 单个标签页,同源 iframe 共享 | 同源,受 path 限制 |
| 随请求发送 | 否 | 否 | 是 |
| 服务端可读 | 否 | 否 | 是(HttpOnly 时仅服务端) |
| 容量 | 由浏览器策略决定,常见 ≥5MB | 同上 | KB 级 |
按这个表落地:
- 放
localStorage:主题偏好、语言、上次打开的筛选条件、非敏感的草稿。丢了不影响登录态,重进页面能重建。 - 放
sessionStorage:单次流程的中间状态,比如多步表单、一次性校验标记。用户新开标签页就该从零开始的东西放这里。 - 放 Cookie / 服务端:认证态、权限、任何服务端需要校验的东西。客户端存的信息,服务端不能当事实。
代码片段
能力探测
function storageAvailable(type) {
try {
const storage = window[type];
const k = '__storage_test__';
storage.setItem(k, '1');
storage.removeItem(k);
return true;
} catch (e) {
return false;
}
}
如果要区分失败原因,可以多看一眼异常名:QuotaExceededError 说明配额满了,SecurityError 说明这个来源根本不允许访问。
JSON 读写封装
const store = {
set(key, value) {
localStorage.setItem(key, JSON.stringify(value));
},
get(key, fallback = null) {
const raw = localStorage.getItem(key);
if (raw === null) return fallback;
try {
return JSON.parse(raw);
} catch {
return fallback; // 值被外部改坏时不至于让页面挂掉
}
},
remove(key) {
localStorage.removeItem(key);
},
};
写入也可能抛 QuotaExceededError,关键路径上要捕获。
跨标签页同步
window.addEventListener('storage', (e) => {
// sessionStorage 不跨标签页共享,这里只处理 localStorage
if (e.storageArea !== localStorage) return;
switch (e.key) {
case 'theme':
applyTheme(e.newValue);
break;
case 'logout':
location.reload();
break;
}
// e.key / e.oldValue / e.newValue / e.url 都可读;clear() 时 e.key 为 null
});
写数据的那一侧不会收到事件,UI 需要自己同步更新一次,别指望事件回调替你做这件事。