API

nsh/storage

nsh/storage 把应用的数据保存在设备上:以字符串为键、字符串为值,应用下次启动时数据仍在。适合保存设置、草稿、用户上次打开的标签页、服务器返回内容的缓存——任何需要比应用进程活得更久的状态。

mod-storage.tsx 在线编辑 在新标签页打开
mod-storage.tsx运行中
// Storage: a launch counter kept across reloads, the stored keys and a reset.
import { mount, signal, Text, View } from "nsh";
import { Storage } from "nsh/storage";
import { Button } from "nsh/ui/mobile";
import { ui } from "./palette";

function App() {
  const opened = signal<number | null>(null);
  const keys = signal<string[]>([]);

  const start = async () => {
    const count = Number((await Storage.getItem("opened")) ?? "0") + 1;
    await Storage.setItem("opened", String(count));
    await Storage.setItem("lastOpened", new Date().toISOString());
    opened.set(count);
    keys.set(await Storage.getAllKeys());
  };
  const reset = async () => {
    await Storage.clear();
    opened.set(0);
    keys.set(await Storage.getAllKeys());
  };
  start();

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 16, paddingTop: 24, gap: 12 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>Storage</Text>
      <View style={{ borderRadius: 14, padding: 16, gap: 6, ...ui().card }}>
        <Text style={{ fontSize: 28, fontWeight: "700", color: ui().text }}>
          {opened() === null ? "Reading…" : `Opened ${opened()} times`}
        </Text>
        <Text style={{ color: ui().muted }}>Reload the page: the count is still there.</Text>
      </View>
      <Text style={{ color: ui().muted }}>{`Keys: ${keys().length ? keys().join(", ") : "none"}`}</Text>
      <Button title="Reset" variant="secondary" onPress={reset} />
    </View>
  );
}

mount(App);
import { Storage } from "nsh/storage";
const Storage: {
  getItem(key: string): Promise<string | null>;
  setItem(key: string, value: string): Promise<void>;
  removeItem(key: string): Promise<void>;
  getAllKeys(): Promise<string[]>;
  clear(): Promise<void>;
};

每个应用有自己的存储,键和值合计最多 5 MB。调用按应用发起的顺序执行,所以在 setItem 之后调用 getItem 无需等待写入就能读到新值。读取来自内存;写入在后台成批落盘。

平台数据保存位置
iOS应用容器内的 Library/Application Support/nsh/<app id>/storage.json
Android应用私有目录内的 files/nsh/<app id>/storage.json
macOS~/Library/Application Support/<app id>/storage.json
Windows%APPDATA%\<app id>\storage.json
Linux$XDG_DATA_HOME/<app id>/storage.json(~/.local/share/…)
Web浏览器的 localStorage,键以 app id 命名(nsh:com.example.todo:…)

损坏的 storage.json 会被保留为同目录下的 storage.json.corrupt,应用从空存储开始;读不了的文件则让应用的改动只留在内存中,文件本身永远不会被覆盖。

app id 是 nsh.json 中的 id,它随应用一起传递:nsh build 和 nsh package 把它写在 bundle 旁边,开发服务器会发送它,签名包则带有经发布密钥证书担保的 id。因此无论应用在哪里运行,都无需任何设置就各自保存自己的数据;没有 id 的应用保存在 default 下。宿主应用可以另选位置(见在 iOS 中嵌入和在 Android 中嵌入)。

#Storage.getItem

getItem(key: string): Promise<string | null>;

返回 key 的值,没有值时返回 null。

const theme = (await Storage.getItem("theme")) ?? "system";

#Storage.setItem

setItem(key: string, value: string): Promise<void>;

把 key 设为 value,替换原有的值。如果存储会超过 5 MB,则 Promise 被拒绝,值不会被保存。值都是字符串:结构化数据用 JSON.stringify 保存,用 JSON.parse 读回。

Storage.setItem("draft", JSON.stringify({ title: title(), body: body() }));

#Storage.removeItem

removeItem(key: string): Promise<void>;

删除 key 及其值。删除一个没有值的键不做任何事。

#Storage.getAllKeys

getAllKeys(): Promise<string[]>;

按顺序返回所有键。

#Storage.clear

clear(): Promise<void>;

删除应用的所有键,例如在用户退出登录时。

#把 signal 保存在设备上

应用启动时读取保存的值,之后每当 signal 变化就写回去:

import { effect, mount, signal, Text, View } from "nsh";
import { Storage } from "nsh/storage";

const count = signal(0);
const loaded = signal(false);
Storage.getItem("count").then((saved) => {
  count.set(Number(saved ?? 0));
  loaded.set(true);
});
effect(() => {
  if (loaded()) Storage.setItem("count", String(count()));
});

mount(() => (
  <View onPress={() => count.update((n) => n + 1)}>
    <Text>按了 {count()} 次</Text>
  </View>
));

#在 Rust 中使用

Rust 组件通过 nsh_core::storage::Storage 读写同一份存储,它的函数与 TS 一一对应,因此同时有 TSX 入口和 Rust 入口的应用看到的是同一份数据。这些函数是同步的:数据就在内存中。

use nsh_core::storage::Storage;

let count: u32 = Storage::get_item("count").and_then(|n| n.parse().ok()).unwrap_or(0);
Storage::set_item("count", (count + 1).to_string())?;
TSRust
await Storage.getItem(key)Storage::get_item(key) -> Option<String>
await Storage.setItem(key, value)Storage::set_item(key, value) -> Result<(), StorageFull>
await Storage.removeItem(key)Storage::remove_item(key)
await Storage.getAllKeys()Storage::get_all_keys() -> Vec<String>
await Storage.clear()Storage::clear()