feat-ai(storage.js): 添加nodejs环境下的sqlite存储支持,调用方法与浏览器环境下保存一致

This commit is contained in:
windychen0
2026-06-06 19:51:30 +08:00
parent 4f8be6293d
commit 3994bca0ab
7 changed files with 418 additions and 90 deletions
+6 -6
View File
@@ -13,16 +13,16 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Install dependencies
run: npm i # 或 npm install
- name: Run build
run: npm run build
- name: Setup npm for Verdaccio
run: |
npm config set registry http://verdaccio:4873/
npm config set //verdaccio:4873/:_authToken ${{ secrets.NPM_TOKEN }}
- name: Install dependencies
run: npm i --registry http://verdaccio:4873/ # 或 npm install
- name: Run build
run: npm run build
- name: Publish package
run: npm publish
+6
View File
@@ -1,9 +1,15 @@
# Dependencies
node_modules/
package-lock.json
# Build output
dist/
# SQLite database files
*.db
*.db-wal
*.db-shm
# OS files
.DS_Store
Thumbs.db
+123
View File
@@ -0,0 +1,123 @@
# Windychen Utils 项目开发记录
**日期**: 2026-06-06
**项目**: windychen-untils (v1.0.1)
**类型**: Storage 模块跨环境兼容改造
---
## 一、本次需求
`storage.js` 从**仅浏览器环境**扩展为**浏览器 + Node.js 双环境**可用,并在 Node.js 端使用 SQLite 持久化。
---
## 二、新增存储类
### 1. MemoryStorage(通用兜底)
| 方法 | 功能 | 后端 |
| ---------------------- | ------------ | ----- |
| `get(key, initValue?)` | 从内存获取值 | `Map` |
| `set(key, value)` | 写入内存 | `Map` |
- 纯内存,进程退出即丢失
- 所有环境通用,作为最低优先级兜底
### 2. SqliteStorageNode.js 专用)
| 方法 | 功能 | 后端 |
| ---------------------- | -------------- | ---------------- |
| `get(key, initValue?)` | 从 SQLite 查询 | `better-sqlite3` |
| `set(key, value)` | 写入 SQLite | `better-sqlite3` |
**实现细节:**
```javascript
// 表结构
CREATE TABLE IF NOT EXISTS kv_store (key TEXT PRIMARY KEY, value TEXT)
// set: INSERT OR REPLACEvalue 经 JSON.stringify
// get: SELECT value WHERE key = ?,结果经 JSON.parse
```
**优化点:**
- WAL 模式(`PRAGMA journal_mode = WAL`)提升并发读写性能
- 预编译语句(`db.prepare().get/run`)避免重复解析 SQL
- 延迟 require`_SqliteStorageClass` 工厂函数 + try-catch 包装,使 better-sqlite3 在浏览器/webpack 环境中静默降级为 null
---
## 三、环境自动选择策略(更新后)
```
浏览器:IndexedDB → localStorage → MemoryStorage
Node.jsSqliteStorage → MemoryStorage
```
**改动文件:**
| 文件 | 变更 |
| -------------- | ------------------------------------------------------------------ |
| `storage.js` | +MemoryStorage 类,+SqliteStorage 类,更新导出逻辑 |
| `index.js` | 移除 `try { require('./storage') } catch (e) {}`,改为直接 require |
| `test.js` | 新增 5 条 Storage 异步测试(对象/字符串/数字/数组/默认值) |
| `.gitignore` | 新增 `*.db` `*.db-wal` `*.db-shm` |
| `package.json` | 新增 `better-sqlite3` 依赖 |
---
## 四、演进过程
1. **第一版**`FileStorage`(每 key 一个 JSON 文件,`fs.promises` 读写)
2. **TS 报错**async 方法在 try-catch 块内部定义类导致解析异常 → 提取为 `_FileStorageClass` 工厂函数
3. **用户反馈**:改用 SQLite 而非 JSON 文件存储
4. **最终方案**`SqliteStorage`better-sqlite3 同步 API + WAL 模式
---
## 五、数据文件
```
.windychen-storage/
├── storage.db # 主数据库
├── storage.db-shm # WAL 共享内存
└── storage.db-wal # WAL 日志
```
---
## 六、测试结果
```
共 21 项,21 通过,0 失败
─── Object.getPro ─── (4 项)
─── Object.setPro ─── (2 项)
─── Array.findPro ─── (3 项)
─── Array.filterPro ─── (2 项)
─── Array.findIndexPro ─── (1 项)
─── Array.findLastPro ─── (1 项)
─── Array.somePro / everyPro ─── (3 项)
─── Storage ─── (5 项)
```
---
## 七、关键决策记录
1. **SQLite 选型**: 选择 better-sqlite3(原生编译、同步 API)而非 sql.js(纯 JS),优先性能
2. **降级策略**: Node.js → SqliteStorage → MemoryStorage,与浏览器端 IndexedDB → localStorage → MemoryStorage 保持一致的链式降级模式
3. **JSON 序列化**: 所有值通过 `JSON.stringify/parse` 统一存取,保证类型可靠性
4. **WAL 模式**: 启用 Write-Ahead Logging,避免写阻塞读
---
## 八、待改进方向
- [ ] Storage 模块添加 delete/clear 方法
- [ ] 添加单元测试框架(如 Jest/Vitest
- [ ] 支持 TypeScript 类型声明 (.d.ts)
- [ ] 考虑添加更多工具方法(日期、字符串、数字等)
- [ ] CI/CD 自动化构建与发布
+3 -4
View File
@@ -1,4 +1,3 @@
require('./object');
require('./array');
try { require('./storage'); } catch (e) {} // 仅浏览器环境生效
require("./object");
require("./array");
require("./storage");
+3
View File
@@ -18,5 +18,8 @@
"devDependencies": {
"webpack": "^5.90.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"better-sqlite3": "^12.10.0"
}
}
+193 -35
View File
@@ -7,7 +7,9 @@ var log = console.warn;
* @returns {boolean}
* @private
*/
var isString = function (str) { return Object.getTypeString(str) === 'String'; };
var isString = function (str) {
return Object.getTypeString(str) === "String";
};
/**
* IndexedDB 存储实现(大容量存储优先选择)
@@ -29,24 +31,37 @@ class IDBStorage {
/** @type {number} 数据库版本号 */
this.version = version || 1;
/** @type {string} 数据库名称 */
this.dbName = dbName || 'dbName';
this.dbName = dbName || "dbName";
/** @type {string} 对象存储名称 */
this.storeName = storeName || 'storeName';
this.storeName = storeName || "storeName";
var request = indexedDB.open(this.dbName, this.version);
/** @type {Promise<IDBDatabase>} 数据库连接 Promise */
this.requestPromise = new Promise(function (resolve, reject) {
this.requestPromise = new Promise(
function (resolve, reject) {
request.onupgradeneeded = function (e) {
if (e && e.target && !request.result.objectStoreNames.contains(this.storeName)) {
request.result.createObjectStore(this.storeName, { keyPath: 'key' })
.createIndex('key', 'key', { unique: false });
if (
e &&
e.target &&
!request.result.objectStoreNames.contains(this.storeName)
) {
request.result
.createObjectStore(this.storeName, { keyPath: "key" })
.createIndex("key", "key", { unique: false });
}
log('[Index][open]-upgrade');
log("[Index][open]-upgrade");
resolve(request.result);
}.bind(this);
request.onsuccess = function () { log('[IndexDB][open]-success'); resolve(request.result); };
request.onerror = function (e) { log('[IndexDB][open]-error', 2); reject(e); };
}.bind(this));
request.onsuccess = function () {
log("[IndexDB][open]-success");
resolve(request.result);
};
request.onerror = function (e) {
log("[IndexDB][open]-error", 2);
reject(e);
};
}.bind(this),
);
}
/**
@@ -60,11 +75,16 @@ class IDBStorage {
var _this = this;
return new Promise(async function (resolve) {
var db = await _this.requestPromise;
var transaction = db.transaction(_this.storeName, 'readonly');
var transaction = db.transaction(_this.storeName, "readonly");
var objectStore = transaction.objectStore(_this.storeName);
var req = objectStore.get(key);
req.onsuccess = function () { resolve(req.result ? req.result.value : (initValue || null)); };
req.onerror = function (e) { log('[indexDB][get]-error', 2, e); resolve(initValue || null); };
req.onsuccess = function () {
resolve(req.result ? req.result.value : initValue || null);
};
req.onerror = function (e) {
log("[indexDB][get]-error", 2, e);
resolve(initValue || null);
};
});
}
@@ -79,11 +99,16 @@ class IDBStorage {
var _this = this;
return new Promise(async function (resolve) {
var db = await _this.requestPromise;
var transaction = db.transaction(_this.storeName, 'readwrite');
var transaction = db.transaction(_this.storeName, "readwrite");
var objectStore = transaction.objectStore(_this.storeName);
var req = objectStore.put({ key: key, value: value });
req.onsuccess = function () { resolve(value); };
req.onerror = function (e) { log('[indexDB][set]-error', 2, e); resolve(value); };
req.onsuccess = function () {
resolve(value);
};
req.onerror = function (e) {
log("[indexDB][set]-error", 2, e);
resolve(value);
};
});
}
}
@@ -108,11 +133,12 @@ class ILocalStorage {
load() {
var _this = this;
Object.entries(localStorage).forEach(function (entry) {
var key = entry[0], value = entry[1];
var key = entry[0],
value = entry[1];
try {
_this.cache.set(key, JSON.parse(value));
} catch (error) {
log('[localStorage][load]-error', 2, error, key, value);
log("[localStorage][load]-error", 2, error, key, value);
}
});
}
@@ -125,9 +151,13 @@ class ILocalStorage {
* @returns {Promise<T>} 缓存的值或默认值
*/
get(key, initValue) {
return new Promise(function (resolve) {
resolve(this.cache.get(key) !== undefined ? this.cache.get(key) : initValue);
}.bind(this));
return new Promise(
function (resolve) {
resolve(
this.cache.get(key) !== undefined ? this.cache.get(key) : initValue,
);
}.bind(this),
);
}
/**
@@ -138,31 +168,159 @@ class ILocalStorage {
* @returns {Promise<T>} 返回写入的值
*/
set(key, value) {
return new Promise(function (resolve) {
var _value = isString(value) ? value : '';
return new Promise(
function (resolve) {
var _value = isString(value) ? value : "";
try {
_value = JSON.stringify(value);
} catch (e) {
log('[localStorage][set]-error', 2, e);
log("[localStorage][set]-error", 2, e);
}
localStorage.setItem(key, _value);
this.cache.set(key, _value);
resolve(value);
}.bind(this));
}.bind(this),
);
}
}
// 浏览器环境自动选择存储方式
if (typeof window !== 'undefined') {
/**
* 当前环境可用的存储类(IDBStorage 或 ILocalStorage
* @type {Function|null}
* @global
* 纯内存存储实现(无持久化,适用于所有环境兜底
*
* @class MemoryStorage
* @example
* const storage = new MemoryStorage();
* await storage.set('key', { data: 789 });
* await storage.get('key', null);
*/
window.IStorage = window.indexedDB ? IDBStorage : (window.localStorage ? ILocalStorage : null);
if (typeof module !== 'undefined') {
class MemoryStorage {
constructor() {
/** @type {Map<string, *>} 内存缓存 */
this.cache = new Map();
}
/**
* 从内存获取值
* @template T
* @param {string} key - 键名
* @param {T} initValue - 默认值
* @returns {Promise<T>} 缓存的值或默认值
*/
get(key, initValue) {
return Promise.resolve(
this.cache.has(key)
? this.cache.get(key)
: initValue !== undefined
? initValue
: null,
);
}
/**
* 写入内存
* @template T
* @param {string} key - 键名
* @param {T} value - 要存储的值
* @returns {Promise<T>} 返回写入的值
*/
set(key, value) {
this.cache.set(key, value);
return Promise.resolve(value);
}
}
/**
* Node.js SQLite 持久化存储实现
* 仅在 Node.js 环境下可用,浏览器/webpack 打包中为 null
*
* @class SqliteStorage
* @example
* const storage = new SqliteStorage('./data/storage.db');
* await storage.set('key', { data: 123 });
* await storage.get('key', null);
* @private
*/
var _SqliteStorageClass = function () {
var Database = require("better-sqlite3");
var pathModule = require("path");
var fs = require("fs");
return class SqliteStorage {
/**
* 创建 SQLite 存储实例
* @param {string} [dbPath='.windychen-storage/storage.db'] - 数据库文件路径
*/
constructor(dbPath) {
this.dbPath =
dbPath || pathModule.resolve(".windychen-storage", "storage.db");
var dir = pathModule.dirname(this.dbPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
this.db = new Database(this.dbPath);
this.db.pragma("journal_mode = WAL");
this.db.exec(
"CREATE TABLE IF NOT EXISTS kv_store (key TEXT PRIMARY KEY, value TEXT)",
);
}
/**
* 获取存储值
* @template T
* @param {string} key - 键名
* @param {T} initValue - 默认值(未找到时返回)
* @returns {Promise<T>} 存储的值或默认值
*/
async get(key, initValue) {
var row = this.db
.prepare("SELECT value FROM kv_store WHERE key = ?")
.get(key);
if (row) {
try {
return JSON.parse(row.value);
} catch (e) {
log("[SqliteStorage][get]-parse-error", 2, e);
}
}
return initValue !== undefined ? initValue : null;
}
/**
* 设置存储值
* @template T
* @param {string} key - 键名
* @param {T} value - 要存储的值
* @returns {Promise<T>} 返回写入的值
*/
async set(key, value) {
var json = JSON.stringify(value);
this.db
.prepare("INSERT OR REPLACE INTO kv_store (key, value) VALUES (?, ?)")
.run(key, json);
return value;
}
};
};
var SqliteStorage = null;
try {
SqliteStorage = _SqliteStorageClass();
} catch (e) {
// better-sqlite3 不可用(浏览器 / webpack 打包环境),SqliteStorage 保持为 null
}
// ===== 环境检测与自动导出 =====
if (typeof window !== "undefined") {
// 浏览器环境:优先 IndexedDB → localStorage → 内存
window.IStorage = window.indexedDB
? IDBStorage
: window.localStorage
? ILocalStorage
: MemoryStorage;
if (typeof module !== "undefined") {
module.exports = new window.IStorage();
}
} else if (typeof module !== 'undefined') {
module.exports = null;
} else if (typeof module !== "undefined") {
// Node.js 环境:SQLite 存储 → 内存
module.exports = SqliteStorage ? new SqliteStorage() : new MemoryStorage();
}
+66 -27
View File
@@ -1,4 +1,4 @@
require('./index');
require("./index");
let passed = 0;
let failed = 0;
@@ -18,50 +18,89 @@ function group(name) {
}
// ===== Object.getPro =====
group('Object.getPro');
const obj = { a: { b: { c: 1 } }, 'x.y': { z: 2 } };
assert("getPro('a.b.c') === 1", obj.getPro('a.b.c') === 1);
assert("getPro('a.b.d', 'default') === 'default'", obj.getPro('a.b.d', 'default') === 'default');
group("Object.getPro");
const obj = { a: { b: { c: 1 } }, "x.y": { z: 2 } };
assert("getPro('a.b.c') === 1", obj.getPro("a.b.c") === 1);
assert(
"getPro('a.b.d', 'default') === 'default'",
obj.getPro("a.b.d", "default") === "default",
);
assert("getPro(\"['x.y'].z\") === 2", obj.getPro("['x.y'].z") === 2);
assert("getPro('missing', 42) === 42", obj.getPro('missing', 42) === 42);
assert("getPro('missing', 42) === 42", obj.getPro("missing", 42) === 42);
// ===== Object.setPro =====
group('Object.setPro');
group("Object.setPro");
const obj2 = {};
obj2.setPro('foo.bar.baz', 'hello');
assert("setPro 嵌套赋值", obj2.foo.bar.baz === 'hello');
obj2.setPro("foo.bar.baz", "hello");
assert("setPro 嵌套赋值", obj2.foo.bar.baz === "hello");
obj2.setPro("['a.b'].c", 99);
assert("setPro 特殊键名", obj2['a.b'].c === 99);
assert("setPro 特殊键名", obj2["a.b"].c === 99);
// ===== Array Pro 方法 =====
const arr = [
{ user: { name: 'Alice' } },
{ user: { name: 'Bob' } },
{ user: { name: "Alice" } },
{ user: { name: "Bob" } },
null,
{ user: { name: 'Charlie' } },
{ user: { name: "Charlie" } },
];
group('Array.findPro');
group("Array.findPro");
assert("回调 findPro(t=>t>1)", [1, 2, 3].findPro((t) => t > 1) === 2);
assert("key-value findPro('user.name','Bob')", arr.findPro('user.name', 'Bob').user.name === 'Bob');
assert("单值 findPro('cc')", ['aa', 'bb', 'cc'].findPro('cc') === 'cc');
assert(
"key-value findPro('user.name','Bob')",
arr.findPro("user.name", "Bob").user.name === "Bob",
);
assert("单值 findPro('cc')", ["aa", "bb", "cc"].findPro("cc") === "cc");
group('Array.filterPro');
assert("key-value filterPro", arr.filterPro('user.name', 'Alice').length === 1);
group("Array.filterPro");
assert("key-value filterPro", arr.filterPro("user.name", "Alice").length === 1);
assert("单值 filterPro(2)", [1, 2, 2, 3].filterPro(2).length === 2);
group('Array.findIndexPro');
assert("findIndexPro", arr.findIndexPro('user.name', 'Charlie') === 3);
group("Array.findIndexPro");
assert("findIndexPro", arr.findIndexPro("user.name", "Charlie") === 3);
group('Array.findLastPro');
assert("findLastPro", arr.findLastPro('user.name', 'Bob').user.name === 'Bob');
group("Array.findLastPro");
assert("findLastPro", arr.findLastPro("user.name", "Bob").user.name === "Bob");
group('Array.somePro / everyPro');
assert("somePro", arr.somePro('user.name', 'Alice') === true);
assert("everyPro false", arr.everyPro('user.name', 'Alice') === false);
assert("everyPro true", ['a', 'a'].everyPro('a') === true);
group("Array.somePro / everyPro");
assert("somePro", arr.somePro("user.name", "Alice") === true);
assert("everyPro false", arr.everyPro("user.name", "Alice") === false);
assert("everyPro true", ["a", "a"].everyPro("a") === true);
// ===== StorageNode.js 环境:FileStorage =====
group("Storage");
var storage = require("./storage");
async function testStorage() {
await storage.set("testKey", { msg: "hello node" });
var val = await storage.get("testKey", null);
assert("storage.set/get (对象)", val && val.msg === "hello node");
await storage.set("testKey2", "plain string");
var val2 = await storage.get("testKey2", null);
assert("storage.set/get (字符串)", val2 === "plain string");
var val3 = await storage.get("nonExistent", "defaultVal");
assert("storage.get 默认值", val3 === "defaultVal");
await storage.set("testKeyNum", 42);
var val4 = await storage.get("testKeyNum", 0);
assert("storage.set/get (数字)", val4 === 42);
await storage.set("testKeyArr", [1, 2, 3]);
var val5 = await storage.get("testKeyArr", []);
assert("storage.set/get (数组)", Array.isArray(val5) && val5.length === 3);
// 汇总
console.log(`\n\x1b[33m共 ${passed + failed} 项,${passed} 通过,${failed} 失败\x1b[0m`);
console.log(
`\n\x1b[33m共 ${passed + failed} 项,${passed} 通过,${failed} 失败\x1b[0m`,
);
process.exit(failed > 0 ? 1 : 0);
}
testStorage().catch(function (e) {
console.log("\x1b[31mstorage test error:\x1b[0m", e);
process.exit(1);
});