存储方案:chrome.storage 与 IndexedDB 的选型

扩展有多个运行上下文,又要在 Service Worker 随时休眠的前提下保住状态,存储选型因此成为架构决策。候选主要有三类:chrome.storage、IndexedDB、(不推荐的)Web Storage。这一篇给出各区域的能力边界与选型规则。

chrome.storage 的四个区域

区域 容量 生命周期 特点
storage.local 10 MB(Chrome 113 及更早为 5 MB) 持久,卸载扩展才清除 加 unlimitedStorage 可扩容
storage.sync 约 100 KB,单项 8 KB 持久并跨设备同步 跟随 Chrome 账号同步
storage.session 10 MB(Chrome 111 及更早为 1 MB) 内存级,浏览器重启或扩展重载即清 默认不暴露给 content script
storage.managed 由策略决定 只读 企业管理员统一下发配置

几个容易被忽略的行为差异:session 区默认 content script 读不到,需要显式调 setAccessLevel 打开;sync 区在用户关闭同步时退化为 local 行为;清除浏览记录不影响任何 storage 区域的数据——这与 localStorage 的命运完全不同。

基础用法是 Promise 风格的 get/set:

await chrome.storage.local.set({ theme: "dark", fontSize: 15 });
const { theme } = await chrome.storage.local.get("theme");

// 监听任意区域的变化
chrome.storage.onChanged.addListener((changes, area) => {
  if (area === "sync" && changes.theme) {
    applyTheme(changes.theme.newValue);
  }
});

onChanged 是实现「设置页一改、所有上下文即时生效」的标准做法,比广播消息省事。

IndexedDB:大数据量的正解

chrome.storage 的强项是小而结构化的键值;当你要存图片 blob、离线缓存的数据集、几万条记录时,IndexedDB 才是对的工具。它是事务型数据库,容量远超 storage.local,扩展的所有上下文(含 Service Worker)都能用。

const db = await new Promise((resolve, reject) => {
  const req = indexedDB.open("cache", 1);
  req.onupgradeneeded = () => req.result.createObjectStore("pages");
  req.onsuccess = () => resolve(req.result);
  req.onerror = () => reject(req.error);
});

function put(key, value) {
  return new Promise((resolve, reject) => {
    const tx = db.transaction("pages", "readwrite");
    tx.objectStore("pages").put(value, key);
    tx.oncomplete = resolve;
    tx.onerror = () => reject(tx.error);
  });
}

原生 API 偏啰嗦,社区有 idb 这类轻量封装,引入方式随你的构建工具而定。注意 MV3 禁止远程代码,封装库必须打进扩展包内。

为什么不推荐 Web Storage

官方文档明确不建议用 localStorage:Service Worker 里根本没有 Web Storage API;content script 的 localStorage 与宿主页面共享,存扩展数据等于把自己的数据摊在页面面前;用户清浏览记录就全没了。popup 等扩展页面虽然能用,但为了一致性也应统一走 chrome.storage。

选型决策表

按数据形态决策,基本一击即中:

  1. 用户设置、开关、小状态 → storage.sync(要跨设备)或 storage.local;
  2. Service Worker 的会话内缓存 → storage.session;
  3. 大文件、结构化数据集、离线缓存 → IndexedDB;
  4. 企业批量下发配置 → storage.managed;
  5. 服务端为准的配置 → 扩展内 fetch 拉取后落到 local。

Service Worker 视角再补一条:worker 随时休眠,任何「先读缓存再干活」的逻辑都要把异步读取做在事件处理内部(或启动时预载),不能依赖全局变量持有旧值——这一点在 《Service Worker》 篇强调过。

存储与权限的联动只有一条:用 chrome.storage 本身需要 storage 权限;扩容 local 需要 unlimitedStorage。官方文档:https://developer.chrome.com/docs/extensions/reference/api/storage 。数据里如果出现用户文本、正则模式等字符串,处理技巧见 正则实战。

存储的并发与一致性

多上下文同时写是扩展的常态:popup 改设置的同时后台在记数。chrome.storage 的每次 set 都是原子键级写入,不会写坏数据,但「读-改-写」序列在并发下会互相覆盖:

// 有竞态风险的写法
const { count = 0 } = await chrome.storage.local.get("count");
await chrome.storage.local.set({ count: count + 1 }); // 两处同时跑会丢一次计数

// 缓解办法:把计数职责收到单一上下文(如后台),其他上下文发消息

设计上的解法比代码技巧更可靠:为每类数据指定唯一「写者」(后台负责计数、设置页负责配置),其余上下文只读或通过消息请求写。onChanged 负责把变更广播给所有读者。把存储当数据库做「单一写者」设计,一致性自然成立。

迁移与清理

数据结构会演进,两个标准动作。第一,版本迁移挂在 runtime.onInstalled 的 update 分支里做(见 《Service Worker》 篇),迁移函数幂等、按 previousVersion 判断;第二,卸载清理:chrome.storage 数据在扩展卸载时由浏览器自动删除,无需你处理——这也是不建议把扩展数据写到页面 localStorage 的又一个理由(那个真删不掉)。

读取时的兜底也值得写进习惯:storage.get 拿到的值永远是「可能缺失、可能类型不对」的外部输入,关键字段做默认值与类型校验,别直接 .value 一路点下去。

存储选型的速查卡

最后把三篇涉及的存储知识压成一张速查卡,写代码时对着查:

数据 首选 理由
设置项、偏好 storage.sync 跨设备,量小
历史记录、缓存 storage.local 容量大,本地即可
worker 会话内状态 storage.session 浏览器重启即清,语义匹配
图片 blob、大数据集 IndexedDB 容量与结构化查询
企业统一下发配置 storage.managed 只读,管理员控制
标签页级临时数据 storage.session 按标签键名 或干脆放内存

存储方案的全景对比即本篇内容,chrome.storage 的代码层落地在《chrome.storage 实战》一篇,权限侧只有 storage 与 unlimitedStorage 两项,见 《权限模型》 篇。官方文档:https://developer.chrome.com/docs/extensions/reference/api/storage

存 URL、模式串、用户文本时,读出后的清洗与匹配就是正则的主场,工具箱见 正则实战。

本章由 WorkBuddy 原创整理,API 行为依据 Chrome 扩展官方文档(Manifest V3)。