存储 API

Chrome 插件开发指南 · local、sync、session 与配额

作者 David更新于

chrome.storage 是扩展唯一靠谱的持久化手段。它异步、跨上下文、直接存对象——并且是 Service Worker 里唯一可用的存储。

为什么不能用 localStorage

问题说明
Service Worker 里根本没有localStorage 是同步 API,Worker 环境不提供
每个上下文一份Content Script 的 localStorage 属于所在网页,不是你的扩展
只能存字符串每次都要 JSON.parse / stringify
用户清缓存会清掉「清除浏览数据」会连带删除

结论:扩展里不要用 localStorage。 唯一的例外是纯前端的临时 UI 状态(比如选项页记住当前 tab),且你不在乎它丢。

四个存储区

chrome.storage.local     // ~10 MB,本机持久
chrome.storage.sync      // ~100 KB,跨设备同步
chrome.storage.session   // 内存,浏览器关闭即失效
chrome.storage.managed   // 只读,企业策略下发
区容量生命周期用途
local约 10 MB(可申请无限)卸载才清缓存、大列表、本机数据
sync约 100 KB / 单项 8 KB卸载才清,跨设备用户配置
session约 10 MB浏览器关闭清空临时态、敏感数据
managed—只读企业部署的策略

基本用法

// 写
await chrome.storage.local.set({ todos: [...], lastSync: Date.now() });

// 读(带默认值)
const { todos = [] } = await chrome.storage.local.get('todos');

// 读多个
const { a, b } = await chrome.storage.local.get(['a', 'b']);

// 读全部
const all = await chrome.storage.local.get(null);

// 删
await chrome.storage.local.remove('todos');
await chrome.storage.local.clear();

get 的默认值写在解构里,是最简洁的惯用法。

const { count = 0 } = await chrome.storage.local.get('count');

也可以传对象形式:get({ count: 0 })——效果相同,但解构写法更少一层嵌套。

sync 的配额是真的会撞上

QUOTA_BYTES              102400   总共约 100 KB
QUOTA_BYTES_PER_ITEM       8192   单项 8 KB
MAX_ITEMS                   512   最多 512 个键
MAX_WRITE_OPERATIONS_PER_HOUR      1800
MAX_WRITE_OPERATIONS_PER_MINUTE     120

写入频率限制比容量限制更容易撞上。 每分钟 120 次听着很多,但如果你在输入框的 oninput 里直接写 sync,用户打一段字就超了。

超限时 set 会 reject,错误信息是 MAX_WRITE_OPERATIONS_PER_MINUTE quota exceeded。

解法是防抖:

let timer;
function saveDebounced(data) {
  clearTimeout(timer);
  timer = setTimeout(() => chrome.storage.sync.set(data), 500);
}

注意在 popup 里做防抖有风险——popup 关闭时定时器一起没了。popup 里改成「失焦即存 + 关闭前存」更稳妥。

检查用量:

const used = await chrome.storage.sync.getBytesInUse(null);
console.log(`${used} / ${chrome.storage.sync.QUOTA_BYTES}`);

local 要更多空间

{ "permissions": ["storage", "unlimitedStorage"] }

加上 unlimitedStorage 后 local 不再受 10 MB 限制。但要克制——这个权限会出现在安装提示里,而且真存几百 MB 会拖慢用户的浏览器配置文件。

session:放敏感数据的地方

await chrome.storage.session.set({ accessToken: token });

三个特点:

  1. 存内存,浏览器一关就没——适合 token、临时解密的数据。
  2. 默认不暴露给 content script,比 local 安全一层:
// 需要让 CS 也能读时才开
chrome.storage.session.setAccessLevel({ accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS' });
  1. 它能跨 SW 回收存活——这点很关键:SW 被回收后全局变量清零,但 session 还在。

chrome.storage.session 是 SW 全局变量的正确替代品。

需要在 SW 里缓存一点运行时状态(比如「当前是否正在同步中」),既不想写磁盘、又要扛住 SW 回收——session 正是为此设计的。

onChanged:跨上下文同步

chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName !== 'local') return;
  for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
    console.log(key, oldValue, '→', newValue);
  }
});

这是扩展里最好用的状态同步机制——比发消息简单得多:

popup 改配置  →  storage.set  →  onChanged 广播
                                    ├──► background 更新角标
                                    ├──► content script 重新渲染
                                    └──► options 页同步 UI

不需要知道有谁在听,也不用担心对方是否在线。实战见第 5 课。

注意 onChanged 不会在自己 set 的那个上下文里被跳过——你写入后自己也会收到通知。要避免循环,比较新旧值:

chrome.storage.onChanged.addListener((changes) => {
  if (!changes.config) return;
  const { oldValue, newValue } = changes.config;
  if (JSON.stringify(oldValue) === JSON.stringify(newValue)) return;
  applyConfig(newValue);
});

并发写的坑

// ✗ 有竞态:两个上下文同时执行,会丢一次
const { count = 0 } = await chrome.storage.local.get('count');
await chrome.storage.local.set({ count: count + 1 });

chrome.storage 没有事务、没有原子自增。读-改-写之间,别的上下文可能插进来。

三种缓解:

做法适用
把计数逻辑集中到 SW,其他上下文发消息请求最可靠
用队列串行化本上下文的写入单上下文内的并发
拆键,避免多方写同一个键结构允许时最简单

别把所有数据塞进一个键。 很多人写 chrome.storage.local.set({ appState: {...整个应用状态} })——这会让任何一处改动都要读写整个对象,既慢又极易丢更新。

按更新频率拆键:配置一个键、缓存一个键、计数一个键。

数据迁移

版本升级时改了数据结构,要在 onInstalled 里迁移:

chrome.runtime.onInstalled.addListener(async ({ reason, previousVersion }) => {
  if (reason !== 'update') return;

  const { schemaVersion = 0 } = await chrome.storage.local.get('schemaVersion');

  if (schemaVersion < 1) {
    const { todos = [] } = await chrome.storage.local.get('todos');
    await chrome.storage.local.set({
      todos: todos.map((t) => ({ ...t, createdAt: t.createdAt ?? Date.now() })),
      schemaVersion: 1,
    });
  }
});

用独立的 schemaVersion 而不是扩展版本号——扩展版本会因为无关改动频繁变,数据结构版本才是你真正要判断的东西。

选型速查

用户配置、要跨设备         → sync(小心 8 KB 单项限制)
缓存、大列表、本机数据      → local
token、临时态、SW 全局变量  → session
企业策略                   → managed(只读)

小结

  • 扩展里不用 localStorage——SW 里根本没有。
  • sync 的写入频率限制(120 次/分钟)比容量更容易撞,输入类操作务必防抖。
  • session 存内存、浏览器关闭即清,是放 token 和替代 SW 全局变量的正确位置。
  • onChanged 是最省事的跨上下文同步机制,但自己写入也会收到通知。
  • 没有事务,读-改-写有竞态;按更新频率拆键,计数逻辑集中到 SW。
  • 用独立的 schemaVersion 做数据迁移。

下一篇:网络与 CORS。

本页目录