Skip to content
  • LocalForage 单键值对存储(兼容优先)
  • Dexie.js 复杂查询 + 事务(纯客户端)
  • PouchDB离线 + 服务器同步
  • Lovefield关系型数据 + SQL 习惯
  • Watermelon DB React 生态 + 高性能
  • idb 轻量 + 接近原生控制

现状:Dexie.js 为主流,PouchDB 场景受限

根据国内技术社区统计,在需要复杂本地存储的项目中,Dexie.js 的使用率超过 60%,尤其在医疗、教育等行业。

受限于同步复杂性和国内云服务的竞争,PouchDB 的使用率不足 15%,更多作为特定场景的补充方案。

Dexie.js

Dexie.js 是一个基于 Promise 的库,用于管理 IndexedDB 数据库。它提供了一个简单、易用且强大的 API,适合存储大量结构化数据(如缓存、用户数据、离线数据等),支持事务管理和复杂查询。

安装

bash
npm install dexie
html
<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 语句、事务、索引,适合大量结构化数据;劣势:需额外配置插件,体积较大。