LocalForage单键值对存储(兼容优先)Dexie.js复杂查询 + 事务(纯客户端)PouchDB离线 + 服务器同步Lovefield关系型数据 + SQL 习惯Watermelon DBReact 生态 + 高性能idb轻量 + 接近原生控制
现状:Dexie.js 为主流,PouchDB 场景受限
根据国内技术社区统计,在需要复杂本地存储的项目中,Dexie.js 的使用率超过 60%,尤其在医疗、教育等行业。
受限于同步复杂性和国内云服务的竞争,PouchDB 的使用率不足 15%,更多作为特定场景的补充方案。
Dexie.js
Dexie.js 是一个基于 Promise 的库,用于管理 IndexedDB 数据库。它提供了一个简单、易用且强大的 API,适合存储大量结构化数据(如缓存、用户数据、离线数据等),支持事务管理和复杂查询。
安装
bash
npm install dexiehtml
<script src="https://cdn.jsdelivr.net/npm/dexie/dist/dexie.min.js"></script>基本使用
js
import Dexie from "dexie";
// 创建数据库实例
const db = new Dexie("MyDatabase");
// 定义数据库表和索引
db.version(1).stores({
friends: "++id, name, age, email", // 主键 id,自增,索引 name, age 和 email
});
// 插入数据
async function addFriend() {
await db.friends.add({
name: "张三",
age: 25,
email: "zs@qq.com",
});
}
// 查询数据
async function getFriends() {
const friends = await db.friends.toArray();
}
// 更新数据
async function updateFriend(id) {
await db.friends.update(id, { age: 26 });
}
// 删除数据
async function deleteFriend(id) {
await db.friends.delete(id);
}复杂查询
js
// 范围查询
const res = await db.friends.where("age").below(30).toArray();
// 多条件查询
const res = await db.friends.where({ name: "张三", age: 25 }).first();
// 使用索引查询
const res = await db.friends.where("email").equals(email).first();事务支持
js
async function transactionExample() {
await db.transaction("rw", db.friends, async () => {
await db.friends.add({ name: "里斯", age: 30 });
await db.friends.add({ name: "王五", age: 28 });
});
}版本升级
当需要修改数据库结构时,只需:
增加DB_VERSION版本号
使用this.version(newVersion).stores(...)定义新的表结构
通过.upgrade()方法处理数据迁移逻辑
当用户打开应用时,Dexie 会自动检测版本变化并执行升级操作,无需手动处理。
详情
js
// 版本1: 初始版本
this.version(1).stores({
users: '++id, username, email, createdAt',
products: '++id, name, price, category',
settings: 'key, value'
});
// 版本2: 添加新表和字段
this.version(2).stores({
users: '++id, username, email, avatar, createdAt', // 添加avatar字段
products: '++id, name, price, category, stock', // 添加stock字段
settings: 'key, value',
orders: '++id, userId, productIds, total, status, createdAt' // 新增orders表
}).upgrade(tx => {
// 升级逻辑:为现有用户添加默认头像
return tx.table('users').toArray().then(users => {
return Promise.all(users.map(user => {
if (!user.avatar) {
user.avatar = 'default-avatar.png';
return tx.table('users').put(user);
}
return Promise.resolve();
}));
});
});
// 版本3: 索引优化
this.version(3).stores({
users: '++id, username, email, avatar, createdAt',
products: '++id, name, price, category, stock, [category+price]', // 添加复合索引
settings: 'key, value',
orders: '++id, userId, productIds, total, status, createdAt, &orderNumber' // 添加唯一索引orderNumber
}).upgrade(tx => {
// 为订单添加orderNumber字段
return tx.table('orders').toArray().then(orders => {
return Promise.all(orders.map((order, index) => {
order.orderNumber = `ORD-${Date.now()}-${index}`;
return tx.table('orders').put(order);
}));
});
});数据库事件 监听
js
db.friends.hook("creating", (primKey, obj, transaction) => {
console.log("Creating new friend:", obj);
});Dexie 封装示例
db.js
js
import Dexie from 'dexie';
// 数据库名称和当前版本
const DB_NAME = 'ReaderAppDatabase';
const DB_VERSION = 1; // 版本号,升级时递增
// 单例实例
let dbInstance = null;
/**
* 数据库类,继承自Dexie
*/
class AppDatabase extends Dexie {
constructor() {
super(DB_NAME);
// 定义数据库结构和版本管理
// 版本1: 初始版本
this.version(1).stores({
books: '++id, title, author, info, type, coverUrl, chapters, fileSize, addedAt',
readerSettings: '&bookId, fontSize, lineHeight, theme, fontFamily, pageTurnMode, updatedAt',
readingHistory: '&bookId, chapterIndex, scrollTop, percent, updatedAt'
}).upgrade(tx => {
// 版本升级时的操作
// return tx.table('orders').toArray().then(orders => {
// return Promise.all(orders.map((order, index) => {
// order.orderNumber = `ORD-${Date.now()}-${index}`;
// return tx.table('orders').put(order);
// }));
// });
});
// 注册表格,方便类型提示和直接访问
this.books = this.table('books');
this.readerSettings = this.table('readerSettings');
this.readingHistory = this.table('readingHistory');
// 监听数据库事件
this.on('ready', () => {
console.log('数据库准备就绪');
});
this.on('versionchange', (event) => {
console.log(`数据库版本将变更: ${event.oldVersion} -> ${event.newVersion}`);
// 可以在这里处理版本变更时的逻辑
});
// // 这里监听error会报错
// this.on('error', (error) => {
// console.error('数据库错误:', error);
// });
}
// ------------------------------
// 通用CRUD方法
// ------------------------------
/**
* 插入数据
* @param {string} table 表名
* @param {object|object[]} data 要插入的数据
* @returns {Promise}
*/
insert(table, data) {
if (!this[table]) {
return Promise.reject(new Error(`表${table}不存在`));
}
if (data) {
return Array.isArray(data)
? this[table].bulkAdd(data)
: this[table].put(data);
}
}
/**
* 查询数据
* @param {string} table 表名
* @param {object} query 查询条件
* @param {object} options 选项 {limit, offset, orderBy, desc}
* @returns {Promise}
*/
query(table, query = {}, options = {}) {
if (!this[table]) {
return Promise.reject(new Error(`表${table}不存在`));
}
let collection = this[table];
// 应用查询条件
Object.entries(query).forEach(([key, value]) => {
collection = collection.where(key).equals(value);
});
// 应用排序
if (options.orderBy) {
collection = options.desc
? collection.orderBy(options.orderBy).reverse()
: collection.orderBy(options.orderBy);
}
// 应用分页
if (options.offset) {
collection = collection.offset(options.offset);
}
if (options.limit) {
collection = collection.limit(options.limit);
}
return collection.toArray();
}
/**
* 根据ID获取数据
* @param {string} table 表名
* @param {number|string} id 记录ID
* @returns {Promise}
*/
getById(table, id) {
if (!this[table]) {
return Promise.reject(new Error(`表${table}不存在`));
}
return this[table].get(id);
}
/**
* 更新数据
* @param {string} table 表名
* @param {object} data 要更新的数据(必须包含id)
* @returns {Promise}
*/
update(table, data) {
if (!this[table]) {
return Promise.reject(new Error(`表${table}不存在`));
}
// if (!data.id) {
// return Promise.reject(new Error('更新数据必须包含id'));
// }
return this[table].put(data);
}
/**
* 批量更新数据
* @param {string} table 表名
* @param {object[]} data 要更新的数据数组(每个对象必须包含id)
* @returns {Promise}
*/
bulkUpdate(table, data) {
if (!this[table]) {
return Promise.reject(new Error(`表${table}不存在`));
}
const invalidItems = data.filter(item => !item.id);
if (invalidItems.length > 0) {
return Promise.reject(new Error('更新数据必须包含id'));
}
return this[table].bulkPut(data);
}
/**
* 智能删除数据
* 支持:
* - delete(table, primaryKeyValue) → 按主键删除
* - delete(table, { field: value }) → 按条件删除
* @param {string} table 表名
* @param {number|string|object} keyOrQuery 主键值 或 查询对象
* @returns {Promise}
*/
delete(table, keyOrQuery) {
if (!this[table]) {
return Promise.reject(new Error(`表 ${table} 不存在`));
}
try {
// 情况1:传的是基本类型(数字、字符串)→ 按主键删除
if (['number', 'string'].includes(typeof keyOrQuery)) {
const tableSchema = this.table(table).schema;
const primaryKey = tableSchema.primKey.name;
if (!primaryKey) {
return Promise.reject(new Error(`表 ${table} 没有定义主键`));
}
return this[table].where(primaryKey).equals(keyOrQuery).delete();
}
// 情况2:传的是对象 → 按条件删除(支持多字段)
else if (typeof keyOrQuery === 'object' && keyOrQuery !== null) {
let query = this[table];
// 遍历对象每个字段,构建 where 查询
Object.entries(keyOrQuery).forEach(([key, value]) => {
query = query.where(key).equals(value);
});
return query.delete();
}
// 其他情况
else {
return Promise.reject(new Error(`不支持的删除键类型: ${typeof keyOrQuery}`));
}
} catch (error) {
return Promise.reject(new Error(`删除数据失败: ${error.message}`));
}
}
/**
* 批量删除数据
* @param {string} table 表名
* @param {number[]|string[]} ids 记录ID数组
* @returns {Promise}
*/
bulkDelete(table, ids) {
if (!this[table]) {
return Promise.reject(new Error(`表${table}不存在`));
}
return this[table].bulkDelete(ids);
}
/**
* 清空表
* @param {string} table 表名
* @returns {Promise}
*/
clearTable(table) {
if (!this[table]) {
return Promise.reject(new Error(`表${table}不存在`));
}
return this[table].clear();
}
/**
* 获取表记录总数
* @param {string} table 表名
* @param {object} query 查询条件(可选)
* @returns {Promise}
*/
count(table, query = {}) {
if (!this[table]) {
return Promise.reject(new Error(`表${table}不存在`));
}
let collection = this[table];
// 应用查询条件
Object.entries(query).forEach(([key, value]) => {
collection = collection.where(key).equals(value);
});
return collection.count();
}
// ------------------------------
// 表特定的快捷方法(可选)
// ------------------------------
/**
* 获取用户设置
* @param {string} key 设置键
* @returns {Promise}
*/
getSetting(key) {
return this.settings.get(key).then(setting => setting?.value);
}
/**
* 保存用户设置
* @param {string} key 设置键
* @param {any} value 设置值
* @returns {Promise}
*/
saveSetting(key, value) {
return this.settings.put({ key, value });
}
/**
* 根据类别获取产品
* @param {string} category 产品类别
* @returns {Promise}
*/
getProductsByCategory(category) {
return this.products.where('category').equals(category).toArray();
}
}
/**
* 获取数据库单例实例
* @returns {AppDatabase} 数据库实例
*/
export function getDB() {
if (!dbInstance) {
dbInstance = new AppDatabase();
}
return dbInstance;
}
// 默认导出单例
export default getDB();使用示例
js
import db from './db.js';
// 添加数据
async function addUser() {
try {
const userId = await db.insert('users', {
username: 'john_doe',
email: 'john@example.com',
createdAt: new Date()
});
console.log('新增用户ID:', userId);
} catch (error) {
console.error('添加用户失败:', error);
}
}
// 查询数据
async function getUsers() {
try {
const users = await db.query('users',
{ category: 'admin' }, // 查询条件
{ orderBy: 'createdAt', desc: true, limit: 10 } // 选项
);
console.log('查询到的用户:', users);
} catch (error) {
console.error('查询用户失败:', error);
}
}
// 更新数据
async function updateUser(userId) {
try {
await db.update('users', {
id: userId,
username: 'john_updated',
// 其他要更新的字段
});
} catch (error) {
console.error('更新用户失败:', error);
}
}
// 删除数据
async function deleteUser(userId) {
try {
await db.delete('users', userId);
} catch (error) {
console.error('删除用户失败:', error);
}
}
// 使用表特定快捷方法
// 保存和获取设置
async function saveAndGetSetting() {
await db.saveSetting('theme', 'dark');
const theme = await db.getSetting('theme');
console.log('当前主题:', theme);
}微信小程序
miniprogram-db模拟关系型数据库的基本功能,支持表、字段、简单查询。wx-db封装了小程序存储 API,提供更友好的链式查询语法(类似 Dexie.js)。
微信小程序支持引入 wxsqlite3 插件(需在小程序后台配置),实现本地 SQLite 数据库操作,功能接近传统数据库。
优势:支持 SQL 语句、事务、索引,适合大量结构化数据;劣势:需额外配置插件,体积较大。