storages 持久化存储
storages 持久化存储
storages 提供的是 JSXHook 里的轻量持久化能力,底层基于 SharedPreferences。它很适合保存:
- 开关状态
- 表单配置
- 用户选项
- 少量缓存
- 一些“下次启动还要记住”的 JSON 数据
但它并不是数据库,也不适合拿大文件、复杂二进制内容、超大结构去硬塞。
这一页会重点把几个特别容易搞混的地方讲透:
storages和storage到底是什么关系storage.get()在 key 不存在时到底返回什么storage.put()的值到底哪些能存、哪些不能存undefined、null、函数、循环引用对象各自会怎样clear()和storages.remove()到底删的是哪一层
先记住这几件事
storages.create(name)返回的是一个“命名存储空间实例”,不是普通对象。storages只负责“创建 / 删除命名空间”,真正读写数据的是storage.get()、storage.put()这些实例方法。storage.get(key)在 key 不存在时返回undefined;只有你传了第二个参数,才会返回默认值。storage.put(key, value)支持的是 JSON 兼容值,不是任意 JS 值。storage.put(key, undefined)、storage.put(key, function () {})这种顶层值会直接报错。- 对象属性里的
undefined/ 函数会被跳过;数组里的undefined/ 函数会被写成null。 NaN、Infinity、-Infinity这种非有限数字不能存。- 循环引用对象不能存,源码会直接抛异常。
storage.clear()只是清空当前空间里的全部键值;storages.remove(name)才是删除整个命名空间。storages.create("")或者不传名字,也会落到一个真实的命名空间上,只是不推荐这样用。
先分清 storages 和 storage
storages 是命名空间管理器
它只负责两件事:
storages.create(name):创建或打开一个命名空间storages.remove(name):删除整个命名空间
storage 是单个空间实例
storages.create(name) 返回的实例才负责这些事:
storage.get(key, defaultValue?)storage.put(key, value)storage.remove(key)storage.contains(key)storage.clear()
最小心智模型
你可以把它理解成这样:
storages像“仓库管理员”storage像“某一个具体仓库”
例如:
const userStore = storages.create("user_settings");
const cacheStore = storages.create("cache");
这里就已经有两个完全独立的空间了:
user_settingscache
它们之间不会互相串值。
storages.create(name)
参数、返回值与默认行为
| 项目 | 内容 |
|---|---|
| 参数 | name: string |
| 返回值 | storage 实例 |
| 不传参数时 | 会把名字当成空字符串 "" |
| 空字符串时 | 仍然会打开一个真实命名空间,不会报错 |
源码里拿名字的方式本质上是:
const name = args.firstOrNull()?.toString().orEmpty();
所以这几种写法都会成立:
storages.create("demo")
storages.create("")
storages.create()
但从项目维护角度看,后两种都不推荐,因为你后面很难一眼看出这个空间到底是干什么的。
最基本的用法
const store = storages.create("demo-config");
多个模块分开存的例子
const settingStore = storages.create("settings");
const networkStore = storages.create("network");
const cacheStore = storages.create("cache");
推荐怎样给名字
建议命名空间名满足这几个特点:
- 稳定
- 可读
- 能看出用途
例如:
user_settingsnetwork_configdraft_cachepanel_state
不太建议:
ademo2temp_new_final- 空字符串
同名空间会怎样
同一个名字再次 create(name) 时,打开的是同一个存储空间,不会自动新建一份副本。
const store1 = storages.create("demo-config");
const store2 = storages.create("demo-config");
store1.put("name", "rose");
log(store2.get("name")); // rose
storages.remove(name)
参数、返回值与删除层级
| 项目 | 内容 |
|---|---|
| 参数 | name: string |
| 返回值 | boolean |
| 删除对象 | 整个命名空间 |
| 不存在时 | 返回 false |
这个 API 删除的是“整个命名空间”,不是命名空间里的某一个 key。
最小例子
const ok = storages.remove("demo-config");
log(ok);
返回值会在什么情况下是 true
源码会先判断这个命名空间是否“存在过”,只要满足下面任意一种情况,就会认为它存在过:
- 注册表里有这个名字
- 这个空间里当前还有数据
然后才会尝试:
- 清空这个空间里的全部键值
- 从命名空间注册表里删掉这个名字
只有这两步都成功,才会返回 true。
返回值会在什么情况下是 false
常见有两种:
- 这个命名空间从来没创建过,也没有任何数据
- 底层删除动作失败
它和 storage.clear() 的区别
这个区别一定要记住:
| 操作 | 删掉什么 |
|---|---|
storage.clear() | 只清空当前空间里的全部键值 |
storages.remove(name) | 删除整个命名空间记录 |
看例子最清楚:
const store = storages.create("network");
store.clear(); // network 这个空间还在,只是里面没内容了
storages.remove("network"); // network 这个命名空间整体删掉
什么时候该用它
- 做“恢复出厂设置”
- 删除某个模块的全部配置
- 清掉某块缓存并且不想保留命名空间痕迹
storage.get(key, defaultValue?)
参数、返回值与缺失行为
| 项目 | 内容 |
|---|---|
| 参数 1 | key: string |
| 参数 2 | defaultValue?: any |
| 返回值 | 任意已存入的 JSON 兼容值,或默认值,或 undefined |
| key 不存在且传了默认值 | 返回默认值 |
| key 不存在且没传默认值 | 返回 undefined |
这是最关键的一条规则
storage.get("missing") 和 storage.get("missing", null) 不是一回事。
| 写法 | 结果 |
|---|---|
store.get("missing") | undefined |
store.get("missing", null) | null |
store.get("missing", false) | false |
store.get("missing", []) | [] |
所以如果你想区分:
- key 根本不存在
- key 存在但值就是
null
那就不要只看 get() 的返回值,最好配合 storage.contains(key) 一起用。
读取布尔值的例子
const enabled = store.get("enabled", false);
读取数字的例子
const retryCount = store.get("retry_count", 3);
读取对象的例子
const profile = store.get("profile", {
id: 0,
name: ""
});
读取数组的例子
const history = store.get("history", []);
明确判断“有没有这个 key”的例子
const value = store.get("missing_key");
if (value === undefined) {
log("missing");
}
默认值参数本身不会自动写回
这一点也很容易误会。
const count = store.get("count", 0);
上面只是“读取不到时返回 0”,并不会顺便把 count=0 写进存储。
如果你要真正补默认值,得自己再写一次:
if (!store.contains("count")) {
store.put("count", 0);
}
storage.put(key, value)
参数、返回值与失败方式
| 项目 | 内容 |
|---|---|
| 参数 1 | key: string |
| 参数 2 | value: any |
| 返回值 | undefined |
| 失败方式 | 不符合规则时直接抛异常 |
这是这组 API 里最需要讲细的一个方法。
顶层值支持哪些类型
最稳妥的理解是:支持 JSON 兼容值,以及能自然转换成 JSON 兼容值的结构。
当前源码支持这些顶层值:
| 顶层值类型 | 是否支持 | 说明 |
|---|---|---|
null | 支持 | 存成 JSON null |
string | 支持 | 原样存 |
number | 支持 | 但必须是有限数字 |
boolean | 支持 | 原样存 |
| 普通对象 | 支持 | 会转成 JSON 对象 |
| 数组 | 支持 | 会转成 JSON 数组 |
Map 风格对象 | 支持 | key 会转字符串 |
Iterable | 支持 | 会转成 JSON 数组 |
| Java 数组 | 支持 | 会转成 JSON 数组 |
CharSequence | 支持 | 会先转成字符串 |
JSONObject / JSONArray | 支持 | 原样当 JSON 结构处理 |
顶层值不支持哪些类型
这些值直接 put() 会抛异常:
| 顶层值 | 是否支持 | 失败原因 |
|---|---|---|
undefined | 不支持 | 顶层 undefined 被明确禁止 |
function () {} | 不支持 | 顶层函数被明确禁止 |
NaN | 不支持 | 不是有限数字 |
Infinity / -Infinity | 不支持 | 不是有限数字 |
| 循环引用对象 | 不支持 | 会触发循环引用检查 |
| 任意复杂 Java 对象 | 不支持 | 不属于 JSON 兼容值 |
例如下面这些都会出问题:
store.put("bad1", undefined);
store.put("bad2", function () {});
store.put("bad3", NaN);
const a = {};
a.self = a;
store.put("bad4", a);
对象属性和数组元素的规则要分开看
这一点是很多人第一次用时最意外的地方。
对象属性里的 undefined / 函数
对象属性中的这类值不会报错,而是会被跳过,不写进去。
store.put("obj", {
a: 1,
b: undefined,
c() {}
});
再读出来时更接近:
store.get("obj"); // { a: 1 }
数组里的 undefined / 函数
数组里的这类值不会被跳过,而是会变成 null。
store.put("arr", [1, undefined, function () {}]);
再读出来时更接近:
store.get("arr"); // [1, null, null]
数字到底能填什么
| 数值 | 是否支持 | 说明 |
|---|---|---|
0、1、3.14、-10 | 支持 | 有限数字 |
NaN | 不支持 | 会抛出 only supports finite numbers |
Infinity | 不支持 | 同上 |
-Infinity | 不支持 | 同上 |
最常见的写入例子
store.put("enabled", true);
store.put("retry_count", 3);
store.put("base_url", "https://example.com");
store.put("profile", {
id: 1,
name: "jsxhook",
vip: true
});
store.put("history", ["a", "b", "c"]);
store.put("nullable", null);
做“读 - 改 - 写”时的标准写法
对象或数组不会自动跟存储同步,所以修改后要手动 put() 回去。
const profile = store.get("profile", {});
profile.vip = true;
store.put("profile", profile);
const history = store.get("history", []);
history.push("new-item");
store.put("history", history);
两条很实用的建议
- 想持久化的数据,尽量先整理成“纯 JSON 结构”再存。
- 如果你不确定值稳不稳,先
JSON.stringify(value)自检一下心里会更有数。
storage.remove(key)
参数、返回值与删除范围
| 项目 | 内容 |
|---|---|
| 参数 | key: string |
| 返回值 | undefined |
| 删除范围 | 只删除当前命名空间里的单个 key |
删除某个配置项的例子
store.remove("temp_token");
删除某个缓存项的例子
store.remove("last_result");
它不会做什么
- 不会删除整个命名空间
- 不会影响其他
storage - 不会返回“删没删成功”的布尔值
所以如果你需要“删完确认还有没有”,请再配合 contains() 看:
store.remove("enabled");
log(store.contains("enabled"));
storage.contains(key)
参数、返回值与适用场景
| 项目 | 内容 |
|---|---|
| 参数 | key: string |
| 返回值 | boolean |
| 作用 | 判断这个 key 是否真实存在 |
为什么它有时比 get() 更靠谱
因为 get() 有默认值逻辑,也可能读到 null,所以在你需要精确区分下面两种情况时,contains() 更稳:
- key 根本不存在
- key 存在,但值就是
null
最常见的写法
if (store.contains("enabled")) {
log("enabled exists");
}
区分“缺失”和“值为空”的例子
if (store.contains("profile")) {
const profile = store.get("profile");
log(JSON.stringify(profile));
} else {
log("profile 不存在");
}
storage.clear()
参数、返回值与清空范围
| 项目 | 内容 |
|---|---|
| 参数 | 无 |
| 返回值 | undefined |
| 清空范围 | 当前命名空间里的全部键值 |
最小例子
store.clear();
它清掉的是什么
它会清空当前空间里的所有 key,但不会把这个空间的名字本身从命名空间记录里删除。
所以你下次再这样写:
const store = storages.create("demo");
store.clear();
const sameStore = storages.create("demo");
sameStore 依然会打开这个叫 demo 的空间,只是内容已经是空的了。
适合哪些场景
- 退出登录后清掉会话数据
- 重置某个模块的全部配置
- 清缓存但保留空间名称
和 storages.remove(name) 再对比一次
| 需求 | 应该用哪个 |
|---|---|
| 保留空间名,只清内容 | storage.clear() |
| 连命名空间一起删 | storages.remove(name) |
常见的“设置 / 获取 / 修改”套路
设置一个布尔开关
store.put("enabled", true);
读取一个布尔开关
const enabled = store.get("enabled", false);
反转布尔开关
store.put("enabled", !store.get("enabled", false));
设置一个字符串
store.put("base_url", "https://example.com");
读取并修改字符串
const host = store.get("base_url", "https://example.com");
store.put("base_url", `${host}/v1`);
设置一个对象
store.put("profile", {
id: 1,
name: "jsxhook",
vip: true
});
读取对象并修改后写回
const profile = store.get("profile", {});
profile.vip = true;
store.put("profile", profile);
设置一个数组
store.put("history", ["a", "b", "c"]);
读取数组并追加后写回
const history = store.get("history", []);
history.push("d");
store.put("history", history);
重置当前空间
store.clear();
storages 模块完整实战例子
这个例子基本把手册里的常见用法都串起来了。
const store = storages.create("demo-config");
const sameStore = storages.create("demo-config");
// 写入 number / string / boolean / null / object / array
store.put("count", 1);
store.put("name", "rose");
store.put("enabled", true);
store.put("empty", null);
store.put("profile", {
nickname: "JSXHook",
tags: ["autojs", "storages"],
level: 5
});
// 读取与默认值
log(store.get("count"));
log(store.get("name", "default"));
log(typeof store.get("missing"));
log(store.get("missing", "fallback"));
log(JSON.stringify(sameStore.get("profile"), null, 2));
// contains / remove
log(store.contains("enabled"));
store.remove("enabled");
log(store.contains("enabled"));
// clear 只清空当前 storage 的全部键值
store.clear();
log(typeof store.get("name"));
// remove 删除整个命名 storage
log(storages.remove("demo-config"));
log(storages.remove("demo-config"));
常见误区与排查
误区 1:storage.get("x", 0) 会自动把 x=0 写进去
不会。它只是“读不到时返回 0”,不等于写回存储。
误区 2:storage.clear() 和 storages.remove(name) 一样
不一样:
clear()清的是内容remove(name)删的是整个命名空间
误区 3:什么 JS 值都能 put()
不能。最稳的思路就是:
- 字符串、数字、布尔、
null - 纯对象
- 纯数组
其余值先怀疑,再验证。
误区 4:对象改完会自动同步回存储
不会。get() 读出来后,你拿到的是脚本层数据;改完还得自己 put() 回去。
误区 5:undefined 和 null 是同一种“空值”
在这组 API 里不是:
- key 不存在且没传默认值:
undefined - key 存在且你存进去的是
null:null
最后一条实战建议
如果你准备把一个数据长期放进 storages,先问自己一句:
“这份数据能不能被我老老实实写成 JSON?”
如果答案是“不能”,那这份数据大概率就不适合直接塞进 storages。
