一. 前言
在JavaScript进阶指南 | Lian中简单使用了一下indexeddb, 这里重新记录一下使用中的一些细节.
相关内容参考:
浏览器模型 - 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
在生产环境中使用还是有很多问题的, 但是对于自己使用, 完全可控, 这是个很好的数据存储选择.