feat-ai(storage.js): 添加nodejs环境下的sqlite存储支持,调用方法与浏览器环境下保存一致
This commit is contained in:
+10
-10
@@ -2,27 +2,27 @@ name: Node.js Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "main", "master" ]
|
branches: ["main", "master"]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main", "master" ]
|
branches: ["main", "master"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: docker # 匹配你 runner 的标签
|
runs-on: docker # 匹配你 runner 的标签
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- 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
|
- name: Install dependencies
|
||||||
run: npm i # 或 npm install
|
run: npm i --registry http://verdaccio:4873/ # 或 npm install
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: npm 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: Publish package
|
- name: Publish package
|
||||||
run: npm publish
|
run: npm publish
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
# Build output
|
# Build output
|
||||||
dist/
|
dist/
|
||||||
|
|
||||||
|
# SQLite database files
|
||||||
|
*.db
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|||||||
@@ -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. SqliteStorage(Node.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 REPLACE,value 经 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.js:SqliteStorage → 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 自动化构建与发布
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
require('./object');
|
require("./object");
|
||||||
require('./array');
|
require("./array");
|
||||||
|
require("./storage");
|
||||||
try { require('./storage'); } catch (e) {} // 仅浏览器环境生效
|
|
||||||
|
|||||||
+4
-1
@@ -18,5 +18,8 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"webpack": "^5.90.0",
|
"webpack": "^5.90.0",
|
||||||
"webpack-cli": "^5.1.4"
|
"webpack-cli": "^5.1.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"better-sqlite3": "^12.10.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+204
-46
@@ -7,7 +7,9 @@ var log = console.warn;
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
var isString = function (str) { return Object.getTypeString(str) === 'String'; };
|
var isString = function (str) {
|
||||||
|
return Object.getTypeString(str) === "String";
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* IndexedDB 存储实现(大容量存储优先选择)
|
* IndexedDB 存储实现(大容量存储优先选择)
|
||||||
@@ -29,24 +31,37 @@ class IDBStorage {
|
|||||||
/** @type {number} 数据库版本号 */
|
/** @type {number} 数据库版本号 */
|
||||||
this.version = version || 1;
|
this.version = version || 1;
|
||||||
/** @type {string} 数据库名称 */
|
/** @type {string} 数据库名称 */
|
||||||
this.dbName = dbName || 'dbName';
|
this.dbName = dbName || "dbName";
|
||||||
/** @type {string} 对象存储名称 */
|
/** @type {string} 对象存储名称 */
|
||||||
this.storeName = storeName || 'storeName';
|
this.storeName = storeName || "storeName";
|
||||||
|
|
||||||
var request = indexedDB.open(this.dbName, this.version);
|
var request = indexedDB.open(this.dbName, this.version);
|
||||||
/** @type {Promise<IDBDatabase>} 数据库连接 Promise */
|
/** @type {Promise<IDBDatabase>} 数据库连接 Promise */
|
||||||
this.requestPromise = new Promise(function (resolve, reject) {
|
this.requestPromise = new Promise(
|
||||||
request.onupgradeneeded = function (e) {
|
function (resolve, reject) {
|
||||||
if (e && e.target && !request.result.objectStoreNames.contains(this.storeName)) {
|
request.onupgradeneeded = function (e) {
|
||||||
request.result.createObjectStore(this.storeName, { keyPath: 'key' })
|
if (
|
||||||
.createIndex('key', 'key', { unique: false });
|
e &&
|
||||||
}
|
e.target &&
|
||||||
log('[Index][open]-upgrade');
|
!request.result.objectStoreNames.contains(this.storeName)
|
||||||
resolve(request.result);
|
) {
|
||||||
}.bind(this);
|
request.result
|
||||||
request.onsuccess = function () { log('[IndexDB][open]-success'); resolve(request.result); };
|
.createObjectStore(this.storeName, { keyPath: "key" })
|
||||||
request.onerror = function (e) { log('[IndexDB][open]-error', 2); reject(e); };
|
.createIndex("key", "key", { unique: false });
|
||||||
}.bind(this));
|
}
|
||||||
|
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),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,11 +75,16 @@ class IDBStorage {
|
|||||||
var _this = this;
|
var _this = this;
|
||||||
return new Promise(async function (resolve) {
|
return new Promise(async function (resolve) {
|
||||||
var db = await _this.requestPromise;
|
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 objectStore = transaction.objectStore(_this.storeName);
|
||||||
var req = objectStore.get(key);
|
var req = objectStore.get(key);
|
||||||
req.onsuccess = function () { resolve(req.result ? req.result.value : (initValue || null)); };
|
req.onsuccess = function () {
|
||||||
req.onerror = function (e) { log('[indexDB][get]-error', 2, e); resolve(initValue || null); };
|
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;
|
var _this = this;
|
||||||
return new Promise(async function (resolve) {
|
return new Promise(async function (resolve) {
|
||||||
var db = await _this.requestPromise;
|
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 objectStore = transaction.objectStore(_this.storeName);
|
||||||
var req = objectStore.put({ key: key, value: value });
|
var req = objectStore.put({ key: key, value: value });
|
||||||
req.onsuccess = function () { resolve(value); };
|
req.onsuccess = function () {
|
||||||
req.onerror = function (e) { log('[indexDB][set]-error', 2, e); resolve(value); };
|
resolve(value);
|
||||||
|
};
|
||||||
|
req.onerror = function (e) {
|
||||||
|
log("[indexDB][set]-error", 2, e);
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,11 +133,12 @@ class ILocalStorage {
|
|||||||
load() {
|
load() {
|
||||||
var _this = this;
|
var _this = this;
|
||||||
Object.entries(localStorage).forEach(function (entry) {
|
Object.entries(localStorage).forEach(function (entry) {
|
||||||
var key = entry[0], value = entry[1];
|
var key = entry[0],
|
||||||
|
value = entry[1];
|
||||||
try {
|
try {
|
||||||
_this.cache.set(key, JSON.parse(value));
|
_this.cache.set(key, JSON.parse(value));
|
||||||
} catch (error) {
|
} 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>} 缓存的值或默认值
|
* @returns {Promise<T>} 缓存的值或默认值
|
||||||
*/
|
*/
|
||||||
get(key, initValue) {
|
get(key, initValue) {
|
||||||
return new Promise(function (resolve) {
|
return new Promise(
|
||||||
resolve(this.cache.get(key) !== undefined ? this.cache.get(key) : initValue);
|
function (resolve) {
|
||||||
}.bind(this));
|
resolve(
|
||||||
|
this.cache.get(key) !== undefined ? this.cache.get(key) : initValue,
|
||||||
|
);
|
||||||
|
}.bind(this),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,31 +168,159 @@ class ILocalStorage {
|
|||||||
* @returns {Promise<T>} 返回写入的值
|
* @returns {Promise<T>} 返回写入的值
|
||||||
*/
|
*/
|
||||||
set(key, value) {
|
set(key, value) {
|
||||||
return new Promise(function (resolve) {
|
return new Promise(
|
||||||
var _value = isString(value) ? value : '';
|
function (resolve) {
|
||||||
try {
|
var _value = isString(value) ? value : "";
|
||||||
_value = JSON.stringify(value);
|
try {
|
||||||
} catch (e) {
|
_value = JSON.stringify(value);
|
||||||
log('[localStorage][set]-error', 2, e);
|
} catch (e) {
|
||||||
}
|
log("[localStorage][set]-error", 2, e);
|
||||||
localStorage.setItem(key, _value);
|
}
|
||||||
this.cache.set(key, _value);
|
localStorage.setItem(key, _value);
|
||||||
resolve(value);
|
this.cache.set(key, _value);
|
||||||
}.bind(this));
|
resolve(value);
|
||||||
|
}.bind(this),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 浏览器环境自动选择存储方式
|
/**
|
||||||
if (typeof window !== 'undefined') {
|
* 纯内存存储实现(无持久化,适用于所有环境兜底)
|
||||||
|
*
|
||||||
|
* @class MemoryStorage
|
||||||
|
* @example
|
||||||
|
* const storage = new MemoryStorage();
|
||||||
|
* await storage.set('key', { data: 789 });
|
||||||
|
* await storage.get('key', null);
|
||||||
|
*/
|
||||||
|
class MemoryStorage {
|
||||||
|
constructor() {
|
||||||
|
/** @type {Map<string, *>} 内存缓存 */
|
||||||
|
this.cache = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前环境可用的存储类(IDBStorage 或 ILocalStorage)
|
* 从内存获取值
|
||||||
* @type {Function|null}
|
* @template T
|
||||||
* @global
|
* @param {string} key - 键名
|
||||||
|
* @param {T} initValue - 默认值
|
||||||
|
* @returns {Promise<T>} 缓存的值或默认值
|
||||||
*/
|
*/
|
||||||
window.IStorage = window.indexedDB ? IDBStorage : (window.localStorage ? ILocalStorage : null);
|
get(key, initValue) {
|
||||||
if (typeof module !== 'undefined') {
|
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();
|
module.exports = new window.IStorage();
|
||||||
}
|
}
|
||||||
} else if (typeof module !== 'undefined') {
|
} else if (typeof module !== "undefined") {
|
||||||
module.exports = null;
|
// Node.js 环境:SQLite 存储 → 内存
|
||||||
|
module.exports = SqliteStorage ? new SqliteStorage() : new MemoryStorage();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
require('./index');
|
require("./index");
|
||||||
|
|
||||||
let passed = 0;
|
let passed = 0;
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
@@ -18,50 +18,89 @@ function group(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== Object.getPro =====
|
// ===== Object.getPro =====
|
||||||
group('Object.getPro');
|
group("Object.getPro");
|
||||||
const obj = { a: { b: { c: 1 } }, 'x.y': { z: 2 } };
|
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.c') === 1", obj.getPro("a.b.c") === 1);
|
||||||
assert("getPro('a.b.d', 'default') === 'default'", obj.getPro('a.b.d', 'default') === 'default');
|
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(\"['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 =====
|
// ===== Object.setPro =====
|
||||||
group('Object.setPro');
|
group("Object.setPro");
|
||||||
const obj2 = {};
|
const obj2 = {};
|
||||||
obj2.setPro('foo.bar.baz', 'hello');
|
obj2.setPro("foo.bar.baz", "hello");
|
||||||
assert("setPro 嵌套赋值", obj2.foo.bar.baz === 'hello');
|
assert("setPro 嵌套赋值", obj2.foo.bar.baz === "hello");
|
||||||
obj2.setPro("['a.b'].c", 99);
|
obj2.setPro("['a.b'].c", 99);
|
||||||
assert("setPro 特殊键名", obj2['a.b'].c === 99);
|
assert("setPro 特殊键名", obj2["a.b"].c === 99);
|
||||||
|
|
||||||
// ===== Array Pro 方法 =====
|
// ===== Array Pro 方法 =====
|
||||||
const arr = [
|
const arr = [
|
||||||
{ user: { name: 'Alice' } },
|
{ user: { name: "Alice" } },
|
||||||
{ user: { name: 'Bob' } },
|
{ user: { name: "Bob" } },
|
||||||
null,
|
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("回调 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(
|
||||||
assert("单值 findPro('cc')", ['aa', 'bb', 'cc'].findPro('cc') === 'cc');
|
"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');
|
group("Array.filterPro");
|
||||||
assert("key-value filterPro", arr.filterPro('user.name', 'Alice').length === 1);
|
assert("key-value filterPro", arr.filterPro("user.name", "Alice").length === 1);
|
||||||
assert("单值 filterPro(2)", [1, 2, 2, 3].filterPro(2).length === 2);
|
assert("单值 filterPro(2)", [1, 2, 2, 3].filterPro(2).length === 2);
|
||||||
|
|
||||||
group('Array.findIndexPro');
|
group("Array.findIndexPro");
|
||||||
assert("findIndexPro", arr.findIndexPro('user.name', 'Charlie') === 3);
|
assert("findIndexPro", arr.findIndexPro("user.name", "Charlie") === 3);
|
||||||
|
|
||||||
group('Array.findLastPro');
|
group("Array.findLastPro");
|
||||||
assert("findLastPro", arr.findLastPro('user.name', 'Bob').user.name === 'Bob');
|
assert("findLastPro", arr.findLastPro("user.name", "Bob").user.name === "Bob");
|
||||||
|
|
||||||
group('Array.somePro / everyPro');
|
group("Array.somePro / everyPro");
|
||||||
assert("somePro", arr.somePro('user.name', 'Alice') === true);
|
assert("somePro", arr.somePro("user.name", "Alice") === true);
|
||||||
assert("everyPro false", arr.everyPro('user.name', 'Alice') === false);
|
assert("everyPro false", arr.everyPro("user.name", "Alice") === false);
|
||||||
assert("everyPro true", ['a', 'a'].everyPro('a') === true);
|
assert("everyPro true", ["a", "a"].everyPro("a") === true);
|
||||||
|
|
||||||
// 汇总
|
// ===== Storage(Node.js 环境:FileStorage) =====
|
||||||
console.log(`\n\x1b[33m共 ${passed + failed} 项,${passed} 通过,${failed} 失败\x1b[0m`);
|
group("Storage");
|
||||||
|
|
||||||
process.exit(failed > 0 ? 1 : 0);
|
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`,
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
testStorage().catch(function (e) {
|
||||||
|
console.log("\x1b[31mstorage test error:\x1b[0m", e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user