IndexedDB使用摘要

一. 前言

JavaScript进阶指南 | Lian中简单使用了一下indexeddb, 这里重新记录一下使用中的一些细节.

相关内容参考:

IndexedDB - Web API | MDN

浏览器模型 - IndexedDB API - < 阮一峰 JavaScript 教程> - 书栈网 - BookStack

如何把indexedDB简单封装成"localStorage"本文把indexedDB简单封装成"localStorag - 掘金

如何优雅的封装indexedDB如何优雅的封装indexedDB? 调用方如何安全放心的操作indexedDB数据库? 如何 - 掘金

总体而言, indexeddb的使用不是很麻烦, 就是所有的操作都是异步和常用的MySQL等数据库在使用上比较反习惯.

由于上面的几个参考将大部分内容讲解很详细, 下面将主要集中在一些细节的使用上.

1.1 基则

需要知道两个基本准则:

  • 绝大部分操作都是异步的
  • 所有读写操作都是在事务下完成的, 对包括读也需要在事务下完成.

二. 细节

2.1 onupgradeneeded

事件中需要比较注意的项是: onupgradeneeded

创建/删除表, 修改索引等改动表/数据库结构的操作, 必须在这个事件触发之下才能完成操作.

{
    // 不需要额外创建数据库, .假如不存在则自动创建
    const db = indexedDB.open('test');

    db.onupgradeneeded = (e) => {
        const db_instance = e.target.result;
        const a = db_instance.createObjectStore('test_a', { keyPath: 'mid' });
        a.transaction.oncomplete = () => {
            console.log('a');
        };
        console.log('---');
        const b = db_instance.createObjectStore('test_b', { keyPath: 'vid' });
        const d = new Promise((resolve) => b.transaction.oncomplete = () => {
            console.log('b');
            resolve();
        });
        console.log('---');
        d.then(() => console.log('c'))
    };
    // 上面的事件优先于本事件
    db.onsuccess = () => ...
}

这个事件的触发条件:

  • 第一次创建数据库时, 这点很好理解, 就是第一次创建

  • 当打开的数据库版本大于当前数据库版本

     var request = window.indexedDB.open(databaseName, version);
    

    version版本控制, 如创建了数据库之后, 还需要在下一次继续创建新表, 就可以在获取了原有数据库的版本号之后, 关闭数据库, 重新以高版本号重新打开数据库, 从而触发这个事件

    const v = this.#db_instance.version + 1;
    this.#db_instance.close();
    this.#db_instance = indexedDB.open(this.#db_name, v);
    return this.initialize();
    

2.2 onclose

这个事件需要注意, 并不是用在数据库正常关闭的时候触发, 而是异常关闭的时候触发, 如数据库被手动删掉,或者其他一些原因导致页面崩溃导致的数据库关闭.

2.3 事务

事务需要注意执行的先后顺序, 以及oncomplete这个事件.

var trans1 = db.transaction('foo', 'readwrite');
var trans2 = db.transaction('foo', 'readwrite');
var objectStore2 = trans2.objectStore('foo')
var objectStore1 = trans1.objectStore('foo')
objectStore2.put('2', 'key');
objectStore1.put('1', 'key');

事务在前的先执行, 而不是具体的执行动作的先后.

// 打开数据库( 如果数据库不存在, 则创建它) 
var request = indexedDB.open('myDatabase', 1);

// 处理数据库版本升级
request.onupgradeneeded = function (event) {
    var db = event.target.result;
    // 如果对象存储不存在, 则创建它
    console.log('ok update');
    if (!db.objectStoreNames.contains('myObjectStore')) {
        db.createObjectStore('myObjectStore', { keyPath: 'id' });
    }
};

// 处理数据库成功打开事件
request.onsuccess = function (event) {
    var db = event.target.result;

    // 开始一个事务
    var transaction = db.transaction(['myObjectStore'], 'readwrite');

    // 获取对象存储
    var objectStore = transaction.objectStore('myObjectStore');

    // 要插入的数据
    var dataToInsert = [
        { id: 1, name: 'Alice', age: 25 },
        { id: 2, name: 'Bob', age: 30 },
        { id: 1, name: 'Charlie', age: 35 },
        // 可以继续添加更多数据
    ];

    // 批量插入数据
    dataToInsert.forEach(function (item) {
        var request = objectStore.put(item);
        request.onerror = function (event) {
            console.error('Error adding item:', event.target.error);
        };
        request.onsuccess = function (event) {
            console.log('Item added successfully.');
        };
    });

    // 处理事务完成事件
    transaction.oncomplete = function (event) {
        console.log('All items added successfully.');
    };

    const r = objectStore.get(1);
    r.onsuccess = function (event) {
        console.log(event.target.result);
    };

    // 处理事务错误事件
    transaction.onerror = function (event) {
        console.error('Transaction error:', event.target.error);
    };
};

// 处理数据库打开错误事件
request.onerror = function (event) {
    console.error('Database error:', event.target.error);
};

在很多代码中较少使用的oncomplete事件, 这个事件的触发表示, 所执行的动作皆成功完成.

#create_tables() {
    // transaction中只有最后的事务执行完毕才会执行oncomplete回调函数, 而不是每个事务(创建表)执行完毕时执行oncomplete回调函数
    return this.#transaction_wrapper(this.#table_arr.map(e => {
        const keypath = e.key_path;
        return this.#db_instance.createObjectStore(e.table_name, keypath ? { keyPath: keypath } : { autoIncrement: true });
    }).pop().transaction);
}

在创建表的时候, 也可以通过表对象获得transaction对象, 假如创建多个表, 也只会触发一次该事件而不是多次执行回调.

2.4 add/put的差异

添加数据的时候有两种方法/可选

// 要插入的数据
var dataToInsert = [
    { id: 1, name: 'Alice', age: 25 },
    { id: 2, name: 'Bob', age: 30 },
    { id: 1, name: 'Charlie', age: 35 }, // { id: 1, name: 'Charlie', age: 35, email: 'charlie@example.com' },
    // 可以继续添加更多数据
];

// 批量插入数据
dataToInsert.forEach(function (item) {
    var request = objectStore.put(item);
    request.onerror = function (event) {
        console.error('Error adding item:', event.target.error);
    };
    request.onsuccess = function (event) {
        console.log('Item added successfully.');
    };
});

由于indexeddb并没有集成封装批量插入数据的方法, 要逐个添加.

add, 当数据存在时会直接报错.

put, 当数据存在时, 会自动更新数据, 不需要考虑前后两种数据的结构是否完全一致, 新的数据会直接覆盖旧的数据.

2.5 索引

  • 加快检索的速度
  • 多条数据检索的返回
// 数据库名称
const dbName = "myDatabase";
// 对象存储区名称
const objectStoreName = "customers";

// 打开数据库
const request = indexedDB.open(dbName, 1);

request.onupgradeneeded = function(event) {
    const db = event.target.result;

    // 如果对象存储区不存在, 则创建它
    if (!db.objectStoreNames.contains(objectStoreName)) {
        const objectStore = db.createObjectStore(objectStoreName, { keyPath: "id", autoIncrement: true });

        // 创建索引
        objectStore.createIndex("name", "name", { unique: false });
        objectStore.createIndex("age", "age", { unique: false });
    }
};

request.onsuccess = function(event) {
    const db = event.target.result;

    // 添加数据到对象存储区
    const transaction = db.transaction([objectStoreName], "readwrite");
    const objectStore = transaction.objectStore(objectStoreName);

    const customers = [
        { name: "Alice", age: 30 },
        { name: "Bob", age: 25 },
        { name: "Charlie", age: 35 },
        { name: "David", age: 28 }
    ];

    customers.forEach(customer => {
        objectStore.add(customer);
    });

    transaction.oncomplete = function(event) {
        // 数据添加完成后, 通过索引检索数据
        const indexNameTransaction = db.transaction([objectStoreName], "readonly");
        const indexNameObjectStore = indexNameTransaction.objectStore(objectStoreName);
        const ageIndex = indexNameObjectStore.index("age");

        // 通过索引检索年龄大于25的客户
        const range = IDBKeyRange.lowerBound(26); // 注意: lowerBound 是开区间, 不包括25
        const request = ageIndex.openCursor(range);

        request.onsuccess = function(event) {
            const cursor = event.target.result;
            if (cursor) {
                console.log("Customer:", cursor.value);
                cursor.continue();
            }
        };

        request.onerror = function(event) {
            console.error("Error retrieving data:", event.target.error);
        };
    };

    transaction.onerror = function(event) {
        console.error("Error adding data:", event.target.error);
    };
};

request.onerror = function(event) {
    console.error("Error opening database:", event.target.error);
};

2.6 多条数据返回

getAll()
getAll(query)
getAll(query, count)

获取多条数据, 除了游标之外, 还有getall()方式返回多条数据.

A key or IDBKeyRange to be queried. If nothing is passed, this will default to a key range that selects all the records in this object store.

注意: A key

支持检索的条件, 当然检索的方式和关系型数据库差异很大的.(用过mongodb对于这种模式应该很熟悉)

Range Code
All keys ≥ x IDBKeyRange.lowerBound(x)
All keys > x IDBKeyRange.lowerBound(x, true)
All keys ≤ y IDBKeyRange.upperBound(y)
All keys < y IDBKeyRange.upperBound(y, true)
All keys ≥ x && ≤ y IDBKeyRange.bound(x, y)
All keys > x &&< y IDBKeyRange.bound(x, y, true, true)
All keys > x && ≤ y IDBKeyRange.bound(x, y, true, false)
All keys ≥ x &&< y IDBKeyRange.bound(x, y, false, true)
The key = z IDBKeyRange.only(z)
{
    const request = indexedDB.open("botdatabase", 1);
    request.onupgradeneeded = function () {
        const db = request.result;
        const store = db.createObjectStore("bots", { keyPath: "id" });
        store.createIndex("branch_db", ["branch"], { unique: false });
    };
    request.onsuccess = function () {
        const db = request.result;
        const transaction = db.transaction("bots", "readwrite");
        const store = transaction.objectStore("bots");
        const branchIndex = store.index("branch_db");
        store.add({ id: 1, name: "jason", branch: "IT" });
        store.add({ id: 2, name: "praneeth", branch: "CSE" });
        store.add({ id: 3, name: "palli", branch: "IT" });
        store.add({ id: 4, name: "abdul", branch: "IT" });
        store.put({ id: 6, name: "leevana", branch: 'IT' });
        store.put({ id: 5, name: "jeevana", branch: "CSE" });
        const query = branchIndex.getAll(null, 3);
        store.getAll(null, 3).onsuccess = function (e) {
            console.log(e.target.result);
        };
        query.onsuccess = function () {
            console.log(query.result);
        };
        transaction.oncomplete = function () {
            db.close;
        };
    };
}

index/table都有此方法.

{
    const request = indexedDB.open("botdatabase", 1);
    request.onupgradeneeded = function () {
        const db = request.result;
        const store = db.createObjectStore("bots", { keyPath: "id" });
        store.createIndex("branch_db", ["branch"], { unique: false });
    };
    request.onsuccess = function () {
        const db = request.result;
        const transaction = db.transaction("bots", "readwrite");
        const store = transaction.objectStore("bots");
        const branchIndex = store.index("branch_db");
        store.add({ id: 1, name: "jason", branch: "IT" });
        store.add({ id: 2, name: "praneeth", branch: "CSE" });
        store.add({ id: 3, name: "palli", branch: "IT" });
        store.add({ id: 4, name: "abdul", branch: "IT" });
        store.put({ id: 6, name: "leevana", branch: 'IT' });
        store.put({ id: 5, name: "jeevana", branch: "CSE" });
        store.put({ id: 5, name: "jeevana", branch: "PHD" });
        // [], 只能传入一个值
        const query = branchIndex.getAll(['IT'], 5);
        query.onsuccess = function () {
            console.log(query.result);
        };
        transaction.oncomplete = function () {
            db.close;
        };
    };
}

总体而言, IDBKeyRange的使用方式并不是很好用, 数据不多而且筛选条件多, 还不如直接使用游标逐个判断来得简单.

三. 简单封装

class Indexed_DB {
    #db_name = null;
    #db_open = null;
    #db_instance = null;
    #table_arr = null;
    #is_update_flag = false;
    #error_flag = false;
    get error_flag() { return this.#error_flag; }
    // 错误处理
    #error_wrapper(event, source_name) {
        this.#error_flag = true;
        const error = 'indexeddb error, ' + source_name + ': ' + event.target.error.message;
        console.log(error);
        return error;
    }
    // request的简单封装
    #request_wrapper(request, return_type) {
        return new Promise((resolve, reject) => {
            request.onsuccess = (e) => resolve(return_type === 'value' ? e.target.result : e.target.result ? true : false);
            request.onerror = (e) => reject(this.#error_wrapper(e, 'request wrapper'));
        });
    }
    // 事务的简单封装
    #transaction_wrapper(transaction) {
        return new Promise((resolve, reject) => {
            transaction.oncomplete = () => resolve();
            transaction.onerror = () => reject(this.#error_wrapper(e, 'transaction wrapper'));
        });
    }
    // 其他事件
    #other_event() {
        this.#db_open.onversionchange = (_e) => console.log("The version of this database has changed");
        // 意外关闭才会触发这个事件, 如数据库被手动删除, 正常关闭数据库不会触发这个事件
        this.#db_open.onclose = (e) => this.#error_wrapper(e, 'close error');
    }
    // 检查表是否存在
    #check_tables_is_exist() { return this.#table_arr.filter(e => !this.#db_instance.objectStoreNames.contains(e.table_name)); }
    // 重新打开数据库, 创建新的表
    #reopen_db() {
        const v = this.#db_instance.version + 1;
        this.#db_instance.close();
        this.#db_instance = indexedDB.open(this.#db_name, v);
        return this.initialize();
    }
    // 创建表
    #create_tables() {
        // transaction中只有最后的事务执行完毕才会执行oncomplete回调函数, 而不是每个事务执行完毕时执行oncomplete回调函数
        return this.#transaction_wrapper(this.#table_arr.map(e => {
            const keypath = e.key_path;
            return this.#db_instance.createObjectStore(e.table_name, keypath ? { keyPath: keypath } : { autoIncrement: true });
        }).pop().transaction);
    }
    // 获得表对象
    #get_table_obj(table_name, rwmode) { return this.#db_instance.transaction(table_name, rwmode).objectStore(table_name); }
    /**
     * 具体的表内容操作, 增/删/改/查
     * @param {string|Array} data
     * @param {string} table_name
     * @param {string} rwmode
     * @param {string} operator
     * @param {string} value_type
     * @returns {Promise}
     */
    #table_operation(data, table_name, rwmode, operator, value_type, is_mult_args = false) {
        if (Array.isArray(data) && !is_mult_args) {
            const table = this.#get_table_obj(table_name, rwmode);
            return Promise.all(data.map(e => this.#request_wrapper(table[operator](e), value_type)));
        } else return this.#request_wrapper(this.#get_table_obj(table_name, rwmode)[operator](...(is_mult_args ? data : [data])), value_type);
    }
    add(table_name, data) { return this.#table_operation(data, table_name, 'readwrite', 'add', 'boolean'); }
    delete(table_name, data) { return this.#table_operation(data, table_name, 'readwrite', 'delete', 'boolean'); }
    get(table_name, data) { return this.#table_operation(data, table_name, 'readonly', 'get', 'value'); }
    check(table_name, data) { return this.#table_operation(data, table_name, 'readonly', 'get', 'boolean'); }
    /**
     * 自定义筛选检索
     * @param {string} table_name
     * @param {Function} condition_func 自定义条件函数
     * @param {any} args
     * @param {number} limit
     * @returns {Promise}
     */
    batch_get_by_condition(table_name, condition_func, args, limit = 10) {
        return new Promise((resolve, reject) => {
            const table = this.#get_table_obj(table_name, 'readonly'), request = table.openCursor(), results = [];
            request.onsuccess = (e) => {
                const cursor = e.target.result;
                if (cursor && results.length < limit) {
                    const value = cursor.value;
                    condition_func(value, ...args) && results.push(value);
                    cursor.continue();
                } else resolve(results.length === 0 ? false : results);
            };
            request.onerror = (e) => reject(this.#error_wrapper(e, 'batch get error'));
        });
    }
    batch_get(table_name, limit = 10) { return this.#table_operation([null, limit], table_name, 'readonly', 'getAll', 'value', true); }
    close() { this.#db_instance.close(); }
    initialize() {
        return new Promise((resolve, reject) => {
            // 升级事件, 当数据库不存在时, 先触发
            // 创建表格等操作必须在这个事件之下才能执行
            this.#db_open.onupgradeneeded = (e) => {
                this.#db_instance = e.target.result;
                this.#is_update_flag = true;
                this.#create_tables().then(() => resolve(0)).catch(() => reject('fail to create tables'));
            };
            // 数据库不存在时, 先触发上面的升级事件, 然后才触发本事件
            this.#db_open.onsuccess = (e) => {
                this.#other_event();
                if (this.#is_update_flag) return;
                this.#db_instance = e.target.result;
                const arr = this.#check_tables_is_exist();
                if (arr.length > 0) {
                    this.#table_arr = arr;
                    this.#reopen_db().then(() => resolve(2)).catch(e => reject(this.#error_wrapper(e, 'reopen error')));
                } else resolve(1);
            };
            this.#db_open.onerror = (e) => reject(this.#error_wrapper(e, 'open error'));
            /*
            The event handler for the blocked event.
            This event is triggered when the upgradeneeded event should be triggered _
            because of a version change but the database is still in use (i.e. not closed) somewhere,
            even after the versionchange event was sent.
            */
            this.#db_open.onblocked = (e) => reject(this.#error_wrapper(e, 'blocked error'));;
        });
    }
    /**
     * @param {string} dbname
     * @param {Array} table_dics [{'table_name':'', 'key_path':''}]
     */
    constructor(dbname, table_arr) {
        this.#db_name = dbname;
        this.#table_arr = table_arr;
        this.#db_open = indexedDB.open(dbname);
    }
}
// const db = new Indexed_DB('test', [{ table_name: 'test1', key_path: 'vid' }, { table_name: 'test2', key_path: 'mid' }]);
// db.initialize().then(() => {
//     console.log('db initialized');
//     db.add('test1', [{ 'vid': 'abc1', 'content': 'test1' }, { 'vid': 'abc2', 'content': 'test2' }]).then(() => {
//         db.batch_get('test1').then((data) => console.log(data));
//         db.check('test1', 'abc1').then((data) => console.log(data));
//     });
// });

// 上下两部分代码是等价的
(async () => {
    const db = new Indexed_DB('test', [{ table_name: 'test1', key_path: 'vid' }, { table_name: 'test2', key_path: 'mid' }]);
    try {
        await db.initialize();
        console.log('db initialized');
        await db.add('test1', [{ 'vid': 'abc1', 'content': 'test1' }, { 'vid': 'abc2', 'content': 'test2' }]);
        const data = await db.batch_get('test1');
        console.log(data);
        const b = await db.batch_get_by_condition('test1', (value, arg) => value.vid === 'abc1' && value.content.includes(arg), ['test'], 2);
        console.log(b);
        const c = await db.check('test1', ['abc1', 'abc2']);
        console.log(c);
    } catch (error) {
        console.log(error);
    }
})();

indexeddb简单封装, 在调用上可以使用传统的then()...catch()方式, 但考虑到代码的可阅读性, 也可以使用async....await方式, 相对于前者后者更为直观.

四. 小结

由于浏览器的不稳定使用场景(数据库随时可能被删除, 数据库所处的环境的难以测量等), indexeddb在生产环境中使用还是有很多问题的, 但是对于自己使用, 完全可控, 这是个很好的数据存储选择.