前端 Cookie 读写完全指南:从原理到封装,避开路径与编码的坑
2026/9/24 19:19:11 网站建设 项目流程

做前端这么多年,cookie 这个东西几乎天天碰,但说实话,很多人对它的理解停留在“document.cookie = 'xxx'”这种程度。真要问起来,路径为什么失效、中文为什么乱码、对象怎么存、删除为什么删不掉,能一口气说清楚的人并不多。这篇博客不聊库、不聊框架,就用纯 js 把前端读写 cookie 这件事彻底讲明白,从原理到封装再到排坑,全部过一遍。

这篇文章适合谁看?刚入门想搞懂 cookie 机制的前端新手,被 cookie 路径和编码问题折磨过的开发,备前端面试被问到“cookie 和 localStorage 有什么区别”的求职者,都应该能从中拿到点东西。核心内容不依赖任何框架,原生 JavaScript 直接可跑,看完就能用到自己的项目里。

1. cookie 在今天的开发里为什么还在用

1.1 cookie 的身份地位:比想象中更重要

先给个结论:虽然现在有了 localStorage、sessionStorage、IndexedDB 这些花里胡哨的浏览器存储方案,但 cookie 在 web 开发里依然不可替代。原因是它的一个独特本质——cookie 会在每次 HTTP 请求时自动携带到服务端。也就是说,cookie 不光是浏览器本地存储,它还是前端和服务端之间传递状态的一种约定。

举几个最常见的场景:

  • 用户登录后,服务端下发一个 sessionId 存到 cookie,之后每次请求自动带上,服务端就能识别当前是谁。
  • 埋点统计,把用户标识 uid 写进 cookie,上报数据时一起发到服务端。
  • 偏号设置,比如用户切换了深色模式、改了语言,写进 cookie,下次访问依然能记住。
  • A/B 实验分组,用户被分到哪一组需要持久化,cookie 是默认的方案。

你会发现,凡是需要“服务端也能读到的前端状态”,基本都靠 cookie 来完成。localStorage 仅存前端,请求不会自动带上,除非你用 JS 手动塞到 header 里,但这会引入 CSRF 等一堆安全风险,没人会这么干。

1.2 cookie 和其他存储方案怎么选

前端常用的几种存储,我直接用一张表说明白,面试也常考这个对比:

存储方案容量请求自动携带生命周期前端可脚本读取
cookie单个约 4KB可设置,不设则为会话级可,HttpOnly 除外
localStorage约 5-10MB永久,需手动清理
sessionStorage约 5-10MB标签页关闭即失效
IndexedDB很大(GB 级)持久

如果你只是想在浏览器本地存点非敏感数据,比如用户偏好、草稿内容,优先用 localStorage,容量更大、API 更简单、请求不带出去还省流量。但如果这个数据需要服务端在请求时自动获取,或者需要遵循 cookie 的过期策略、安全策略,那就只能用 cookie。

所以我的建议是:不要纠结“谁替代谁”,它们在各自的位置上都有存在的理由。前端开发者的基本功,是每种方案都明白它的机制和坑,然后按需选型。

2. 读懂 cookie 的构成,再谈读写

2.1 document.cookie 到底做了什么

纯 js 操作 cookie,核心就一个接口:document.cookie。它有两个身份,读的时候是当前页面可访问的所有 cookie 拼接而成的字符串,写的时候是设置一个新 cookie 的入口。这种“读写同属性”的设计在 JS 里很少见,也导致很多新手一开始比较懵。

读取时,document.cookie返回的格式大概是这样的:

name1=value1; name2=value2; name3=value3

注意几个细节:

  • 每个 cookie 之间用分号加空格分隔。
  • 返回的只是键值对,不包含过期时间、路径、domain 等元信息,这些信息浏览器不会通过这个 API 吐给你。
  • 返回的是当前页面有权访问的所有 cookie。什么叫有权?取决于 cookie 的 path 和 domain 属性,稍后细讲。
  • 默认情况下,cookie 里的值不经过解码就是原始的编码状态。所以你自己写进去的时候用 encodeURIComponent 编码,读的时候必须手动 decodeURIComponent 解码。

写入时,document.cookie = 'name=value; expires=...; path=...; domain=...; secure'。这里有个反直觉的点:=赋值并不会覆盖掉之前的 cookie,而是追加一个新 cookie。同一 name 的 cookie 会被更新,但不同 name 的 cookie 会共存。也就是说,document.cookie的写操作本质是“增量写入”,不是“整体替换”。

很多人对 cookie 的存储位置有误解,以为它在浏览器某个目录里以文本文件存在。实际上,现代浏览器把 cookie 存储在本地数据库文件中,Chrome 用的是 SQLite 格式,但这些底层细节对前端开发者是透明的,你只需要通过 document.cookie 或 DevTools 的 Application 面板来操作和查看即可。

2.2 每一个属性字段的取舍逻辑

设置 cookie 时,可以在分号后面追加属性,每个属性都会影响 cookie 的行为。逐个拆解:

  • expires:过期时间,UTC 字符串格式。过了这个时间,cookie 会被浏览器自动清除。如果不设置,cookie 的生命周期是“会话级”,即浏览器标签页关闭就失效。
  • max-age:相对存活时间,单位秒。表示 cookie 从设置时刻起最多存活多少秒。它和 expires 同时存在时,max-age 优先级更高。现代开发建议用 max-age,因为它不用计算具体日期,更直观。
  • path:生效路径。默认是当前页面的路径。这个是坑王之王,很多“cookie 明明设置了但读不到”的问题都是 path 搞的鬼。
  • domain:生效域名。默认是当前域名,不包含子域名。如果你想让a.example.comb.example.com共享一个 cookie,需要显式设置domain=example.com
  • secure:布尔属性,只需写名字不需要赋值。设置后,cookie 只在 HTTPS 连接下才会被发送。本地开发用 http://localhost 时,localhost 被浏览器特殊对待,通常也能正常测试。
  • sameSite:控制第三方上下文的发送策略,取值 Strict、Lax、None。这已经是现代 web 安全里绕不开的字段了。默认行为在不同浏览器中略有差异,现代 Chrome 默认 Lax,效果是:用户在地址栏输入网址访问时携带 cookie,但通过链接跳转的跨站场景下限制部分携带。

说个我自己的习惯:但凡设置 cookie,path 一定显式写成/。因为默认机制是“当前路径”,你要是恰好在一个子路由页面设置了 cookie,就会发现其他页面全读不到,排查半天最后发现是 path 的问题。这是个成本极低但回报极高的好习惯。

2.3 为什么写 cookie 之前一定要编码

cookie 的值有几个字符限制:分号、逗号、空格等特殊字符会导致 cookie 解析异常。比如你存一个用户的昵称,叫“张三; admin”,这个分号会直接把 cookie 截断成两个部分,后面部分全部丢失。

解决方案就是用encodeURIComponent编码值,读取时用decodeURIComponent解码。这等于把特殊字符转成%XX格式,浏览器和 JS 都能正确处理。同理,中文也必须编码,不然有些浏览器会直接报错或者乱码。

我把编码这个点单独拎出来说,是因为它属于“不会报错但逻辑出错”的隐形问题。你不编码,大部分简单值跑得好好的,但一旦遇到中文、特殊符号、JSON 字符串,就翻车了。而翻车的表现还特别隐蔽,不是立刻报错,而是数据变成乱码或丢失。

3. 纯 js 读写 cookie 的完整封装实战

3.1 从零手写:一个能用在生产环境的封装

现在进入正题。下面这套封装是我自己项目里在用的,去掉了框架依赖,纯原生 JavaScript,逻辑很直白,每一行都有注释。别直接复制就完事,建议自己敲一遍,理解每个参数从哪里来、到哪里去。

const CookieUtil = { // 设置 cookie // name: cookie 名 // value: cookie 值,内部自动编码,传对象也行,会被序列化 // days: 存活天数,不传则默认会话级 // options: 额外配置 path、domain、secure、sameSite set(name, value, days = 0, options = {}) { const { path = '/', domain = '', secure = false, sameSite = 'Lax' } = options; // 值统一走编码:对象序列化成 JSON 字符串再编码 let encodedValue; if (typeof value === 'object') { encodedValue = encodeURIComponent(JSON.stringify(value)); } else { encodedValue = encodeURIComponent(value); } let cookieStr = `${encodeURIComponent(name)}=${encodedValue}`; // 过期时间 if (days > 0) { const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000); cookieStr += `; expires=${expires.toUTCString()}`; } else if (days < 0) { // days 传负数,直接走删除逻辑 const expires = new Date(0); cookieStr += `; expires=${expires.toUTCString()}`; } cookieStr += `; path=${path}`; if (domain) { cookieStr += `; domain=${domain}`; } if (secure) { cookieStr += '; secure'; } if (sameSite) { cookieStr += `; SameSite=${sameSite}`; } document.cookie = cookieStr; return cookieStr; }, // 获取 cookie 值 // 返回解码后的字符串;如果是对象序列化存的,自动转回对象 get(name) { const cookieArr = document.cookie.split('; '); for (let i = 0; i < cookieArr.length; i++) { const [rawKey, ...rest] = cookieArr[i].split('='); const key = decodeURIComponent(rawKey); if (key === name) { const value = decodeURIComponent(rest.join('=')); // 尝试把 JSON 字符串转回对象,失败则原样返回字符串 try { return JSON.parse(value); } catch (e) { return value; } } } return null; }, // 删除 cookie remove(name, options = {}) { this.set(name, '', -1, options); }, // 获取全部 cookie,返回对象 getAll() { const cookieArr = document.cookie.split('; '); const result = {}; cookieArr.forEach((cookie) => { if (!cookie) return; const [rawKey, ...rest] = cookie.split('='); const key = decodeURIComponent(rawKey); const value = decodeURIComponent(rest.join('=')); result[key] = value; }); return result; } };

这套封装解决的核心问题有四个:

  1. 编码解码全自动,调用方不用操心特殊字符和中文。
  2. 对象可以直接存取,内部用 JSON 序列化,取出来自动解析回对象。
  3. 删除不是单独写一段逻辑,而是复用 set,把 days 传负数,设置一个过去的时间点让浏览器立刻清除。
  4. 默认 path 设为/,规避最常用的路径坑。

3.2 过期时间的计算与边界

过期时间是 cookie 最容易算错的地方。这背后的核心是:expires属性接收的是UTC 字符串,不能用new Date()直接拼。很多人写的时候直接用new Date().toISOString(),这是错的——toISOString()返回的是带毫秒的 ISO 格式,Cookie 的标准格式类似Wed, 21 Oct 2026 07:28:00 GMT,虽然某些浏览器兼容 ISO,但标准应该用toUTCString()

比如你想让 cookie 活 7 天:

const days = 7; const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000); document.cookie = `token=abc; expires=${expires.toUTCString()}; path=/`;

这里Date.now()拿到的是当前毫秒时间戳,7 天的毫秒数是7 * 24 * 60 * 60 * 1000,相加后就是第 7 天后的时间点。之所以用这种方式而不是new Date('2026-01-01'),是为了保证过期时间是相对当前时间的,代码在任意时刻执行都正确。

max-age怎么写?更简单:

// 存活 7 天 document.cookie = `token=abc; max-age=${7 * 24 * 60 * 60}; path=/`; // 会话级 cookie:不设置 expires 和 max-age document.cookie = `token=abc; path=/`; // 立即删除:max-age=0 document.cookie = `token=abc; max-age=0; path=/`;

实测下来,如果你不需要计算具体到期日期,用 max-age 更省心,不用管时区、UTC 格式这些细节。但对老版本浏览器的兼容性,expires 更稳妥。我个人在浏览器环境会优先用 expires,在 Node 服务端中间件场景会基于框架默认行为处理。

还有一个边界:如果 days 传了小数,会怎样?比如days = 0.5,那expires就是 12 小时后到期。这其实是合理行为,不等于报错。但如果你传的是NaN,整个 cookie 字符串会变成非法值,浏览器会默默忽略这次写入,不报任何错。所以封装里最好加一层参数校验,比如判断typeof days !== 'number'就 return。

3.3 真实业务场景:登录状态存储与主题偏好

封装写完了,光看代码不够,带入真实场景才知道怎么用。我挑两个有代表性的场景展开。

场景一:登录态存储

用户登录成功后,服务端返回一个 token 和过期时间,前端把它写到 cookie 里。这里有两个选择:本地算过期天数,或者直接用服务端返回的过期时间。

简单做法是服务端返回一个expiresIn字段,单位秒。比如 7200 秒 = 2 小时,前端换算成天数:

const expiresInSeconds = 7200; // 服务端返回 const expiresInDays = expiresInSeconds / (24 * 60 * 60); // 约等于 0.083 天 CookieUtil.set('access_token', response.token, expiresInDays);

但更稳妥的做法是直接用服务端下发的绝对过期时间。服务端有时会返回expires_at这种具体时间戳,前端就不需要自己换算天数了:

// 服务端返回 expires_at 为毫秒级时间戳 function setTokenWithExpiresAt(name, token, expiresAt) { const secondsLeft = Math.max(0, Math.floor((expiresAt - Date.now()) / 1000)); const cookieStr = `${encodeURIComponent(name)}=${encodeURIComponent(token)}; max-age=${secondsLeft}; path=/`; document.cookie = cookieStr; }

用 max-age 的优势在这就体现出来了:不需要把剩余时间换算成天数,直接上秒数。而且Math.max(0, ...)保证了过期时间已经过去时,不会写出负数 max-age(浏览器虽然也兼容,但保险起见还是自己处理一下)。

场景二:主题偏好

用户切换深色模式,前端把偏好存到 cookie。这里考验的是对象存取:

// 存主题偏好 const themeSettings = { mode: 'dark', accentColor: '#1890ff', fontSize: 14 }; CookieUtil.set('theme_preference', themeSettings, 30); // 存 30 天 // 读回主题偏好 const savedTheme = CookieUtil.get('theme_preference'); if (savedTheme) { document.documentElement.setAttribute('data-theme', savedTheme.mode); }

因为封装内部自动做了序列化,存进去的是encodeURIComponent(JSON.stringify(themeSettings)),读出来JSON.parse还原成对象。如果 get 的时候解析失败,会 fallback 返回原始字符串,不会抛异常搞崩页面。这种容错在实际开发里非常重要——因为你永远不知道用户或者上一个开发者往 cookie 里塞了什么奇怪的值。

4. 常见问题与排查技巧实录

4.1 cookie 问题速查表

先把最常踩的坑汇总成一张表,快速定位问题方向:

现象大概率原因解决方案
设置了 cookie,但其他页面读不到path 默认是当前路径设置时显式加path=/
中文或特殊符号乱码/丢失未编码或编码不完整统一encodeURIComponent写入,decodeURIComponent读取
存对象/数组,取出来是[object Object]直接document.cookie = 'obj=' + objJSON.stringify再编码存入
cookie 删不掉删除时的 path 和设置时不一致删除必须带上和写入时相同的 path、domain
设置后刷新页面失效没设置 expires 或 max-age需要持久化时必须设置过期时间
获取的值里带%乱码读的时候忘了解码读取时手动decodeURIComponent
页面访问时 cookie 没带上HttpOnly 限制或 SameSite 限制HttpOnly 的 cookie 前端读不到,属于正常行为;SameSite 需按场景调
子域名之间 cookie 不互通未设置 domain顶级域名下设置domain=example.com

4.2 重点问题展开:路径失效、乱码、对象存取

路径失效

这是 cookie 新手最经典的翻车现场。假设你在https://example.com/dashboard/settings这个页面执行了document.cookie = 'theme=dark',没有写 path。这个 cookie 的实际生效路径是/dashboard/settings,也就是说只有/dashboard/settings及其子路径能读到它。你再去https://example.com/home,这个 cookie 就像消失了一样。

原理在于:浏览器判断一个 cookie 是否能发送/访问,依据是请求的路径是否匹配 cookie 的 path。cookie 的 path 默认继承当前页面的目录,所以你在admin/users页面设置,cookie 就在admin/users目录生效。你要想让整个域名都生效,必须显式写path=/

中文和特殊字符的乱码

cookie 的标准规范里,value 本身允许的字符集是受限的。虽然浏览器实际实现时容忍度很高,但保守做法就是所有非 ASCII 字符全部编码。我的习惯是无论是 key 还是 value,写入前统一 encodeURIComponent,读取前统一 decodeURIComponent。封装里这么做了,生产环境基本不会因为编码问题出 bug。

有个细节:document.cookie.split('; ')之后,每个元素形如name=value,但你 value 里如果有=号(比如 JWT token、Base64 字符串),直接split('=')会把后面的=都丢掉。我封装里用了一个技巧:const [rawKey, ...rest] = cookieArr[i].split('='),然后把 rest 用join('=')拼回去。这个细节能省掉你排查半天“token 怎么少了后半截”的烦恼。

对象存取

cookie 里存字符串没问题,存复杂对象就需要Serialize。为什么直接存会变成[object Object]?因为对象隐式调用toString()时就是这个结果。正确的姿势是JSON.stringify(obj)存进去,JSON.parse取出来。但 fetch 回来的值不一定是 JSON 格式,所以读取时要做 try-catch,解析失败就当普通字符串返回。

补充一点实践心得:cookie 不要存大对象。单个 cookie 大小限制约 4KB,一个稍微复杂点的对象 JSON 化之后很容易超。而且 cookie 一发就是全量发到服务端,太大很浪费带宽,影响接口性能。复杂的、前端专用的数据交给 localStorage,cookie 里只放必要的状态标识。

4.3 调试 cookie 的实用技巧

遇到“cookie 莫名其妙”的问题,先别急着写 console.log 输出 document.cookie,而是打开 DevTools 的 Application 面板。Chrome 里路径是:DevTools -> Application -> Storage -> Cookies,点开左侧的域名,右侧能看到当前域名下所有 cookie 的完整信息,包括 Name、Value、Domain、Path、Expires、Size、HttpOnly、Secure、SameSite 每一列都清清楚楚。

这个面板能直接看到 cookie 的完整生命周期和生效范围,很多问题一眼就能定位。比如你发现页面上某个请求带了 cookie A,但 document.cookie 读不到 A——那基本可以断定这个 cookie 是 HttpOnly 的,前端脚本本来就无权访问,属于正常行为。

我自己排查 cookie 问题时的套路是:

  1. 先用 Application 面板确认 cookie 是否存在、属性和预期是否一致。
  2. 再在 Console 里执行 document.cookie 看能不能读到。如果面板里有但 console 读不到,那就是 HttpOnly。
  3. 检查请求头里的 Cookie 字段,看实际发送了什么。如果请求头里没有,那就是 path 或 domain 不匹配,或者 SameSite 限制。
  4. 最后一招是临时给 set 去掉所有高级属性,只留 name=value,看能不能读。能读就说明是某个属性配置问题,逐个加回去定位。

这套流程解决了我遇到的绝大多数 cookie 问题,效率比瞎猜高得多。

5. cookie 的安全边界与浏览器限制

5.1 HttpOnly、SameSite、Secure 怎么用

这几个属性是前端必须理解的,尤其做登录场景时绕不开。不说多深的攻防,但至少要明白它们分别防什么。

HttpOnly:标记了这个属性的 cookie 无法被 JavaScript 的 document.cookie 读取。它存在的核心目的是防止 XSS 攻击。如果攻击者在你页面里注入了一段恶意 JS,想偷走你的登录态,HttpOnly 会让这段 JS 读不到 cookie,偷了个寂寞。

需要提醒的是:HttpOnly 是服务端设置并通过响应头Set-Cookie下发的能力,前端 JS 通过 document.cookie 设置 cookie 是不能加 HttpOnly 的。因此在前后端分离的架构下,登录 token 往往有两种存放思路:一是服务端下发 HttpOnly cookie,前端无感,靠浏览器自动携带;二是前端拿到 token 自己存 cookie,但这样防御面就窄了一些。

Secure:只有 HTTPS 连接下才发送这个 cookie。你如果在本地 http 环境调试,默认 localhost 是例外,可以正常发,但部署到公网环境如果没有 HTTPS,Secure cookie 就永远发不到服务端。生产环境强烈建议给涉及登录态的 cookie 都加上 Secure。

SameSite:解决的是 CSRF 和第三方请求携带 cookie 的问题。三个取值:

  • Strict:任何跨站请求都不带 cookie。最安全,但用户体验有代价——比如从外站链接进入你的站点,初始请求不会带登录态,用户看起来像“未登录”一样。
  • Lax:现代浏览器的默认值。跨站的基础请求(比如用户自己点击链接跳转)会带 cookie,但跨站 POST、iframe 等场景不会带。
  • None:所有跨站请求都带 cookie,但前提是必须同时设置 Secure,否则浏览器拒绝。

实际业务中如果遇到“A 站 iframe 嵌入 B 站时,B 站的 cookie 不被发送”这种问题,大概率是 SameSite 默认限制所致。要放行,需要 B 站把 cookie 的 SameSite 设为 None,且 B 站必须跑在 HTTPS 下。

5.2 跨域与第三方 cookie 的现代约束

严格来说,cookie 天然受同源策略保护,但浏览器实现时做了一定的松绑:只要 domain 和 path 匹配,跨端口、跨协议也是允许的。比如http://localhost:3000http://localhost:8080在某种程度上可以共享同一个 cookie,只要 domain 相同。这既是便利,也是潜在风险来源,所以现代浏览器对手动设置 cookie 的 domain 有了更严格的约束——前端 JS 不能设置一个当前域名的父级域名对应的 cookie。例如你的页面在www.example.com,你无法通过 JS 把 cookie 设置成domain=example.com,因为这会扩大 cookie 的影响范围,浏览器直接拒绝。

服务端通过 Set-Cookie 响应头设置时限制相对宽松一些,可以设置为当前域名的父级域名。这也解释了为什么 HTTP-only 的跨子域 cookie 通常是由服务端而不是前端处理的。

另外,第三方 cookie 正在被浏览器逐步淘汰。所谓第三方 cookie,就是在访问 A 站时,B 站域名下的 cookie。典型场景是广告追踪。Safari 的 ITP(Intelligent Tracking Prevention)机制早就对第三方 cookie 做了严格限制,Chrome 也在推进相关策略。对于做前端的开发者,这意味着:不要依赖第三方 cookie 做核心业务逻辑。如果你在多个站点之间需要共享用户状态,更靠谱的方案是自己实现一套基于 token 的身份认证,而不是依赖浏览器对第三方 cookie 的放行。

还有个容易被忽略的坑:cookie 的 domain 和 path 匹配,跟端口没有关系,但一旦设置了端口相关的内容(比如 localhost 携带端口号),就可能导致奇怪的兼容问题。开发环境建议让后端把 cookie 的 domain 设置为localhost而不是127.0.0.1,两者在部分浏览器规则下有区别。

结尾

cookie 本身是个几十年的老技术了,但偏偏是这个“老”字,让它积累了不少反直觉的细节。我在项目里踩过的坑,从 path 丢失到中文乱码,从删除不掉到 SameSite 拦截,每一个都不是大问题,但每一个都能让人排查一个下午。这篇博客里的封装和排查思路,是我在多个真实项目中反复验证过的,直接拿去用没问题。最后一个小提醒:涉及登录态和用户身份的 cookie,一定优先交给服务端用 HttpOnly + Secure + SameSite 来控制,前端只管挖坑埋坑,安全边界这堵墙得由服务端来砌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询