欢迎来到 Sciter.js 的世界!对于许多桌面 GUI 应用来说,能够在本地持久化存储和管理数据是至关重要的。无论你是经验丰富的前端开发者,还是从其他语言转过来的新手,理解如何在 Sciter.js 中处理本地数据都将是开发过程中的关键一步。本指南将介绍 Sciter.js 提供的两种主要本地数据存储机制:内建的 Storage 模块和集成的 SQLite 数据库。
1. Sciter.js 内建 Storage (@storage)
Sciter.js 提供了一个名为 Storage 的内建模块,它实现了一个基于文件的、类似 NoSQL 的持久化对象存储。你可以把它想象成一个本地的、持久化的 JavaScript 文档数据库,非常适合存储应用程序配置、用户偏好或结构相对简单的数据。
1.1 基本概念
- 持久化: 数据存储在文件中,即使应用关闭或重启,数据依然存在。
- 对象存储: 你可以直接存储 JavaScript 对象、数组、基本类型等。
- 根对象 (
storage.root): 数据库的核心是一个根对象。所有的数据都作为这个根对象的属性或嵌套属性来存储。 - 索引 (
Index): 为了快速查找数据,可以创建索引。
1.2 初始化和打开数据库
首先,你需要导入 Storage 和 Env (用于获取标准路径) 模块。然后使用 Storage.open() 打开或创建一个数据库文件。如果文件不存在,它会被创建。
import * as Storage from "@storage";
import * as Env from "@env";
// 建议将数据库文件存储在用户文档或应用数据目录
const dbPath = Env.path("documents") + "/my-app-data.db";
// 或者 Env.path("appdata") + "/YourAppName/my-app-data.db";
let storage = Storage.open(dbPath);
// 检查数据库是否是新创建的,如果是,则初始化
if (!storage.root) {
console.log("Initializing new database...");
storage.root = {
version: 1,
users: {},
settings: {
theme: "light"
},
// ... 其他初始数据结构
};
storage.commit(); // 确保初始结构被写入
}
let root = storage.root; // 获取根对象,后续操作都基于此对象
// 别忘了在应用退出前关闭数据库
document.on("beforeunload", function() {
if (storage) {
storage.close();
storage = null;
root = null;
}
});
1.3 基本数据操作 (CRUD)
数据操作就像操作普通的 JavaScript 对象一样简单。
// 创建/更新 (Set)
root.settings.theme = "dark";
root.users["user123"] = { name: "Alice", email: "alice@example.com" };
// 读取 (Get)
let currentTheme = root.settings.theme; // "dark"
let user = root.users["user123"]; // { name: "Alice", ... }
// 删除 (Delete)
delete root.users["user123"];
// **重要**: 默认情况下,Storage 会自动提交更改。
// 但在某些复杂操作或需要确保写入时,可以手动调用 storage.commit();
storage.commit();
1.4 使用索引 (storage.createIndex)
当需要根据某个字段快速查找大量数据时,索引非常有用。例如,存储笔记并按创建日期或 ID 查找。
// 在初始化时创建索引
function initDb(storage) {
storage.root = {
version: 1,
// 按日期索引笔记 (日期可能重复,所以 unique = false)
notesByDate: storage.createIndex("date", false),
// 按 ID 索引笔记 (ID 必须唯一,所以 unique = true)
notesById: storage.createIndex("string", true)
};
return storage.root;
}
// ... 打开数据库,如果需要则调用 initDb ...
var root = storage.root || initDb(storage);
// 添加数据到索引
let note1 = { id: "uuid1", date: new Date(2023, 10, 1), text: "Note 1" };
let note2 = { id: "uuid2", date: new Date(2023, 10, 5), text: "Note 2" };
root.notesByDate.set(note1.date, note1);
root.notesById.set(note1.id, note1);
root.notesByDate.set(note2.date, note2);
root.notesById.set(note2.id, note2);
storage.commit();
// 通过索引查询数据
let noteFromId = root.notesById.get("uuid1"); // 获取 ID 为 uuid1 的笔记
// 遍历按日期排序的笔记 (Index 对象本身是可迭代的)
console.log("Notes by date:");
for (let note of root.notesByDate) {
console.log(` - ${note.date.toLocaleDateString()}: ${note.text}`);
}
// 删除索引中的数据
// 对于非唯一索引,删除时需要提供键和值
root.notesByDate.delete(note1.date, note1);
// 对于唯一索引,只需要提供键
root.notesById.delete(note1.id);
storage.commit();
1.5 存储自定义类的对象
Storage 可以存储自定义类的实例,并在从数据库读取时恢复其原型链,这意味着对象的方法可以被正确调用。
import * as Sciter from "@sciter"; // 用于生成 UUID
export class Note {
constructor(text, date = undefined, id = undefined) {
this.id = id || Sciter.uuid(); // 生成唯一 ID
this.date = date || new Date();
this.text = text;
// 添加到 Storage (假设 storage 和 root 已定义)
let root = storage.root;
root.notesByDate.set(this.date, this);
root.notesById.set(this.id, this);
storage.commit();
console.log(`Note created: ${this.id}`);
// 可以触发事件通知 UI 更新
document.post(new Event("new-note", { bubbles: true, data: this }));
}
// 实例方法
updateText(newText) {
this.text = newText;
// 更新存储 (因为对象是引用,直接修改属性即可,但需要 commit)
// 注意:如果索引键 (date, id) 改变,需要先 delete 再 set
storage.commit();
console.log(`Note updated: ${this.id}`);
}
// 实例方法
delete() {
let root = storage.root;
root.notesByDate.delete(this.date, this);
root.notesById.delete(this.id);
storage.commit();
console.log(`Note deleted: ${this.id}`);
// 可以触发事件通知 UI 更新
document.post(new Event("delete-note", { bubbles: true, data: this }));
}
// 静态方法:通过 ID 获取 Note 实例
static getById(id) {
// Storage 会自动恢复 Note 的原型
return storage.root.notesById.get(id);
}
// 静态方法:获取所有笔记 (按日期排序)
static all() {
return root.notesByDate; // 返回 Index 对象,可直接迭代
}
}
// **重要**: 必须注册类,Storage 才能正确地序列化和反序列化
storage.registerClass(Note);
// --- 使用 ---
let newNote = new Note("My first persistent note!");
// ... 稍后或应用重启后 ...
let retrievedNote = Note.getById(newNote.id);
if (retrievedNote) {
console.log("Retrieved note text:", retrievedNote.text);
retrievedNote.updateText("Updated text!");
// retrievedNote.delete();
}
console.log("All notes:");
for(let note of Note.all()) {
console.log(` - ID: ${note.id}, Text: ${note.text}`);
}
2. Sciter.js SQLite 集成 (sciter-sqlite)
对于需要更复杂的关系数据模型、强大的查询能力(如 JOIN、GROUP BY)和事务支持的场景,Sciter.js 提供了与 SQLite 的集成。SQLite 是一个轻量级的、基于文件的关系型数据库。
2.1 基本概念
- 关系型数据库: 数据存储在结构化的表中,表之间可以建立关系。
- SQL: 使用结构化查询语言 (SQL) 来操作数据。
- Native Library: SQLite 功能是通过加载一个本地库 (
sciter-sqlite.dll或类似文件) 来实现的。
2.2 设置和加载
你需要确保 sciter-sqlite 库文件位于 Sciter 引擎可以找到的地方(通常与主程序或 Sciter DLL 在同一目录,或在 PATH 环境变量中)。然后使用 loadLibrary 加载它。
import { loadLibrary } from "@sciter";
// 尝试加载 SQLite 库
if (!globalThis.SQLite) { // 避免重复加载
try {
globalThis.SQLite = loadLibrary("sciter-sqlite");
if (!globalThis.SQLite) {
throw new Error("Failed to load sciter-sqlite library. Make sure it's accessible.");
}
console.log("SQLite library loaded successfully.");
} catch (e) {
console.error(e.message);
// 在 UI 中提示用户错误,或禁用数据库功能
document.body.innerText = "Error loading SQLite library!";
}
}
2.3 打开和关闭数据库
使用 SQLite.open() 打开数据库。你可以指定一个文件路径,或者使用 :memory: 创建一个临时的内存数据库。
if (globalThis.SQLite) { // 确保库已加载
let db;
try {
// 打开/创建文件数据库
const dbFilePath = Env.path("documents") + "/my-sqlite-app.db";
db = SQLite.open(dbFilePath);
console.log(`Database opened: ${dbFilePath}`);
// 或者,打开内存数据库 (应用关闭后数据丢失)
// db = SQLite.open(":memory:");
// console.log("In-memory database opened.");
// ... 执行数据库操作 ...
} catch (e) {
console.error("Database operation failed:", e);
} finally {
// **重要**: 完成操作后务必关闭数据库连接
if (db) {
db.close();
console.log("Database closed.");
}
}
}
2.4 执行 SQL 语句 (db.exec)
db.exec() 是执行 SQL 语句的核心方法。
- 无参数查询: 直接传入 SQL 字符串。
- 带参数查询: 传入 SQL 字符串(使用
?作为占位符)和一个包含参数值的数组。这是推荐的方式,可以防止 SQL 注入。 - 返回值:
- 对于
SELECT查询,成功时返回一个Recordset对象。 - 对于
INSERT,UPDATE,DELETE等操作,返回一个数组[resultCode, rowsAffected],resultCode是 SQLite 的结果代码 (如SQLite.OK),rowsAffected是受影响的行数。 - 对于
CREATE TABLE等 DDL 语句,通常返回[SQLite.OK, 0]。 - 如果发生错误,会抛出异常。
- 对于
if (db) { // 确保 db 已打开
try {
// 创建表 (DDL)
db.exec(`
CREATE TABLE IF NOT EXISTS stocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT,
trans TEXT,
symbol TEXT UNIQUE,
qty REAL,
price REAL
)
`);
console.log("Table 'stocks' created or already exists.");
// 插入数据 (DML) - 字符串方式 (不推荐)
// db.exec("INSERT INTO stocks (date, trans, symbol, qty, price) VALUES ('2006-01-05','BUY','RHAT',100,35.14)");
// 插入数据 - 使用参数绑定 (推荐)
let [result, rows] = db.exec(
"INSERT OR IGNORE INTO stocks (date, trans, symbol, qty, price) VALUES (?, ?, ?, ?, ?)",
["2006-04-05", "BUY", "MSFT", 1000, 72.00]
);
if (result === SQLite.OK) {
console.log(`Inserted ${rows} row(s) for MSFT.`);
} else {
console.warn(`Failed to insert MSFT, result code: ${result}`);
}
[result, rows] = db.exec(
"INSERT OR IGNORE INTO stocks (date, trans, symbol, qty, price) VALUES (?, ?, ?, ?, ?)",
["2006-04-06", "SELL", "IBM", 500, 53.00]
);
if (result === SQLite.OK) {
console.log(`Inserted ${rows} row(s) for IBM.`);
} else {
console.warn(`Failed to insert IBM, result code: ${result}`);
}
// 更新数据
[result, rows] = db.exec("UPDATE stocks SET qty = ? WHERE symbol = ?", [1500, "MSFT"]);
console.log(`Updated ${rows} row(s) for MSFT.`);
// 查询数据 (SELECT)
console.log("\nStocks ordered by price:");
let rs = db.exec("SELECT symbol, qty, price FROM stocks ORDER BY price DESC");
// 检查返回的是否是 Recordset
if (Asset.instanceOf(rs, "Recordset")) {
// 处理 Recordset (见下一节)
showRecordset(rs);
} else {
console.error("Expected a Recordset, but got:", rs);
}
// 删除数据
// [result, rows] = db.exec("DELETE FROM stocks WHERE symbol = ?", ["IBM"]);
// console.log(`Deleted ${rows} row(s) for IBM.`);
} catch (e) {
console.error("SQL execution error:", e);
}
}
2.5 处理查询结果 (Recordset)
Recordset 对象代表 SELECT 查询返回的结果集。
- 迭代: 使用
rs.next()移动到下一行,当没有更多行时返回false。通常与do...while或while循环结合使用。 - 访问列数据:
- 通过索引:
rs[0],rs[1]... - 通过列名:
rs["symbol"],rs["price"](注意:Sciter 的 SQLite 包装器可能不支持直接通过列名访问,示例中使用的是索引。请以实际测试为准或查阅最新文档。如果不支持,你需要知道列的顺序)。
- 通过索引:
- 获取列信息:
rs.length: 列的数量。rs.name(index): 获取指定索引列的名称。rs.name(index, "database"),rs.name(index, "table"),rs.name(index, "field"): 获取更详细的列来源信息。
function showRecordset(rs) {
if (!rs || !Asset.instanceOf(rs, "Recordset") || !rs.valid) {
console.log("No records found or invalid recordset.");
return;
}
// 1. 获取表头
let headers = [];
for (let i = 0; i < rs.length; ++i) {
headers.push(rs.name(i));
}
console.log("| " + headers.join(" | ") + " |");
console.log("|-" + headers.map(h => "-".repeat(h.length)).join("-|-") + "-|");
// 2. 迭代数据行
let rowCount = 0;
do {
let rowData = [];
for (let i = 0; i < rs.length; ++i) {
// 访问数据,注意 rs[i] 返回的是原始值
rowData.push(rs[i]);
}
console.log("| " + rowData.join(" | ") + " |");
rowCount++;
} while (rs.next()); // 移动到下一行
console.log(`\nTotal rows: ${rowCount}`);
// Recordset 使用完毕后会自动释放资源,无需手动 close rs
}
// 在 db.exec("SELECT ...") 之后调用
// if (Asset.instanceOf(rs, "Recordset")) {
// showRecordset(rs);
// }
2.6 SQLite 语法差异
Sciter 集成的 SQLite 通常遵循标准的 SQLite 语法。主要的“差异”在于你如何通过 Sciter 的 JavaScript API (db.exec, Recordset) 与之交互,而不是 SQL 语言本身。你需要熟悉 db.exec 的参数和返回值,以及如何处理 Recordset。
3. 如何选择:Storage vs SQLite?
-
使用
Storage(@storage) 当:- 数据结构相对简单,主要是键值对或嵌套对象/数组。
- 主要需求是快速存储和检索整个对象。
- 不需要复杂的跨对象查询、连接或聚合。
- 希望使用更接近 JavaScript 原生对象的操作方式。
- 性能要求不高,或者数据量不大时,索引能满足查询需求。
-
使用
SQLite(sciter-sqlite) 当:- 数据是关系型的,需要在多个表之间建立联系。
- 需要执行复杂的 SQL 查询(JOIN, GROUP BY, WHERE 子句等)。
- 需要 ACID 事务保证数据一致性。
- 数据量较大,需要关系型数据库的查询优化能力。
- 团队熟悉 SQL。
在某些应用中,甚至可以两者结合使用:用 Storage 存储应用配置和简单的状态,用 SQLite 存储核心的业务数据。
4. 最佳实践
- 错误处理: 始终将数据库操作(打开、执行、关闭)放在
try...catch...finally块中,以捕获潜在错误并确保资源(如数据库连接)被正确释放。 - 数据库位置: 使用
Env.path("documents")或Env.path("appdata")将数据库文件存储在用户特定的、可写的位置,避免权限问题。 - 关闭连接: 确保在应用程序退出时 (
document.on("beforeunload")) 或不再需要数据库时调用storage.close()或db.close()。 - 参数化查询 (SQLite): 绝对优先使用
db.exec(sql, params)的方式执行 SQL,以防止 SQL 注入攻击。 - 索引 (Storage & SQLite): 合理使用索引 (
storage.createIndex或 SQLite 的CREATE INDEX) 来优化查询性能,但也要注意索引会增加写操作的开销。 - 数据验证: 在将数据写入数据库之前,进行必要的验证,确保数据类型和格式正确。
- 事务 (SQLite): 对于需要原子性的一系列操作(要么全部成功,要么全部失败),使用 SQLite 的事务 (
BEGIN TRANSACTION,COMMIT,ROLLBACK)。Sciter 的db.exec可以执行这些命令。 - 备份与迁移: 考虑数据库备份策略。如果未来可能更新数据库结构,需要设计数据迁移方案(例如,通过检查
storage.root.version或 SQLite 的PRAGMA user_version)。