全文约2200字,预计阅读8分钟。
页面要记住登录态、要缓存一份列表、要存一段用户的草稿,下意识就是"写个本地存储吧"。但浏览器里能存东西的地方不止一个,Cookie、localStorage、sessionStorage、IndexedDB 各有各的脾气,用错了轻则数据存不进去,重则把隐私模式下的页面直接搞崩。这篇把四种常见方案的容量、生命周期、用法和坑串一遍,附上代码和踩坑记录,下次选型时照着对号入座就行。
01 前言:先搞清楚你要存什么
很多选型纠结,根源是没先把"要存的东西"分清楚。动笔写代码之前,先问三个问题:
- 存多少:是几十字节的一个标记,还是几十兆的结构化数据?
- 存多久:是关掉标签页就该没,还是下次打开还要在?
- 存给谁用:只是前端自己读,还是每次请求都要带给服务端?
这三个问题的答案不同,合适的方案完全不一样。先分类,再挑工具,能省掉一半返工。
02 四种方案先对号入座
| 方案 | 容量 | 生命周期 | 是否随请求自动带上 | 适合存什么 |
|---|---|---|---|---|
| Cookie | 约4KB | 可设置过期时间 | 是,每次同域请求都带 | 登录态标识、需要服务端读到的标记 |
| localStorage | 数MB级 | 持久,手动才清 | 否 | 前端自己用的偏好、缓存 |
| sessionStorage | 数MB级 | 标签页关闭即清 | 否 | 单次会话内的临时数据 |
| IndexedDB | 数百MB以上 | 持久,手动才清 | 否 | 结构化数据、离线缓存、大量记录 |
一句话记忆:要带给服务端的小标记用 Cookie;前端自己长期存的小数据用 localStorage;只在当前会话有用的用 sessionStorage;数据量大、要按条件查的用 IndexedDB。
03 Cookie:小,但会跟着请求跑
Cookie 最大的特点不是能存东西,而是每次同域请求都会自动带上它。这既是它的用处,也是它的坑。
写入和读取:
js
// 写入,path 与 expires 决定生效路径和过期时间
document.cookie = 'token=abc123; path=/; max-age=604800; SameSite=Lax'
// 读取是一整串,需要自己拆
document.cookie.split('; ').reduce((obj, item) => {
const [k, v] = item.split('=')
obj[k] = v
return obj
}, {})
要点:
- 容量只有 4KB 左右,别想着往里塞用户信息、塞列表,塞不下还会把每个请求都拖慢。
- 敏感字段加 HttpOnly 和 Secure:能被 JS 读到的 Cookie,就有被脚本偷走的风险。真正的登录凭证,更适合放在 HttpOnly 的 Cookie 里,JS 根本读不到。
- SameSite 要显式设:不设的话跨站场景下浏览器的默认策略可能和你预期不一致,导致登录态莫名丢失。
04 localStorage 与 sessionStorage:一对双胞胎
这俩用法几乎一模一样,区别只在生命周期:localStorage 关掉浏览器还在,sessionStorage 关掉当前标签页就没了。
js
// 存(注意只能存字符串)
localStorage.setItem('theme', 'dark')
// 取
const theme = localStorage.getItem('theme')
// 删
localStorage.removeItem('theme')
存对象要先序列化 :它们只能存字符串,直接塞对象会变成 [object Object],这是新手最常踩的坑。
js
// 正确做法:JSON 转一道
localStorage.setItem('user', JSON.stringify({ name: '小明', role: 'admin' }))
// 读出来再 parse,记得兜一层 try/catch
let user = null
try {
user = JSON.parse(localStorage.getItem('user'))
} catch (e) {
// 存的内容被改坏了,别让整个页面挂掉
}
两者怎么选:要长期保留的用户偏好、最近一次的筛选条件,用 localStorage;只是填了一半的表单、当前向导走到第几步,关掉页面就该清空的,用 sessionStorage。
05 IndexedDB:数据多了再上
当你要存几十上百条记录、还要按条件查询,localStorage 就力不从心了------它本质是个键值表,查一条得把整坨 JSON 读出来再自己过滤。这时候该上 IndexedDB。
它是浏览器内置的事务型数据库,API 偏底层,写起来啰嗦一点,但能力和容量都大得多。一个最薄的封装长这样:
js
function openStore(dbName, storeName) {
return new Promise((resolve, reject) => {
const req = indexedDB.open(dbName, 1)
req.onupgradeneeded = () => {
const db = req.result
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName, { keyPath: 'id' })
}
}
req.onsuccess = () => resolve(req.result)
req.onerror = () => reject(req.error)
})
}
async function put(store, value) {
const tx = store.transaction ? store : null
// 实际项目里按 db.transaction 开读写事务再 add/put
}
要点:
- 它是异步的,API 基于回调和请求对象,不熟悉的话建议包一层 Promise 再用,别在业务里直接写一堆 onsuccess。
- 有事务和索引,适合"一批记录、能按某个字段查"的场景,比如离线缓存的文章列表、本地草稿箱。
- 别为了存一两个字段就上 IndexedDB,杀鸡用牛刀,徒增维护成本。
06 选型判断流程
不知道用哪个时,按这个顺序问自己:
- 这个数据需不需要每次请求都带给服务端?要 → Cookie;
- 数据量是不是几KB以内、纯前端自己用?是 → 长期用 localStorage,单次会话用 sessionStorage;
- 数据量大、需要结构化查询?是 → IndexedDB。
实际项目里常见的组合是:登录凭证放 HttpOnly Cookie,界面偏好放 localStorage,离线缓存的列表走 IndexedDB。各管一摊,别互相串。
07 踩坑记录
踩坑一:存对象没序列化,读出来是 object Object。 修复:凡是存对象,一律 JSON.stringify 进去、JSON.parse 出来,并且 parse 外面包 try/catch。
踩坑二:隐私模式下写入直接报错。 很多浏览器在无痕/隐私模式下会限制或禁掉本地存储,setItem 可能直接抛异常。修复:所有写入都包一层容错,存不进去就降级成内存变量,别让脚本崩在头一行。
踩坑三:存超容量被截断或抛错。 localStorage 不是无限大,塞太多会触发配额异常。修复:写入前做防御,catch 到配额错误时清理一批旧数据再重试,而不是直接把页面搞白。
踩坑四:storage 事件被滥用。 storage 事件只在"其他标签页"修改存储时触发,当前页自己改不会触发。想跨页通信是对的,但别指望它监听自己这一页的变化。
踩坑五:把敏感信息明文塞进 localStorage。 localStorage 能被同域脚本任意读取,一旦有 XSS,里面的东西就等于公开。敏感、能冒充身份的信息别放这儿,放 HttpOnly Cookie 更稳。
踩坑六:以为 sessionStorage 能跨标签页共享。 它是跟当前标签页绑定的,新开一个标签页就是另一份。需要跨页共享的,得用 localStorage 或广播机制。
08 总结
前端本地存储没有万能方案,按数据形态分:
- 要带给服务端、量小的标记 → Cookie,敏感的加 HttpOnly;
- 前端长期存的小数据 → localStorage;
- 只在当前会话有效的临时数据 → sessionStorage;
- 大量结构化、要查询的数据 → IndexedDB。
记住两条底线:存对象先序列化、写入永远包容错;敏感身份信息别交给能被 JS 读的地方。把这几条守住,日常项目里的本地存储基本不会出大问题。选型时多花十秒钟想清楚"存多少、存多久、给谁用",比后面对着报错调半天强得多。