ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

localStorage与sessionStorage实战:告别Cookie存储困境

2026/9/20 14:56:46 拓冰建站 浏览量
localStorage与sessionStorage实战:告别Cookie存储困境 前几天一个同事跑过来跟我诉苦他把用户配置塞进了 cookie结果每次接口请求都背着这坨数据到处跑响应时间肉眼可见地变慢了。我问他为什么不放 localStorage他愣了一下反问“那玩意儿不是有兼容问题吗”这大概是很多前端开发者的共同误区——把 Web Storage 当成“新东西”不敢用转头继续在 4KB 的 cookie 里塞各种不该塞的数据。实际上localStorage 和 sessionStorage 从 IE8 时代就存在了如今所有主流浏览器全面支持就连微信内置浏览器、各类 App 的 WebView 都能稳定使用。这篇文章不打算堆概念我直接从真实项目里会遇到的问题出发cookie 到底哪里不好用、localStorage 和 sessionStorage 怎么选、按 id 删除数据怎么写、登录态该放哪、以及我在各种项目里踩过的坑一次性说透。1. Cookie 在真实项目里的四大硬伤逼着你换赛道1.1 4KB 的容量天花板存点配置就报警Cookie 的容量限制是 4KB 左右具体看浏览器实现通常一个域名下所有 cookie 总和不超过 4KB 是安全线。这是什么概念一个普通的用户配置对象序列化成 JSON 之后动辄 1-2KB再塞点登录态、埋点标识、AB 实验分组还没开口问接口要数据cookie 已经快满了。我见过一个项目为了硬塞数据把 JSON 里的 key 全部改成单字母缩写代码可读性直接归零后来还是换了 localStorage 才解脱。而且 cookie 里存中文还有个额外麻烦cookie 值本身不支持非 ASCII 字符写中文必须走 encodeURIComponent读出来还要 decode稍不注意就是乱码。localStorage 压根没这个烦恼值虽然也是字符串但 UTF-8 中文直接读写不用转码。实际调用几个典型数据项做对比差距一目了然数据项体积估算Cookie 4KBlocalStorage 5MB用户偏好设置约 800B能存 3-5 个几千个购物车商品20 件约 3KB勉强到极限绰绰有余接口响应缓存文章列表约 50KB完全不行可以1.2 每次请求都自动带上带宽浪费于无形Cookie 最“坑”的一点是你用它存任何东西它都会在每次同源请求的请求头里自动带上。前端看不到这块开销但服务器端和网络层感受明显。如果你在一个接口密集的页面上存了 3KB 的 cookie用户每点一个按钮这 3KB 就在请求里往返一次10 个接口就是 30KB 的冗余传输。在移动端弱网环境下这种浪费直接影响首屏加载速度和接口响应时间。localStorage 的数据只存在于浏览器本地不会参与任何请求头这是它最根本的优势。数据存得再多网络层完全无感知。1.3 XSS 面前毫无还手之力HttpOnly 也救不了所有场景说到安全问题cookie 有一个看似安全实则不完全安全的设定HttpOnly 标记的 cookie 无法被 JavaScript 读取能有效防止 XSS 盗取。但 HttpOnly 是服务器端设置 cookie 时才能加的属性如果你在前端用 document.cookie 写入数据那这些数据天然对 XSS 敞开大门。而 localStorage 里的数据只要页面存在一处 XSS 漏洞攻击者一行 js 就能全部打包带走。所以“用 localStorage 取代 cookie”并不是一刀切后面第 5 章我会专门讲清楚什么数据可以放、怎么放才安全。这里先记住结论敏感信息不管存哪都有风险区别只是风险大小和缓解手段不同。1.4 浏览器三方规则收紧Cookie 的使用成本越来越高过去几年主流浏览器先后调整了第三方 Cookie 策略跨域场景下的 cookie 越来越难存活。很多开发者应该都遇到过嵌入第三方 iframe 时登录态莫名失效、跨子域共享 cookie 需要手动设置 domain、浏览器对 SameSite 的默认值调整导致旧接口突然返回 401。这些问题不是代码 bug是平台策略变化的结果你很难通过改业务代码去对抗。相比之下Web Storage 的同源规则稳定得多很少因为浏览器升级突然不能用了。这也让 Web Storage 在“本地状态管理”这个赛道上越来越有吸引力。2. localStorage 和 sessionStorage同一套 API两套脾气2.1 生命周期关标签页、关窗口、关浏览器分别是哪种结果两者 API 完全一样区别只在生命周期和作用域。localStorage 除非你主动 removeItem 或 clear否则会一直存在关标签页、关浏览器、重启电脑都在。sessionStorage 的生命周期绑定的是“页面会话”——关闭标签页或窗口数据就没了。但要注意刷新页面 sessionStorage 不会清空只有关闭当前标签页或窗口才会清。这里的实际选型经验是需要持久化保存的用户偏好、主题色、草稿箱、非敏感缓存 → localStorage只要会话内有效的表单步骤状态、临时筛选条件、页面间跳转传参 → sessionStorage2.2 同源限制下的共享与隔离为什么两个标签页数据不一样localStorage 在所有同源标签页之间共享一个页面写入另一个页面立刻能读到。sessionStorage 则严格隔离到单个标签页——同一网站开两个标签页它们在 sessionStorage 里各自为政互不可见。这个特性在实际项目中经常被忽略。有次我在新标签页里改了主题色设置用 sessionStorage 存的切回旧标签页发现完全没变化反过来排查了半天才意识到是作用域问题。所以“标签页级别隔离”的需求用 sessionStorage“跨标签页共享”的需求用 localStorage。另外协议和端口不同也会导致存储隔离http 和 https 下的 localStorage 是两套这一点在本地联调和线上环境切换时要格外注意。你在 http://localhost:8080 写入的数据切到 https://localhost:8443 是读不到的反之亦然。2.3 容量与存储介质5MB 听着小用起来其实很能装单域名下 localStorage 的容量一般是 5-10MB具体因浏览器而异是 cookie 的上千倍。但它不是无限空间存大字符串比如 base64 图片时很容易撞顶。我的建议是只存“可再生的数据”比如接口响应的缓存、用户操作产生的状态不要把上传的图片、视频这类大文件往 localStorage 里塞既占空间又没有意义。sessionStorage 的容量规则和 localStorage 基本一致同样是字符串键值对。3. 核心 API 上手读写删清四件套 遍历技巧3.1 setItem、getItem、removeItem 的标准姿势Web Storage 的 API 少得可怜核心就三个方法// 写入 localStorage.setItem(username, 张三); // 读取 const name localStorage.getItem(username); // 张三 // 删除单个 localStorage.removeItem(username);这里有几个细节容易踩坑。第一getItem 在 key 不存在时返回 null 而不是 undefined所以判断有没有数据用! null比! undefined更严谨第二存储的值永远是字符串数字存进去再拿出来会变成字符串 18做加法运算前要先 Number() 转换第三setItem 重复写同一个 key 是覆盖而不是报错这在初始化默认配置时很方便。sessionStorage 的用法一模一样把 localStorage 换成 sessionStorage 即可。提示在生产代码中建议把存储操作封装一层函数不要把 localStorage 直接散落在业务代码里。后面第 6 章会给出完整封装示例。3.2 存储对象的正确打开方式JSON 序列化与反序列化既然存储值只能是字符串那存对象必须经过 JSON 序列化const user { id: 1, name: 张三, tags: [前端, Vue] }; localStorage.setItem(userInfo, JSON.stringify(user)); // 读取时反序列化 const raw localStorage.getItem(userInfo); const userInfo raw ? JSON.parse(raw) : null;这段代码看着简单但真实项目里最容易出问题的是 JSON.parse 这一步。如果之前写入的数据格式被改过、或者用户手动在 DevTools 里改坏了、或者数据被另外一段代码覆盖成非法 JSONJSON.parse 会直接抛异常导致整个页面白屏。所以读取时一定要做容错function safeParse(raw, fallback null) { try { return JSON.parse(raw); } catch { return fallback; } }3.3 用 key() 和 length 遍历存储以及 clear 的杀伤半径除了三个基础方法还有这两个冷门 API// 存储条目总数 localStorage.length; // 按下标获取 key localStorage.key(0); // 第 0 个条目对应的 key // 遍历所有条目 for (let i 0; i localStorage.length; i) { const key localStorage.key(i); const value localStorage.getItem(key); console.log(key, value); }key() length 的组合在“批量导出/迁移数据”场景下很管用。我做过一个导出功能把用户本地所有配置打成 JSON 文件供下载再用 clear 清掉新版本上线后导入实现配置无损迁移。clear 是“一键清空整个域下所有 Web Storage 数据”的核武器调用前务必想清楚。有时候项目里多个模块共用 localStorage一个模块调了 clear其他模块的缓存全没了。我的建议是宁可用 removeItem 逐个删也不要在没有独享命名空间的情况下用 clear。4. 热搜问题实战按 id 删除 localStorage 里的某一项4.1 为什么“按 id 删除”会成为高频搜索社区里搜“根据id删除localstorage数据”的人非常多说明很多人存的数据都是一个“对象数组”结构比如购物车列表、收藏列表、待办事项、最近浏览记录每条记录都有自己的 id。但 localStorage 的 API 只支持按 key 操作根本没有“按 id 删数组元素”这种概念所以新手在这里卡住非常正常。问题的本质是localStorage 只能粗暴地“整存整取”你要修改细节必须自己把数据读出来、在内存里改好、再整份写回去。理解了这一点所谓的“按 id 删除”其实就是一次读-过滤-写的过程。4.2 从数组过滤到工具函数封装deleteById 的完整实现先看最直接的实现// 假设数据结构localStorage.getItem(todoList) 返回一个对象数组 // 每条数据形如 { id: 3, title: 写博客, done: false } function deleteTodoById(id) { const raw localStorage.getItem(todoList); if (!raw) return; const list JSON.parse(raw); const newList list.filter((item) item.id ! id); localStorage.setItem(todoList, JSON.stringify(newList)); }就这么几行核心是 Array.prototype.filter 的过滤逻辑。但把它封装成通用工具后可以适用更广的场景// 按条件删除支持传入谓词函数 function removeFromStorage(key, predicate) { const raw localStorage.getItem(key); if (!raw) return 0; let list; try { list JSON.parse(raw); } catch { return 0; } if (!Array.isArray(list)) { console.warn([storage] ${key} 对应的数据不是数组跳过删除); return 0; } const filtered list.filter((item) !predicate(item)); localStorage.setItem(key, JSON.stringify(filtered)); return list.length - filtered.length; // 返回删除条数 } // 按 id 删除 removeFromStorage(todoList, (item) item.id 3); // 按条件批量删除删除所有已完成任务 removeFromStorage(todoList, (item) item.done true);这个封装的价值在于删完返回实际删除数量方便业务层提示“已删除 N 条”对非数组数据做了防御谓词函数让调用方任意定制条件不局限于 id。4.3 删错了怎么恢复导出备份才是真正的后悔药“按 id 删除”用起来顺手但也容易误操作。我在项目里给的方案是在 DevTools 的 Console 里执行一段导出脚本先把当前所有 localStorage 数据备份成 JSON 文件// 导出全部 localStorage 数据为 JSON 文件 const allData {}; for (let i 0; i localStorage.length; i) { const key localStorage.key(i); allData[key] localStorage.getItem(key); } const blob new Blob([JSON.stringify(allData, null, 2)], { type: application/json }); const a document.createElement(a); a.href URL.createObjectURL(blob); a.download localStorage-backup-${Date.now()}.json; a.click(); URL.revokeObjectURL(a.href);恢复的时候用存储事件或者直接在 Console 里跑一个循环把数据写回去。这套备份方案不依赖任何第三方库在排障、迁移、试验新版本时都非常实用。我通常会在改版本前先导出一次确保新版本跑了 dev 分支代码后还能把本地状态找回来。5. 登录态与 TokenlocalStorage 不是银弹Cookie 也别一棍子打死5.1 前端存 Token 的三种常见姿势和翻车现场热搜里“cookie登录”“cookie和session实现登录”排得很靠前但我要先泼一盆冷水如果你是为了让后端拿到登录态那前端把 Token 存哪里只是整条链路里的最后一公里。目前常见姿势有三种第一种Token 存 localStorage。优点是 JS 可读可写发起请求时从 localStorage 取出来塞进 Authorization 头即可配套起来非常顺手缺点是一旦页面被 XSS 攻破攻击者可以拿到 Token 模拟登录而且没法通过 HttpOnly 防护。第二种Token 存 cookie 并设置 HttpOnly。安全等级最高JS 拿不到XSS 偷不走但跨域、CSRF 问题随之而来需要配合 CSRF Token 双重校验。第三种Token 存内存变量页面刷新后重新请求换取。最安全但体验最差刷新一下登录态就没了通常需要配合“刷新 Token”机制才能保证连续操作不中断。5.2 XSS 与 CSRF 的安全边界到底该把 Token 放哪很多人纠结 Token 放 localStorage 会被 XSS 偷放 cookie 会被 CSRF 打其实这两个问题要分开看。XSS 的攻击路径是“页面脚本被注入”攻击者拿到的危害等价于页面自身 JS 的危害所以无论你存 localStorage 还是用 document.cookie 写入的 cookie都能被偷只有 HttpOnly cookie 能挡住。CSRF 的攻击路径是“浏览器自动携带 cookie”攻击者诱导你在已登录状态下发起一个跨站请求你无法干预请求头后端无法区分这个请求是不是你本人主动发起的而 localStorage 里的 Token 不会自动携带反而天然免疫 CSRF。安全边界划下来是这样威胁localStorage 存 TokenHttpOnly Cookie 存 TokenXSS 窃取会被偷偷不到CSRF 伪造不会自动携带基本免疫自动携带需额外防护前端读取方便读不到只能靠后端下发5.3 个人推荐的登录态方案HttpOnly Cookie 二次校验在真实项目里我通常采用组合方案短期 Access Token 用 HttpOnly Cookie 下发同时后端返回一个 CSRF Token普通 cookie 或接口返回体都行前端在请求头里带上它后端校验通过再放行Refresh Token 存 HttpOnly Cookie 或服务端 Session用于静默续期。这个方案既挡得住大部分 XSS 窃取又用 CSRF Token 抵消了 cookie 自动携带的短板。如果项目实在没有条件上 HttpOnly比如纯前端 Mock、或后端不配合改 cookie 属性那我宁可把非敏感 Token 放 sessionStorage也不放 localStorage——至少能减少会话持久化带来的泄露面积用户关掉页面登录态就没了攻击面小一大截。注意这里的逻辑是能上 HttpOnly 的场景直接上 HttpOnly上不了才用 sessionStorage 兜底别为了省事把所有登录态都丢 localStorage。6. 进阶玩法跨标签页通信、过期时间与数据同步6.1 storage 事件实时感知其他页面的数据变更localStorage 虽然是浏览器本地存储但当你在 A 标签页修改或删除某条数据时同源的其他标签页B、C会触发一个 storage 事件。这个事件的监听方式很特殊——它只会在“其他页面”触发当前写数据的页面自己是不会收到事件的window.addEventListener(storage, (event) { console.log(变化的 key:, event.key); console.log(旧值:, event.oldValue); console.log(新值:, event.newValue); console.log(来源页面:, event.url); });这个特性在“多标签页数据同步”场景里非常实用。比如后台管理系统的左侧菜单折叠状态用户在一个标签页里折叠后另一个标签页应该跟着同步这时用 storage 事件广播状态变化比轮询接口高效得多。我用它做过一个简易的“跨标签页登出联动”一个标签页收到 401 后写入一个 logout 标记其他标签页监听 storage 事件检测到标记自动清理本地数据并跳转登录页。这个方案比在每个页面的请求拦截器里做轮询干净得多。6.2 封装一个支持过期时间的 storage 工具顺手解决读脏数据localStorage 原生不提供过期时间但很多业务数据有明确的时效性比如接口响应缓存希望 5 分钟后过期埋点配置希望 1 小时后失效。与其在业务代码里到处写“拿到数据先比对时间戳”不如封装一个带 TTL 的存储工具const smartStorage { set(key, value, ttlMs) { const payload { value, expireAt: ttlMs ? Date.now() ttlMs : null }; localStorage.setItem(key, JSON.stringify(payload)); }, get(key) { const raw localStorage.getItem(key); if (!raw) return null; try { const payload JSON.parse(raw); if (payload.expireAt Date.now() payload.expireAt) { localStorage.removeItem(key); return null; } return payload.value; } catch { return null; } }, remove(key) { localStorage.removeItem(key); } }; // 使用示例缓存接口数据 5 分钟 smartStorage.set(articleList, data, 5 * 60 * 1000); const cached smartStorage.get(articleList);这套封装我会放到一个单独的工具文件里在所有项目里复用。它顺手解决了另一个痛点读取到被改坏的 JSON 时直接返回 null而不是抛异常让页面崩溃。7. 实测开箱经验隐私模式、超限写入与调试排查7.1 隐私模式下 setItem 静默失败的坑有段时间测试反馈说“主题色设置总是丢”我排查了半天发现是在浏览器无痕模式下测的。隐私模式下 localStorage 依然存在、也能读写但浏览器会把它隔离在内存里关闭无痕窗口后数据也不存在了。更坑的是某些老版本浏览器在隐私模式下直接对 localStorage 的写入抛 QuotaExceededError 异常或者干脆让 localStorage 不可用。你能做的就是写入前做能力检测写入时做异常捕获function safeSetItem(key, value) { try { localStorage.setItem(key, value); return true; } catch (err) { console.warn(写入 localStorage 失败, err); return false; } }7.2 容量超限时报错捕获与优雅降级按 5MB 来算正常情况下业务数据很难写满但如果你在页面上缓存了多页列表数据、或者把图片 base64 存进去了就很容易触顶。触顶时 setItem 会抛 QuotaExceededError页面 UI 不感知但数据写不进去后续读到的还是旧数据。处理思路是捕获异常后做降级——删除最久未使用的缓存条目、缩小缓存粒度、或者干脆放弃写入并通知用户“本地空间已满”。我在封装 smartStorage 时会在 set 方法里套一层 try-catch捕获到 QuotaExceededError 就尝试清掉最老的几个 key 后重试一次仍失败再向上抛。7.3 DevTools 里快速定位存储问题的手段调试 Web Storage最常用的是 Chrome DevTools 的 Application 面板左侧 Local Storage 和 Session Storage 树形菜单能直接看到每个域下的键值对。右键可以编辑、删除、清空某条数据改完刷新页面立即生效非常适合验证“某个 key 缺失时页面行为是否正确”。如果你要模拟存储被 XSS 改坏的场景直接在 Console 里执行localStorage.setItem(token, hacked)再检查页面表现比改代码重新部署高效得多。另外Network 面板可以配合查看请求头里 cookie 的发送情况判断是不是 cookie 体积过大拖慢了接口。还有个细节做接口联调时如果浏览器环境不方便比如要验证登录态但又不想走完整页面流程Postman、JMeter 这类工具都支持手动管理 Cookie/Token。JWT 类接口直接往请求头加 Authorization依赖会话 Cookie 的接口在工具里手动添加 Cookie 头即可。用这种方法可以绕过页面 UI直接验证后端对登录态的判断逻辑。我把 Web Storage 在真实项目里的使用边界理了一遍之后最想强调的一点是localStorage 和 sessionStorage 确实能解决一大批 cookie 解决不了的存储问题但它们不是 cookie 的全方位替代品。存储登录态、防止 XSS、做服务端会话管理这些场景 cookie 依然有不可替代的价值。正确的心态不是非此即彼而是按数据性质分类会话级的、需要服务端控制的、必须隐蔽的走 cookie纯客户端偏好、缓存、草稿、临时状态走 Web Storage。我在项目里落地这套方案后接口请求体积明显下降也不再被 4KB 的容量卡脖子。如果你正在为“数据存哪”发愁建议先从最简单的一步做起——把那些不该进 cookie 的配置数据迁到 localStorage再按上面的封装把读写统一管理起来很快你就能感受到差距。