CookieStore API 实战:替代 document.cookie 正则解析
原文:https://dev.to/parsajiravand/stop-regex-parsing-documentcookie-use-cookiestore-5c41(作者 @parsajiravand)
打开任何一个有 cookie 的老代码库,搜索 document.cookie。你会找到一个类似这样的函数,写它的人早就离职了:
function getCookie(name) {
const match = document.cookie.match(
new RegExp("(^| )" + name + "=([^;]+)")
);
return match ? decodeURIComponent(match[2]) : null;
}
它能用。从 2011 年起就能用。但它也有一份众所周知的踩坑清单——cookie 名互为前缀、值中包含未转义的 = 或 ;、分号后的空格取决于哪个浏览器写入了 header。每个人都至少踩过其中一个坑。没人去重写这个函数,因为它今天还没坏。
正则根本无法解决的一个更棘手的问题是:你无法知道 cookie 何时发生变化。 不是从另一个标签页。不是从 fetch() 响应的 Set-Cookie header。甚至不是从你自己页面上另一个脚本在你之后紧接着调用 document.cookie = ...。document.cookie 是一个普通的字符串属性。读取它告诉你当前状态。它从未告诉你状态何时发生了变化。
大家都在用什么替代方案
一旦出现"我需要对 cookie 变化做出响应"的需求——API 调用设置了一个登录 cookie、另一个标签页刚关掉了同意横幅——常见的解决方案是:
- 定时轮询 `document.cookie`。 它确实能用,从某种意义上说,每 500ms 检查一次字符串的
setInterval最终会发现变化。但这意味着每个用户的每个标签页都在永远地 diff 一个字符串,就为了一个可能整个会话只发生一次的事件。 - 让服务器通过 WebSocket 或 SSE 告诉客户端。 为一个纯粹本地的问题搭建真实的基础设施——cookie 已经在这台机器、这个浏览器上变了,你只是没有对应的钩子。
- 在每个写入 cookie 的地方都包一层自己的发布/订阅。 这能行,直到某个第三方脚本、某个
Set-Cookie响应 header,或者浏览器自身的 cookie 过期逻辑改了一个你的发布/订阅不知道的 cookie。
这三种方案都把"浏览器不告诉我"当作需要工程化绕过的问题。值得问的是,浏览器为什么一开始不告诉你——结果是,最近它已经可以了。
真正能做到这一点的 API:cookieStore
Chrome 和 Edge 内置了 window.cookieStore(在 service worker 中是 self.cookieStore)——一个基于 Promise 的 Cookie Store API,将 cookie 视为结构化对象,而不是一个需要你手动序列化的字符串。
读取不再需要正则:
const session = await cookieStore.get("session_id");
// { name: "session_id", value: "abc123", domain: null, path: "/", ... } or null
const all = await cookieStore.getAll();
// array of every cookie visible to this document, already parsed
写入使用一个对象,而不是手工拼接的 key=value; path=...; expires=... 字符串:
await cookieStore.set({
name: "theme",
value: "dark",
expires: Date.now() + 1000 * 60 * 60 * 24 * 30, // 30 days, in ms
path: "/",
});
await cookieStore.delete("theme");
以及 document.cookie 永远做不到的部分——真正的事件:
cookieStore.addEventListener("change", (event) => {
for (const cookie of event.changed) {
console.log("set:", cookie.name, cookie.value);
}
for (const cookie of event.deleted) {
console.log("deleted:", cookie.name);
}
});
这个监听器会对页面设置的 cookie、fetch() 响应通过 Set-Cookie 设置的 cookie,以及因过期被移除的 cookie 触发——无需轮询,无需自己写发布/订阅。一个标签页中关掉的同意横幅,现在可以在发生的瞬间更新同源下所有其他打开的标签页。
<!-- playground:start -->
动手试试
[打开交互式演示页面 →](https://bestpractic.org/blog/cookie-store-api-async-cookies/playground)
_直接在浏览器中运行——动手试试,实时观察概念的反应。_
<!-- playground:end -->
比你习惯的更严格的默认值
这才是真正让迁移现有代码的人踩坑的地方,而且这不是 bug——这是规范的有意设计,读起来像个脚注,直到它搞坏了什么。
当你用旧方式写 cookie 时,通过 Set-Cookie 头或 document.cookie,如果你不指定 SameSite,浏览器默认将其设为 Lax。这一行为已经持续多年——这就是为什么当用户从其他地方点击普通链接进入你的站点时,你站点上设置的 cookie 仍然会随请求发送,但在跨站 POST 请求中不会发送。
cookieStore.set() 不继承这个默认值。根据规范,如果你不显式传递 sameSite,它默认为 `"strict"`——比 document.cookie 免费给你的更严格。Strict cookie 在任何跨站导航中都不会发送,包括顶级链接点击。
所以失败模式是这样的:你把一行设置 cookie 的代码从 document.cookie = "..." 迁移到 cookieStore.set({...}),运行测试套件,然后发布。你自己站点内部发生的一切都继续正常工作,因为同站请求根本不关心 SameSite。几周后,有人从邮件或合作方站点点击链接进入你的站点,落在一个期望那个 cookie 已经存在的页面上,但它不在。没有错误。没有控制台警告。你设置的 cookie 只是没有附加到那个请求上,因为 Strict 说了不要。
修复方法就是一个关键词,一旦你知道该找什么:
await cookieStore.set({
name: "session_id",
value: token,
sameSite: "lax", // 匹配 document.cookie 本来会给你的行为
});
使用前值得了解的两件事
- 目前仅 Chromium 支持。 Chrome 和 Edge 支持
cookieStore;截至本文撰写时,Firefox 和 Safari 尚未提供支持。在你依赖它做任何没有特性检测包裹的事情之前,先查看 caniuse 上的最新数据——用if ("cookieStore" in window)检测,并以document.cookie作为回退。 - 它需要安全上下文。 和大多数更新的、功能更强的浏览器 API 一样,
cookieStore在localhost之外的普通http://源上根本不存在。如果在生产环境中它是undefined,但在本地测试时存在,那几乎可以肯定就是这个原因。
要点总结
document.cookie 从来就不是为解析而设计的——它是一个字符串接口,硬接在一个比 JSON.parse 还古老的功能上。cookieStore 将 cookie 视为它们本应是的那种结构化、可 await、可观察的数据,仅 change 事件本身就值得为那些需要响应自身代码之外设置的 cookie 的场景进行迁移。只是别让 sameSite 默认值悄悄变成比你依赖的行为更严格的值。
你的代码库里还有手写的 cookie 解析器吗?它有多老了,还有人记得是谁写的吗?
原文:https://dev.to/parsajiravand/stop-regex-parsing-documentcookie-use-cookiestore-5c41(作者 @parsajiravand)
