JavaScript高级程序设计笔记 25

第 25 章 客户端存储

学习目标:理解浏览器端存储方案的演进,掌握 cookie、Web Storage、IndexedDB 的特点、限制和适用场景。

1. 本章核心脉络

客户端存储解决的是“数据如何保存在用户浏览器中”的问题。早期主要依赖 cookie,后来出现了更适合前端应用的 sessionStoragelocalStorage,再往后则有可存储大量结构化数据的 IndexedDB

本章重点:

  • cookie:最早的客户端存储机制,会随 HTTP 请求发送。
  • Web Storage:包含 sessionStoragelocalStorage,API 简单,适合键值存储。
  • IndexedDB:浏览器内置的事务型数据库,适合大量结构化数据。

cookie 是保存在浏览器中的小块文本数据,最初用于在无状态 HTTP 协议中维护状态,比如登录态、用户偏好、追踪标识等。

特点:

  • 按域名存储。
  • 每次匹配请求都会自动带到服务器。
  • 容量小。
  • 字符串格式,不适合复杂数据。
  • 涉及隐私和安全问题。

常见限制:

  • 单个 cookie 大小通常约 4KB。
  • 每个域名可保存 cookie 数量有限。
  • cookie 会随请求自动发送,过多会增加网络开销。
  • 不能存储敏感信息,除非有可靠加密和服务端保护。

常见属性:

属性 作用
name=value cookie 名和值
expires 过期时间
max-age 最大存活秒数
domain 可访问 cookie 的域
path 可访问 cookie 的路径
secure 只在 HTTPS 下发送
HttpOnly 禁止 JavaScript 读取
SameSite 限制跨站请求携带 cookie

示例:

Set-Cookie: token=abc; Path=/; Secure; HttpOnly; SameSite=Lax

读取:

console.log(document.cookie);

写入:

document.cookie = "name=Alice; path=/; max-age=3600";

删除本质上是让它过期:

document.cookie = "name=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT";

注意:document.cookie 的接口很原始,读取时返回的是一个分号分隔的字符串,实际项目通常会封装工具函数。

子 cookie 是一种把多个键值编码进同一个 cookie 的技巧,用来绕开 cookie 数量限制。

示例形式:

data=name=Alice&theme=dark&lang=zh

缺点:

  • 需要手动解析和序列化。
  • 任意子项变更都要重写整个 cookie。
  • 可维护性较差。

现代项目通常不再优先使用这种方式。

  • 不要保存密码、令牌明文等敏感数据。
  • 能用 HttpOnly 的认证 cookie 应尽量由服务端设置。
  • HTTPS 场景下配合 Secure
  • 跨站安全场景下关注 SameSite
  • 不要把大量业务数据放进 cookie。
  • 前端本地状态优先考虑 Web Storage 或 IndexedDB。

3. Web Storage

3.1 Storage 类型

Storage 提供键值对存储能力,键和值都会以字符串形式保存。

常用 API:

localStorage.setItem("theme", "dark");
localStorage.getItem("theme");
localStorage.removeItem("theme");
localStorage.clear();

也可以使用属性形式:

localStorage.theme = "dark";
console.log(localStorage.theme);

更推荐使用 getItem()setItem(),语义更清晰。

3.2 sessionStorage

sessionStorage 的生命周期是页面会话。

特点:

  • 数据只在当前标签页或窗口中有效。
  • 页面刷新后仍存在。
  • 关闭标签页后清除。
  • 不会自动发送给服务器。

适合:

  • 多步骤表单临时数据。
  • 当前页面的临时筛选条件。
  • 单标签页内的短期状态。
sessionStorage.setItem("draft", JSON.stringify({ title: "hello" }));

const draft = JSON.parse(sessionStorage.getItem("draft") || "{}");

3.3 localStorage

localStorage 的生命周期更长,除非手动清除,否则会长期保留。

适合:

  • 用户主题偏好。
  • 本地草稿。
  • 非敏感配置。
  • 小规模缓存。
localStorage.setItem("settings", JSON.stringify({
  theme: "dark",
  fontSize: 16,
}));

注意:

  • 只能存字符串,复杂对象要 JSON.stringify()
  • 读取和写入是同步 API,频繁操作大数据可能阻塞主线程。
  • 不适合保存敏感信息。

3.4 storage 事件

当同源页面中的 Web Storage 被其他页面修改时,会触发 storage 事件。

window.addEventListener("storage", (event) => {
  console.log(event.key);
  console.log(event.oldValue);
  console.log(event.newValue);
  console.log(event.url);
});

常见用途:

  • 多标签页同步登录状态。
  • 多窗口同步主题设置。
  • 通知其他标签页缓存变化。

注意:通常修改发生的当前页面不会收到自己触发的 storage 事件。

3.5 Web Storage 限制

  • 存储容量比 cookie 大,但仍有限。
  • API 同步,不能滥用大数据读写。
  • 用户或浏览器策略可能清理数据。
  • 隐私模式下行为可能不同。
  • 只能按字符串键值存储。

4. IndexedDB

4.1 IndexedDB 是什么

IndexedDB 是浏览器提供的低级客户端数据库,适合保存大量结构化数据。它是异步 API,支持事务、对象仓库、索引和游标。

适合:

  • 离线应用数据。
  • 大量列表缓存。
  • 文件、Blob、结构化对象。
  • 本地搜索和复杂查询。

不适合:

  • 简单偏好设置。
  • 需要服务端强一致性的核心数据。

4.2 打开数据库

const request = indexedDB.open("app", 1);

request.onupgradeneeded = (event) => {
  const db = event.target.result;
  db.createObjectStore("users", { keyPath: "id" });
};

request.onsuccess = (event) => {
  const db = event.target.result;
  console.log(db);
};

request.onerror = () => {
  console.error(request.error);
};

关键点:

  • indexedDB.open(name, version) 打开数据库。
  • 首次创建或版本升级时触发 upgradeneeded
  • 对象仓库和索引通常在升级阶段创建。

4.3 对象存储

对象仓库类似表,用来保存对象。

db.createObjectStore("users", {
  keyPath: "id",
});

主键可以是:

  • 对象中的某个字段,如 id
  • 自动生成的键,如 { autoIncrement: true }
  • 插入时手动传入的外部键。

4.4 事务

IndexedDB 的所有读写都在事务中完成。

const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");

store.add({ id: 1, name: "Alice" });

tx.oncomplete = () => {
  console.log("done");
};

事务模式:

  • readonly:只读。
  • readwrite:读写。

事务可以保证一组操作要么完成,要么失败。

4.5 插入、读取、更新、删除

const store = db.transaction("users", "readwrite").objectStore("users");

store.add({ id: 1, name: "Alice" });
store.put({ id: 1, name: "Alice Updated" });
store.get(1);
store.delete(1);

区别:

  • add():新增,键已存在会失败。
  • put():新增或更新。

4.6 游标查询

游标用于遍历对象仓库或索引。

const request = store.openCursor();

request.onsuccess = (event) => {
  const cursor = event.target.result;

  if (cursor) {
    console.log(cursor.key, cursor.value);
    cursor.continue();
  }
};

适合:

  • 遍历大量数据。
  • 分页。
  • 条件过滤。
  • 范围扫描。

4.7 键范围

IDBKeyRange 用于限定查询范围。

const range = IDBKeyRange.bound(10, 20);
store.openCursor(range);

常用方法:

  • only(value):只匹配指定值。
  • lowerBound(value):下界。
  • upperBound(value):上界。
  • bound(lower, upper):上下界。

4.8 索引

索引用于按非主键字段查询。

store.createIndex("by_email", "email", {
  unique: true,
});

查询:

const index = store.index("by_email");
index.get("a@example.com");

索引能提升查询能力,但会增加写入维护成本。

4.9 并发与版本问题

IndexedDB 有版本概念。升级数据库结构时,旧连接可能阻塞新版本打开。

实践建议:

  • 数据库连接不用时及时关闭。
  • 处理 versionchange 事件。
  • 升级逻辑保持可重复、可迁移。
  • 结构变更集中放在 onupgradeneeded

5. 方案选择

场景 推荐方案
服务端识别登录态 cookie
少量用户偏好 localStorage
当前标签页临时数据 sessionStorage
页面关闭时清除的数据 sessionStorage
大量结构化缓存 IndexedDB
离线应用数据 IndexedDB
需要随请求自动发送 cookie

6. 面试高频问题

6.1 cookie、localStorage、sessionStorage 的区别?

cookie 会随 HTTP 请求自动发送,容量小,常用于服务端会话。localStorage 长期保存,不随请求发送。sessionStorage 只在当前页面会话中保存,关闭标签页后清除。

6.2 localStorage 能存对象吗?

不能直接存对象。Web Storage 只存字符串,对象需要先 JSON.stringify(),读取后再 JSON.parse()

6.3 为什么不建议把 token 放 localStorage?

因为 localStorage 可以被 JavaScript 读取,一旦页面存在 XSS 漏洞,攻击者可能直接窃取 token。更安全的认证方案通常会结合服务端设置的 HttpOnlySecureSameSite cookie。

6.4 IndexedDB 适合解决什么问题?

适合存储大量结构化数据和离线数据,支持异步操作、事务、索引、游标,比 Web Storage 更适合复杂客户端缓存。

6.5 storage 事件有什么用?

它可以让同源的多个标签页感知 Web Storage 的变化,常用于多标签页同步状态,比如退出登录、主题变更等。

7. 复习清单

8. 一句话总结

第 25 章的核心是客户端存储方案的分层选择:cookie 负责与服务端请求状态绑定,Web Storage 负责简单键值状态,IndexedDB 负责大量结构化和离线数据。

posted @ 2024-05-16 10:19  Li_pk  阅读(5)  评论(0)    收藏  举报