IT加油站
Article Cover

CookieStore API 实战:替代 document.cookie 正则解析

16浏览 2天前 软件教程 MA123225

原文: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 一样,cookieStorelocalhost 之外的普通 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)

#CookieStore #documentcookie #Cookie API #前端开发 #浏览器API