LocalStorage 的 5MB 限制和同步阻塞特性简直是生产环境的定时炸弹。当你为 AI Prompt Manager 存储上万条带上下文的模板时,IndexedDB 是浏览器端唯一的“工业级”选择。
原生 IndexedDB 的 API 设计充满了 20 年前的“回调地狱”既视感。这一篇我们用现代 Promise 将其封装成一个像 Map 一样简单的工具类。
1. 为什么 LocalStorage 救不了你?
在处理万级数据时,两者的性能表现天差地别:
| 特性 | LocalStorage | IndexedDB |
|---|---|---|
| 容量 | ~5MB (固定) | 磁盘可用空间的 80% (海量) |
| 读写方式 | 同步 (阻塞主线程) | 异步 (不卡顿) |
| 数据结构 | 仅字符串 | 原生支持 JSON 对象、Blob |
| 搜索 | 全量遍历 | 支持索引 (Index) 极速查询 |
2. 极简封装代码:PromptDB
我们不需要引入像 Dexie 这样庞大的库,直接用 50 行代码搞定核心逻辑。
JavaScript
class PromptDB {
constructor(dbName = 'AIPromptDB', storeName = 'prompts') {
this.dbName = dbName;
this.storeName = storeName;
this.db = null;
}
// 1. 初始化数据库
async init() {
if (this.db) return;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
// 创建存储库,并以 id 作为主键
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
// 为模糊搜索创建索引
store.createIndex('title_idx', 'title', { unique: false });
}
};
request.onsuccess = (e) => {
this.db = e.target.result;
resolve();
};
request.onerror = reject;
});
}
// 2. 统一步骤:获取事务存储空间
_getStore(mode = 'readonly') {
const transaction = this.db.transaction(this.storeName, mode);
return transaction.objectStore(this.storeName);
}
// 3. 存/取 (就像操作对象一样简单)
async set(data) {
await this.init();
return new Promise((resolve) => {
const request = this._getStore('readwrite').put(data);
request.onsuccess = () => resolve(true);
});
}
async get(id) {
await this.init();
return new Promise((resolve) => {
const request = this._getStore().get(id);
request.onsuccess = () => resolve(request.result);
});
}
// 4. 获取全部数据 (性能关键:流式读取)
async getAll() {
await this.init();
return new Promise((resolve) => {
const request = this._getStore().getAll();
request.onsuccess = () => resolve(request.result || []);
});
}
}
3. 针对 AI 业务的 4 个高阶用法
① 像 JSON 一样存取万级模板
JavaScript
const db = new PromptDB();
// 直接存入一个复杂的 AI 上下文对象,无需 JSON.stringify
await db.set({
id: 'p_001',
title: '金融报告分析师',
content: '你是一个资深审计师...',
tags: ['finance', 'audit'],
updatedAt: Date.now()
});
② 批量导入(防止事务频繁开关)
当你有 1000 条 Prompt 需要初始化时,合并到一个事务里:
JavaScript
async batchSet(list) {
const store = this._getStore('readwrite');
list.forEach(item => store.put(item));
}
③ 本地模糊搜索(利用 Index)
利用我们之前创建的 title_idx 索引,避开全量扫描:
JavaScript
async findByTitle(keyword) {
const index = this._getStore().index('title_idx');
// 仅演示逻辑:实际可用 IDBKeyRange 进行范围匹配
const request = index.getAll();
return new Promise(resolve => {
request.onsuccess = () => {
const result = request.result.filter(i => i.title.includes(keyword));
resolve(result);
};
});
}
④ 存储配额监控
作为资深开发,你应该让用户知道硬盘快满了:
JavaScript
const checkStorage = async () => {
if (navigator.storage && navigator.storage.estimate) {
const { usage, quota } = await navigator.storage.estimate();
console.log(`已用: ${(usage / 1024 / 1024).toFixed(2)}MB`);
console.log(`剩余: ${(quota / 1024 / 1024 / 1024).toFixed(2)}GB`);
}
};
4.总结
- 版本管理:修改
createObjectStore逻辑时,记得提升indexedDB.open的版本号,否则onupgradeneeded不会触发。 - 闭包与内存:虽然 IndexedDB 存数据不占内存,但
getAll()拿出来的 1 万条数据会占用 JS 堆内存。大数据量建议使用 Cursor(游标) 配合之前聊过的scheduler.yield分片处理。 - 无痕模式:部分浏览器(如老版本 Safari)在无痕模式下会禁用 IndexedDB,记得加一层
try-catch降级到内存存储。