跳转到正文
返回门户

LFStore ​

全局变量:LFStore · 启用方式:// @require store

应用级键值存储,落在扩展后台 IndexedDB。键按 bundleId 自动隔离:同一应用跨站点共享一份数据,不同应用互不可见。除 watch 外所有方法都是异步的。

方法 ​

ts
await LFStore.get(key, fallbackValue?): Promise<T>

读取键值,键不存在时返回 fallbackValue(值为 null 的键视为存在)

ts
await LFStore.set(key, value): Promise<T>

写入键值(可存任意可 JSON 序列化的值),返回传入的原值(不是落库后的规范化值,例如传 Date 返回的仍是 Date 对象)

ts
await LFStore.remove(key): Promise<void>

删除指定键

ts
await LFStore.keys(): Promise<string[]>

列出当前应用的全部键

ts
await LFStore.clear(): Promise<void>

清空当前应用的全部数据

ts
LFStore.watch(key | null, callback, { immediate?, includeSelf? }?): () => void

订阅变更通知(同步注册),返回取消订阅函数

参数详解 ​

参数类型默认值说明
keystring—逻辑键名,会经 String() 转换(get(undefined) 读的是键 "undefined",空串也是合法键);内部自动加 bundleId 前缀做命名空间隔离,keys() 返回的是不含前缀的逻辑键
fallbackValue(get)Tnull键不存在时 resolve 的兜底值。值为 null 的键(包括 set 时传 undefined 的键)视为存在,get 返回 null 而非 fallbackValue
value(set)T—必须可 JSON 序列化。任意层级出现 Map/Set/WeakMap/WeakSet(先转数组)、函数、BigInt、Symbol 或循环引用都会 reject TypeError,错误信息会指出具体路径,如 LFStore.set cannot store a Map value (at value.cache): ...;Date 等带 toJSON 的值按 JSON 规则存为字符串、不报错(Date 读回来是 ISO 字符串而不是 Date);undefined 存为 null

注意事项:单键 value 经 JSON 序列化后的 UTF-8 大小上限为 8388608 bytes(8MiB),恰好等于上限可写入,超限 reject。get() 每次返回深拷贝,改返回值不影响存储。数据落在扩展后台 IndexedDB——同一应用跨站点共享一份数据(类似油猴 GM_setValue),跨导航持久。

命名空间归属与隔离 ​

存储空间属于「应用」而非「单次执行」:终端 run / run --tab 脚本的 bundleId 后缀(::terminal-run::<pid>)在后台统一剥离(与文件权限同一套规整),因此同一本地项目的 liveDebug、已启用实例(app enable)、每一次 run 注入共用同一份数据——某次 run 写入的值,下一次 run 和已启用的应用都能读到。

隔离边界按 bundleId 划分且经浏览器归因鉴权,应用无法读写或 watch 其他应用的数据(伪造 bundleId 在授权层直接被拒绝)。注意:从市场安装的上架版与本地开发版是两个不同的 bundleId(installed::<app_id> vs 本地 UUID),两者不共用 LFStore——同一个应用的开发数据不会带进安装版,反之亦然。

变更订阅:LFStore.watch ​

LFStore.watch(key, callback, options?) 订阅本应用命名空间内的存储变更,返回取消订阅函数(幂等,热重载时框架也会自动清理)。跨实例生效:both 应用的页面实例能收到后台实例的写入(反之亦然),同应用多个标签页、liveDebug / 已启用实例 / run 注入之间同理——这是页面 UI 与后台分支保持状态同步的推荐方式。监听范围严格限定在本应用命名空间内:无法监听其他应用,本地开发版与市场安装版之间也互相不可见(bundleId 不同)。key 为精确键名,传 null 订阅整个命名空间(此时不支持 immediate)。

选项类型默认值说明
immediatebooleanfalse订阅后立即回调一次当前值(reason 为 immediate),用于建立初始状态
includeSelfbooleanfalse是否回显本实例自己的写入。默认不回显(避免“写完自己又被叫醒”的反馈环);同应用的其他实例始终会收到

回调收到 { key, value, found, reason }:reason 为 set / remove / clear / immediate / resync;remove、clear 时 value 为 null 且 found 为 false。语义是「状态更新」而非事件流:合并窗口(约 25ms)内对同一键的高频写入只投递最终值;超过 64KiB 的大值不随通知携带,客户端会自动回读当前值再回调。传输断开(SW 回收 / 扩展重载 / BFCache)后自动退避重连并回发 resync 快照——快照的值可能与上次相同,回调请按幂等状态处理。watch 在所有执行环境都可用:已启用/已上架应用、liveDebug、run --target background、run --tab(MAIN 与 --world user-script 都行),以及不带 --tab 的 run(编辑器沙箱)。拿不到 chrome.runtime.connect 的环境会经终端桥订阅——由宿主代持真正的 Port,合并、resync、取消订阅的语义完全一致。

其它细节:

  • immediate 对不存在的键也会回调一次,收到 { found: false, value: null, reason: 'immediate' }。
  • 每个连接最多 256 个 watch。订阅被后台拒绝(如超出上限)时只在 console 报错,watch() 照常返回取消函数、不会抛异常。
  • watch() 只在以下情况同步抛错:没有传 callback、当前环境没有任何可用通道、key 为 null 却传了 immediate: true。

示例

js
// @require store

const enabled = await LFStore.get("debug-enabled", true);
await LFStore.set("debug-enabled", !enabled);

const allKeys = await LFStore.keys();
await LFStore.remove("legacy-key");

// 订阅变更:both 应用的页面分支可实时看到后台分支的写入(反之亦然)
const unwatch = LFStore.watch("counter", (change) => {
  // change: { key, value, found, reason }
  LFRuntime.print("counter →", change.value, change.reason);
}, { immediate: true });
// 不需要时取消订阅(热重载时框架也会自动清理)
// unwatch();