Chrome Extension 里缓存已观察数据该放哪里
最近给一个 Jenkins 页面写 Chrome Extension,最开始只是想把构建历史表格整理得清楚一点:谁触发、什么时候触发、用了什么参数、关联了哪些 Git 变更。
做到后面才发现,Jenkins 本身只保留当前可见窗口里的少量构建。页面上能看到最近 7 条,新的构建一出现,最老那条就掉出窗口。旧构建 URL 也不一定还能打开。扩展当然不能恢复 Jenkins 已经清理掉的数据,但它至少可以记住自己已经看过的构建。
于是问题变成了:这份“已观察构建记录”应该存在哪里?
候选项很多:页面 localStorage、chrome.storage.local、IndexedDB、storage.session、内存变量,甚至还有 storage.sync、Cache Storage、OPFS 这类容易被顺手想到的选项。它们都能存东西,但归属、可见性、生命周期和查询能力完全不一样。
这篇记录最后的判断过程。当前结论是:这个 Jenkins enhancer 仍然选 chrome.storage.local,但要把缓存可见化,不能让用户觉得它是黑盒。
先看这份数据属于谁
这不是 Jenkins 原生业务数据。Jenkins 只展示当前窗口里的构建,扩展只是把自己在页面上读到的结果留一份本地快照。它更像“扩展观察日志”,不是“Jenkins 页面状态”。
这个归属判断会直接影响存储选择:
- 如果数据属于页面业务本身,页面
localStorage很自然。 - 如果数据属于扩展能力本身,
chrome.storage.local更自然。 - 如果数据已经变成大量结构化历史,需要索引、分页和复杂查询,
IndexedDB才开始有明显优势。
Chrome 官方 chrome.storage 文档把它描述成 extension-specific 的持久化方式,并且明确 content scripts 默认可以访问。官方同一页也提醒,content script 里用 Web Storage 会共享宿主页存储。这一点在这个场景里很关键:content script 直接写 window.localStorage,写进去的是 Jenkins 这个站点的 origin,而不是扩展自己的 origin。
页面 localStorage 的优势是真的
一开始我对页面 localStorage 有点偏见,觉得它像是在“污染页面”。但这个理由在当前场景里并不够扎实。这个扩展本来就只服务 Jenkins 的两个站点,写一份带前缀的缓存 key,并不会立刻造成工程灾难。
而且页面 localStorage 有一个很真实的优势:透明。
用户打开 Jenkins 页面的 DevTools,在 Application 面板里就能看到 localStorage。如果缓存里出现了奇怪数据,用户可以自己确认,也可以自己删掉。这种可见性对内部工具很重要。尤其这个缓存是为了弥补 Jenkins UI 的缺口,如果它自己又变成一个不可见的小黑盒,体验上会打折。
MDN 的 localStorage 文档说明它按 document origin 存储,且跨浏览器会话保留。放到 Jenkins enhancer 里,这意味着:
- 测试环境和正式环境会天然分开,因为它们是不同 origin。
- 数据跟 Jenkins 站点数据在一起,用户清理站点数据时会一起清掉。
- Jenkins 页面自己的脚本也能看到这些 key。
- 值只能是字符串,复杂对象需要自己
JSON.stringify()和JSON.parse()。 - API 是同步的,少量数据问题不大,大量读写会阻塞主线程。
所以 localStorage 不是不能用。它适合这种判断:缓存只服务某个具体站点,用户调试透明度优先,数据量很小,不需要跨站点或跨扩展页面共享。
但这次我还是没有把它作为主路径。原因不是“它不安全到不能碰”,而是这份数据的归属更像扩展,后续也可能和 Dashboard 收藏、最近访问、缓存清理入口放在同一套扩展设置里。
chrome.storage.local 更像扩展自己的缓存
chrome.storage.local 的优势在于归属清楚。它绑定扩展 ID,而不是 Jenkins 页面 origin;content script、popup、options page、service worker 都可以围绕同一份扩展数据工作。官方文档还列了几个对扩展很友好的特性:异步读写、JSON 可序列化对象、onChanged 监听、getBytesInUse() 统计空间、setAccessLevel() 控制 content script 访问级别。
生命周期也更符合“插件缓存”:storage.local 存在本机,扩展更新、reload、Chrome 重启都不应该清掉;扩展卸载时会清掉。官方文档写到,storage.local 默认上限是 10 MB,Chrome 113 及以前是 5 MB,需要更大空间时可以申请 unlimitedStorage。
这里有一个本地开发边界:chrome.storage.local 跟扩展 ID 绑定。如果换目录重新 Load unpacked,或者没有稳定的开发 ID,新扩展可能读不到旧扩展的数据。Chrome manifest key 文档专门解释了如何在开发阶段保持一致的 extension ID。真正要长期分发时,这件事应该提前定好。
透明度这点以前确实弱一些。现在 Chrome 已经补上了入口:Chrome DevTools 从 Chrome 132 开始支持查看和修改 extension storage。在页面 DevTools 的 Application 面板里,可以展开 Extension Storage,查看运行在当前页面上的扩展存储。这个入口仍然不如页面 localStorage 直觉,但已经不是完全看不到。
这个场景里,当前实现简化后大概是这样的:
const createCacheKey = (context: JenkinsPageContext) => {
const url = new URL(context.href);
return `${CACHE_KEY_PREFIX}:${context.environment}:${url.origin}:${createJobCacheScope(context.href)}`;
};
const saveObservedBuildRows = async (context: JenkinsPageContext, rows: BuildHistoryRow[]) => {
const key = createCacheKey(context);
const cachedRows = await loadCachedBuildRows(context);
const nextRows = mergeAndLimit(rows, cachedRows);
await chrome.storage.local.set({ [key]: nextRows });
return nextRows;
};缓存 key 里有三层含义:
environment:测试环境和正式环境隔离。origin:不同 Jenkins 站点隔离。job scope:同一个 job 从直达 URL 或 view 列表进入时共享缓存。
这比页面 localStorage 多了一点实现成本,但也换来一个好处:隔离和共享都由扩展主动定义,而不是完全交给页面 origin。
IndexedDB 是数据库,不是默认缓存
如果只看 Web 平台能力,web.dev 的 Storage for the web会给出很明确的推荐:资源用 Cache Storage,文件内容用 OPFS,其他数据优先 IndexedDB。MDN 的存储配额文档也把 IndexedDB 放在“大型结构化数据、可索引查询”的位置。
这套建议放在普通 Web App 里很合理。IndexedDB 支持更大的数据量、事务、object store、索引和结构化克隆。存几万条记录、按时间范围查、按 job / branch / appName 建索引,IndexedDB 会比把一个大数组塞进 chrome.storage.local 更合适。
但在 Chrome Extension content script 里,还要多看一层 context。Chrome 的 Storage and cookies 文档说明,扩展自己的存储会在 extension origin 下共享;content script 里调用 Web 平台存储 API 时访问的是宿主页数据。IndexedDB、Cache Storage 这类 Web 平台存储在 extension service worker、extension page、offscreen document 里可以作为扩展自己的存储使用,但 content script 里直接调用时,心智上不能把它当作扩展统一数据库。
这意味着,如果要在当前场景里认真使用扩展自己的 IndexedDB,比较稳的形态会是:
content script
-> chrome.runtime.sendMessage()
-> extension service worker / extension page
-> IndexedDB这条链路当然能做,但对“每个 job 缓存几十条已观察构建”来说太重。它会引入 schema 版本、事务、消息通信、错误恢复、批量导出等一整套数据库问题。
Stack Overflow 上也有类似讨论:chrome.storage.local 和 IndexedDB 的选择主要取决于数据形状。社区里常见的判断是:简单 JSON 和少量状态用 chrome.storage.local,大数组、复杂对象、索引查询和高频局部更新再考虑 IndexedDB。这个讨论不能替代官方文档,但它很好地补上了工程经验层面的取舍。
其他选项可以很快排掉
chrome.storage.session 适合临时状态。官方文档说明它在扩展 reload、禁用、更新和浏览器重启时都会清掉。用它记录“本次页面生命周期内已经处理过哪些 DOM 节点”可以,用它弥补 Jenkins 历史窗口不合适。
chrome.storage.sync 适合用户设置,不适合本地构建历史缓存。它有约 100 KB 总量和 8 KB 单项限制,还会跟随 Chrome 账号同步。Jenkins 构建记录是机器本地观察结果,不应该默认同步到另一台机器。
chrome.storage.managed 是企业策略配置,读多写少,甚至对扩展本身是只读。它可以用来给全员下发配置,不适合存用户本地观察结果。
Cache Storage 适合 HTTP request / response 缓存,OPFS 适合文件型数据。它们都不是“几行构建摘要”的第一选择。
Cookies 更不合适。MDN 和 web.dev 都提醒过,cookies 会随请求发送。把构建历史放进 cookies 会增加请求体积,也会把本地 UI 缓存带到网络层。
内存变量则只适合当前页面运行期。刷新页面、重新注入 content script 或关闭浏览器都会丢。它可以配合防抖、避免重复渲染,但不能作为历史缓存。
第三方生态怎么处理这个问题
第三方框架也能反映开发者心智。Plasmo 的 Storage API 文档把扩展持久化封装成一个跨 background service worker、content script、extension page 的 Storage 抽象,并支持选择 local / sync 等 storage area。它还提供把指定 key 复制到 Web localStorage 的能力。
这个设计很有参考意义:默认把状态放在扩展存储里;只有确实需要页面可见、页面共享或和网页脚本协作时,才显式桥接到 localStorage。
Stack Overflow 上关于 window.localStorage 和 chrome.storage.local 的经典讨论也基本是这个方向:选择取决于扩展要做什么。localStorage 的域名隔离和 DevTools 可见性是真优势;chrome.storage.local 的扩展级中心化、content script 可用、跨页面共享和变化监听,也是真优势。
所以这不是谁全面碾压谁的问题,而是先问这份数据到底应该跟谁走。
回到这个 Jenkins enhancer
这个缓存有几个约束:
- 数据来源只来自当前浏览器已经看过的 Jenkins 页面。
- 缓存不能恢复 Jenkins 从未展示过、已经清理掉的构建。
- 当前 Jenkins 可见行优先,本地缓存只补掉出窗口的旧行。
- 运行中构建不缓存,避免保存过期进度和取消链接。
- 参数值要脱敏,至少隐藏
token、cookie、password、secret、credential这类字段。 - 每个 job 只保留有限条数,避免本地无限膨胀。
这组约束决定了 chrome.storage.local 是当前更合适的默认方案。它把缓存归到扩展,能和收藏、最近访问、缓存清理入口共用一套存储,也能在未来从 content script 之外的页面统一管理。
但用户提到的透明度问题也应该承认。最终方案不应该只是“存在扩展存储里”。更好的产品处理是直接在 UI 里把缓存讲明白:
- 表头显示“当前页面可见 N 条,本地缓存 M 条”。
- 缓存行标记“本地缓存”。
- 提供“查看缓存 JSON”入口。
- 提供“清空当前 job 缓存”入口。
- 需要时再提供“导出缓存”。
这样普通用户不用知道 DevTools 里 Extension Storage 在哪里,也能知道这些记录来自哪里、能不能信、怎么清掉。
我的判断口径
以后再遇到 Chrome Extension 存储选型,我会按这几个问题判断:
- 数据属于页面还是扩展。页面业务状态优先页面存储;扩展状态优先扩展存储。
- 数据是否要跨站点、跨页面、跨扩展上下文共享。需要共享时优先
chrome.storage.local。 - 用户是否需要直接在页面 DevTools 里看到。透明度特别重要、且数据只服务当前站点时,页面
localStorage可以接受。 - 数据是否需要索引、分页和局部更新。需要数据库能力时再上扩展 context 里的 IndexedDB。
- 数据是否只是当前运行期临时状态。临时状态用内存或
storage.session,不要伪装成持久缓存。
这次 Jenkins 构建历史缓存选 chrome.storage.local,不是因为 localStorage 不行,也不是因为 IndexedDB 不强,而是因为这份数据本质上是扩展的观察结果,体量小、查询简单、需要跨扩展上下文管理。真正需要补的不是换存储,而是把缓存入口做得足够可见,让用户知道扩展到底替他记了什么。