API

nsh/storage

存储 API 把应用的数据保存在设备上:以字符串为键、字符串为值,应用下次启动时数据仍在。适合保存设置、草稿、用户上次打开的标签页、服务器返回内容的缓存——任何需要比应用进程活得更久的状态。Rust 代码使用 nsh_core::storage::Storage,TSX 从 nsh/storage 导入 Storage;两者读写同一份数据,因此同时有 Rust 入口和 TSX 入口的应用看到的是同一份数据。

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 { Button } from "nsh/base";
import { Storage } from "nsh/storage";
import { size, ui, wide } 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.keys());
  };
  const reset = async () => {
    await Storage.clear();
    opened.set(0);
    keys.set(await Storage.keys());
  };
  start();

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: wide() ? 20 : 16, paddingTop: 24, gap: 12, alignItems: wide() ? "flex-start" : "stretch" }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>Storage</Text>
      <View style={{ borderRadius: 14, padding: 16, gap: 6, ...ui().card, width: wide() ? 440 : undefined }}>
        <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
        onPress={reset}
        style={wide() ? { ...ui().raised, height: size.control } : ui().raised}
        class={wide() ? "justify-center rounded-lg px-3 active:opacity-75" : "items-center rounded-xl py-3 active:opacity-75"}
      >
        <Text style={{ fontWeight: "600", fontSize: wide() ? 13 : undefined, color: ui().text }}>Reset</Text>
      </Button>
    </View>
  );
}

mount(App);
use nsh_core::storage::Storage;
const Storage: {
  getItem(key: string): Promise<string | null>;
  setItem(key: string, value: string): Promise<void>;
  removeItem(key: string): Promise<void>;
  keys(): Promise<string[]>;
  clear(): Promise<void>;
};
TSRust(nsh_core::storage::Storage)
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.keys()Storage::get_all_keys() -> Vec<String>
await Storage.clear()Storage::clear()

Rust 函数是同步的,因为数据就在内存中:在组件运行的地方(组件函数体、事件处理函数和任务)调用它们。

每个应用有自己的存储,键和值合计最多 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。Rust:Storage::get_item(key),没有值时为 None。

let theme = Storage::get_item("theme").unwrap_or_else(|| "system".into());

#Storage.setItem

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

把 key 设为 value,替换原有的值。如果存储会超过 5 MB,则 Promise 被拒绝,值不会被保存。Rust:Storage::set_item(key, value),此时返回 Err(StorageFull)。值都是字符串:结构化数据保存为 JSON(Rust 用 serde_json::to_string,TSX 用 JSON.stringify),再读回(serde_json::from_str、JSON.parse)。

let draft = serde_json::json!({ "title": title.get(), "body": body.get() });
if let Err(full) = Storage::set_item("draft", draft.to_string()) {
    log::warn!("{full}");
}

#Storage.removeItem

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

删除 key 及其值。删除一个没有值的键不做任何事。Rust:Storage::remove_item(key)。

#Storage.keys

keys(): Promise<string[]>;

按顺序返回所有键。Rust:Storage::get_all_keys()。

#Storage.clear

clear(): Promise<void>;

删除应用的所有键,例如在用户退出登录时。Rust:Storage::clear()。

#把 signal 保存在设备上

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

use nsh_core::{storage::Storage, ui::prelude::*};

#[component]
pub fn App() -> impl IntoElement {
    let saved = Storage::get_item("count").and_then(|n| n.parse().ok());
    let count = signal(saved.unwrap_or(0_u32));
    effect(move || {
        if let Err(full) = Storage::set_item("count", count.get().to_string()) {
            log::warn!("{full}");
        }
    });
    view()
        .on_press(move || count.update(|n| *n += 1))
        .child(text(move || format!("按了 {} 次", count.get())))
}