存储方案: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。
选型决策表
按数据形态决策,基本一击即中:
- 用户设置、开关、小状态 → storage.sync(要跨设备)或 storage.local;
- Service Worker 的会话内缓存 → storage.session;
- 大文件、结构化数据集、离线缓存 → IndexedDB;
- 企业批量下发配置 → storage.managed;
- 服务端为准的配置 → 扩展内 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)。
