IndexedDB
JavaScript IndexedDB
Now, let’s explore a much more powerful built-in database than localStorage: IndexedDB.
It can have almost any value and multiple key types. Transactions are also supported for more reliability. Key range queries and indexes are supported by IndexedDB. Moreover, it can store a lot more data than the localStorage. As a rule, IndexedDB is used for offline applications, to be connected to ServiceWorkers and other technologies. (它可以具有几乎任何值和多种键类型。 还支持交易,以提高可靠性。 IndexedDB支持关键范围查询和索引。 此外,它可以存储比localStorage更多的数据。 通常, IndexedDB用于离线应用程序,以连接到ServiceWorkers和其他技术。)
To begin the work with IndexedDB, it is, first, necessary to open a database. (要开始使用IndexedDB ,首先需要打开数据库。)
The syntax is as follows:
let openReq = indexedDB.open(name, version);
A lot of databases can exist with different names. But, note that all of them should be in the current origin. Websites have the capability of accessing the databases of one another. (许多数据库可能存在不同的名称。但是,请注意,它们都应位于当前原点。网站能够访问彼此的数据库。)
The next step is listening to events on the openRequest request, like here:
success: the database is arranged, there exists the “database object” in openRequest.result, which you should apply for the next calls.
error: the opening is declined.
upgradeneeded: database is arranged, but its version is out-of-date.
There is a built-in mechanism of “schema versioning” in IndexedDB, that doesn’t exist in server-side databases. (IndexedDB中存在“架构版本控制”的内置机制,该机制在服务器端数据库中不存在。)
IndexedDB is considered client-side. The data is kept in the browser, hence the developers can’t access it directly. (IndexedDB被视为客户端。数据保存在浏览器中,因此开发人员无法直接访问。)
But, while publishing a new version of the app, it can be necessary to update the database. (但是,在发布新版本的应用程序时,可能需要更新数据库。)
In case the local database version is less than specified in the open, then a unique upgradeneeded event occurs. So, the versions can be compared, and data structures can be upgraded if required. (如果本地数据库版本低于打开时指定的版本,则会发生唯一的upgradeeneededed事件。因此,可以比较版本,如果需要,可以升级数据结构。)
The event can occur when there is no database yet, so initialization may be performed. (事件可能在还没有数据库时发生,因此可以执行初始化。)
So, the first step is publishing the app, opening it with version 1, and performing initialization in the upgradeneeded handler, like this:
let openRequest = indexedDB.open("store", 1);
openRequest.onupgradeneeded = function () {
// triggers if the client does not have a database
(//如果客户端没有数据库,则触发)
// initialization
};
openRequest.onerror = function () {
console.error("Error", openRequest.error);
};
openRequest.onsuccess = function () {
let db = openRequest.result;
// continue working with the database using the db object
};
Publishing the 2nd version will look like this:
let openRequest = indexedDB.open("store", 2);
openRequest.onupgradeneeded = function () {
// the existing version of the database is less than 2 (or it does not exist) let db = openRequest.result;
switch (db.version) { // existing version of the database
case 0:
// version 0 means the client did not have a database
(//版本0表示客户端没有数据库)
// initialization
(初始化)
case 1:
// client had version 1, update
(//客户端有版本1 ,更新)
}
};
After openRequest.onsuccess, in openRequest.result there is the database object, which can be used for further operations. (在openRequest.onsuccess之后, openRequest.result中有数据库对象,可用于进一步的操作。)
Let’s check out how to delete the database:
let deleteReq = indexedDB.deleteDatabase(name)
// deleteReq.onsuccess/onerror tracks the result
Object Store
Object Store (对象存储)
An object store is necessary for storing something in IndexedDB. It is a primary concept of IndexedDB, and its counterparts in other databases are collections and tables. Multiple stores can exist in a database. Any value, even complex objects, can be stored in it. IndexedDB applies the standard serialization algorithm for cloning and storing an object. (对象存储对于在IndexedDB中存储某些内容是必需的。 它是IndexedDB的主要概念,其在其他数据库中的对应物是集合和表。 数据库中可以存在多个商店。 任何值,即使是复杂的对象,都可以存储在其中。 IndexedDB应用标准序列化算法来克隆和存储对象。)
It is similar to JSON.stringify but can store more data types. For each value in the store, there should be a specific key. A key should include a type one of the following: string, date, number, binary, or array.
In case it is a specific identifier, the values can be searched, removed, and updated by the key. (如果是特定标识符,则可以通过密钥搜索、删除和更新值。)
Now, let’s see how to create an object store in JavaScript. (现在,让我们看看如何在JavaScript中创建对象存储。)
Here is the syntax:
db.createObjectStore(name[, keyOptions]);
This is a synchronous operation, and no await is necessary. If keyOptions is not supplied, then the key should be provided while storing an object. (这是一个同步操作,不需要等待。 如果未提供keyOptions ,则应在存储对象时提供密钥。)
For example, the object store below applies id as a key, like this:
db.createObjectStore('lessons', {
keyPath: 'id'
});
Note that you can create or modify an object store only while updating the version of DB within the upgradeneeded handler. (请注意,只有在upgradeeneeded处理程序中更新数据库版本时,才能创建或修改对象存储。)
For deleting an object store, you can act like this:
db.deleteObjectStore('lessons');
Transactions
Transactions (交易)
In different kinds of databases, you can use the term “transaction”. Generally, it’s a group of operations that can either totally succeed or totally fail. The overall data operations more must be done within a transaction in IndexedDB. (在不同类型的数据库中,您可以使用术语“交易”。 一般来说,它是一组可以完全成功或完全失败的操作。 整体数据操作必须在IndexedDB中的事务中完成。)
This is how you can start the transaction:
db.transaction(store[, type]);
store: it is the name of the store that the transaction intends to access.
type: it is the type of the transaction. It can be one of the following:
readonly: is capable of reading only. readwrite: can both read and write data. But, can’t create, remove, or alter object stores.
There is also another type of transactions: versionchange. It can do anything, but you can’t generate it manually.
A versionchange transaction can be automatically created by IndexedDB while opening the database for the updateneeded handler.
After creating the transaction, it is necessary to add an item to the store as follows:
let transaction = db.transaction("lessons", "readwrite"); // (1)
// get the store of object to operate on it
let lessons = transaction.objectStore("lessons"); // (2)
let lesson = {
id: 'js',
price: 20,
created: new Date()
};
let request = lessons.add(lesson); // (3)
request.onsuccess = function () { // (4)
console.log("Lesson added to the store", request.result);
};
request.onerror = function () {
console.log("Error", request.error);
};
There exist four basic steps:
Creating a transaction, mentioning all the stores it intends to access (1). Getting the store object with transaction.objectStore(name) at (2). Performing the request to the object store books.add(book)at (3). Handling the request success or error (4). Afterward, another request can be made. (创建交易,提及其打算访问的所有商店( 1 )。 获取transaction.objectStore (name)位于(2)的存储对象。 在( 3 )处执行对对象存储books.add ( book )的请求。 处理请求成功或错误(4)。 之后,您可以再次提出申请。)
Transactions’ Autocommit
Transactions’ Autocommit (交易的自动提交)
Once all the transaction requests are over, and the queue of microtasks is empty, it will be automatically committed. (一旦所有事务请求都结束,并且微任务队列为空,它将自动提交。)
It can be assumed that a transaction can commit once all the requests are complete, and the current code ends. (可以假设,一旦所有请求都完成,并且当前代码结束,事务就可以提交。)
In the example, mentioned above, no specific code is required to end the transaction. (在上述示例中,无需特定代码即可结束交易。)
The auto-commit principle of transactions has a significant disadvantage: it is not capable of inserting an async operation like setTimeout or fetch in the middle of the transaction.
Let’s check out another code where request2 fails in the (*) line as the transaction is committed already, and no requests can be made:
let request1 = lessons.add(lesson);
request1.onsuccess = function () {
fetch('/').then(response => {
let request2 = lessons.add(anotherLesson); // (*)
request2.onerror = function () {
console.log(request2.error.name); // TransactionInactiveError
};
});
};
The reason is that the fetch is considered an asynchronous operation, a macrotask. So, the transactions will be closed before the browser begins to do macrotasks. (原因是fetch被认为是一个异步操作,一个宏任务。因此,交易将在浏览器开始执行宏任务之前关闭。)
In the example above, it is also possible to create a new db.transaction before the new () request. (在上面的示例中,也可以在new ()请求之前创建新的db.transaction。)
But, first, it is necessary to do fetch, preparing the data, and then generate a transaction. (但是,首先需要进行提取,准备数据,然后生成交易。)
Also, you can listen to the transaction.oncompleteevent for detecting the moment of successful completion:
let transaction = db.transaction("lessons", "readwrite");
// operations
transaction.oncomplete = function () {
console.log("Transaction is completed");
};
The transaction can be manually aborted by calling the following: transaction.abort();
It will cancel all the modifications made by the requests and trigger the transaction.onabort event. (它将取消请求所做的所有修改,并触发transaction.onabort事件。)
Error Handling
Error Handling (错误处理)
Any failed request will automatically abort the transaction and cancel all the changes. (任何失败的请求都将自动中止交易并取消所有更改。)
But, there are circumstances when you want to handle the failure without aborting the existing changes. It can be possible with the request.onerror handler. It can preclude the transaction abort with the help of the event.preventDefault() call. (但是,在某些情况下,您希望在不中止现有更改的情况下处理故障。可以使用request.onerror处理程序。它可以借助event.preventDefault ()调用来阻止事务中止。)
Here is an example:
let transaction = db.transaction("lessons", "readwrite");
let lesson = {
id: 'js',
price: 20
};
let request = transaction.objectStore("lessonss").add(book);
request.onerror = function (event) {
// ConstraintError occurs when an object with the id already exists
(//当ID为的对象已经存在时发生ConstraintError)
if (request.error.name == "ConstraintError") {
console.log("Lesson with such id already exists"); // handle the error
event.preventDefault(); // do not interrupt transaction
// use a different key for the book?
(//使用不同的书钥匙?)
} else {
// unexpected error, can't handle it
(//意外错误,无法处理)
// transaction will be aborted
(//交易将被中止)
}
};
transaction.onabort = function () {
console.log("Error", transaction.error);
};
Event Delegation
Instead of using onerror and onsuccess on each request, you can use event delegation. (您可以使用事件委派,而不是对每个请求使用onerror和onsuccess。)
The Indexed DB events are capable of bubbling: request → transaction → database.
All the events belong to the DOM. Hence, all the errors can be caught by the db.onerrorhandler like this:
db.onerror = function (event) {
let request = event.target; // error request
console.log("Error", request.error);
};
It is possible to stop bubbling with event.stopPropagation() inside request.onerror, like this:
request.onerror = function (event) {
if (request.error.name == "ConstraintError") {
console.log("Lesson with such id already exists"); // handle the error
event.preventDefault(); // don't abort the transaction
event.stopPropagation(); // don't bubble error up, "chew" it
} else {
// nothing to do
(无所事事 )
// transaction will be aborted
(//交易将被中止)
// we can take care of the error in the transaction.onabort
(//我们可以处理transaction.onabort中的错误)
}
};
Searching by Keys
Searching by Keys (按密钥搜索)
In an object store, you can implement a search of two main types:
By a range or by a key. In other words, by lesson.id in the books’ storage. By a different object field. (按范围或按键。 换句话说,通过书籍存储中的lesson.id。 由不同的对象字段。)
Let’s start at dealing with the keys and key ranges (1). (让我们开始处理密钥和密钥范围( 1 )。)
The methods involving the searching support ( exact keys or range queries) are known as IDBKeyRange. It indicates a key range. (涉及搜索支持(精确键或范围查询)的方法称为IDBKeyRange。它表示密钥范围。)
You can create ranges with the help of the following calls:
IDBKeyRange.lowerBound(lower, [open]) considers: ≥lower (or >lower if open is true)
IDBKeyRange.upperBound(upper, [open]) considers: ≤upper (or <upper if open is true)
IDBKeyRange.bound(lower, upper, [lowerOpen], [upperOpen]) considers: between lower and upper. In case the open flags are considered true, the matching key is not involved within the range.
IDBKeyRange.only(key)– a range, which comprises only a single key, that is used not often. (- IDBKeyRange.only (key) –仅包含一个不经常使用的键的范围。)
The overall searching methods allow a query argument, which may either represent a key range or an exact key:
Istore.get(query) – looking for the first value by a key or a range. (- Istore.get (query) –按键或范围查找第一个值。)
Istore.getAll([query], [count]) – looking for the overall values, limit by count if specified. (- Istore.getAll ([query], [count]) –查找整体值,如果指定,则按计数限制。)
Istore.getKey(query) – looking for the first key, which can satisfy the query. As a rule, it’s a range. (- Istore.getKey (query) –查找可以满足查询的第一个键。一般来说,这是一个范围。)
Istore.getAllKeys([query], [count]) – looking for all the keys that can satisfy the query. As a rule, a range, up to count if specified. (- Istore.getAllKeys ([query], [count]) –查找可以满足查询的所有键。一般来说,这是一个范围,如果指定,则最多可计数。)
Istore.count([query]) – receiving the total count of keys that can satisfy the query. As a rule, a range. (- Istore.count ([query]) –接收可以满足查询的密钥总数。一般来说,这是一个范围。)
Let’s get to the example. Imagine, there are lots of books in the store. The id field will be the key, hence all the methods can be looked for by id. So, the request example will look like this:
//receive one book
lessons.get('js')
// receive lessons with 'html' <= id <= 'css'
lessons.getAll(IDBKeyRange.bound('html', 'css'))
// receive books with id < 'html'
lessons.getAll(IDBKeyRange.upperBound('css', true))
// receive all lessons
lessons.getAll()
(//接收所有课程
lessons.getAll ())
// receive all keys: id > 'js'
lessons.getAllKeys(IDBKeyRange.lowerBound('js', true))
Deleting from the Store
Deleting from the Store (从应用商店中删除)
The delete method searches for the values for deletion by a query. The format of the calls is like getAll. (Delete方法通过查询搜索要删除的值。调用的格式类似于getAll。)
delete(query): it deletes the corresponding values by the query.
An example of using the delete method is as follows:
// delete the lesson with id='js'
lessons.delete('js');
In case, you wish to delete the books on the bases of the price or another object field, then it’s necessary to detect the key in the index, calling delete like this:
// find the key where price = 10
let request = priceIndex.getKey(10);
request.onsuccess = function () {
let id = request.result;
let deleteRequest = lessons.delete(id);
};
For the deletion of everything, you can act as shown below:
lessons.clear(); // clear the storage.
Cursors
Cursors (光标)
As you know, the getAll/getAllKeys methods return an array of values/keys. But, if an object storage is larger than the available memory, getAll won’t be able to receive all the records as an array. (如您所知, getAll/getAllKeys方法返回一个值/键数组。 但是,如果对象存储大于可用内存, getAll将无法以数组的形式接收所有记录。)
In such cases, cursors can become a helping hand. (在这种情况下,光标可以成为一只助手。)
A cursor is a specific object, which traverses the object storage, given a query, returning a single key/value at a time. So, it can save memory. (光标是一个特定的对象,它遍历对象存储,给定一个查询,一次返回一个键/值。因此,它可以节省内存。)
The syntax of the cursor is the following:
// like getAll, but with the cursor:
let request = store.openCursor(query, [direction]);
// to get keys, not values (getAllKeys): store.openKeyCursor
The primary difference of the cursor is that request.onsuccess occurs several times: one time for every result.
Let’s check out an example of using a cursor:
let transaction = db.transaction("lessons");
let lessons = transaction.objectStore("lessons");
let request = lessons.openCursor();
// called for each lesson found by the cursor
request.onsuccess = function () {
let cursor = request.result;
if (cursor) {
let key = cursor.key; // lesson key, id field
let value = cursor.value; // lesson object
console.log(key, value);
cursor.continue();
} else {
console.log("No more lessons");
}
};
Here are the primary cursor methods:
advance(count) – advancing the cursor count times, skipping values. (- ADVANCE (COUNT) –推进光标计数次数,跳过值。)
continue([key]) – advancing the cursor to the next value in range corresponding (or right after key if specified). (- CONTINUE ([KEY]) –将光标前移到相应范围内的下一个值(如果指定,则紧跟在KEY之后)。)
No matter there exist more values or corresponding to the cursor or not - onsuccess will be called. Afterward, in the cursor.key, it will be possible to receive the cursor by pointing to the next record, or there will be undefined. It is, also, possible to create a cursor over an index. (无论是否存在更多值或是否与光标对应,都将调用onsuccess。 之后,在cursor.key中,可以通过指向下一条记录来接收光标,否则将出现未定义的情况。 也可以在索引上创建光标。)
For the over cursor indexes, the index key is cursor.key. For the object key, cursor.primaryKey should be used:
let request = priceIdx.openCursor(IDBKeyRange.upperBound(10));
// call for each record
requests.onsuccess = function () {
let cursor = requests.result;
if (cursor) {
let key = cursor.primaryKey; // next object store key, id field
let value = cursor.value; // next object store object, lesson object
let key = cursor.key; // next index key, price
console.log(key, value);
cursor.continue();
} else {
console.log("No more lessons");
}
};
Example:
In case the memory is short for the data, a cursor can be used. (如果内存不足,可以使用光标。)
<!DOCTYPE html>
<html>
<head>
<title>Title of the Document</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/idb.min.js">
< /head> < body >
</script>
<button onclick="addLesson()">Add a lesson</button>
<button onclick="clearLessons()">Clear lessons</button>
<p>Lessons list:</p>
<ul id="listElem"></ul>
<script>
let db;
init();
async function init() {
db = await idb.openDb('lessonsDb', 1, db => {
db.createObjectStore('lessons', {
keyPath: 'name'
});
});
list();
}
async function list() {
let tx = db.transaction('lessons');
let lessonStore = tx.objectStore('lessons');
let lessons = await lessonStore.getAll();
if(lessons.length) {
listElem.innerHTML = lessons.map(lesson => `<li>
name: ${lesson.name}, price: ${lesson.price}
</li>`)
.join('');
} else {
listElem.innerHTML = '<li>No lessons yet. Please add lessons.</li>'
}
}
async function clearLessons() {
let tx = db.transaction('lessons', 'readwrite');
await tx.objectStore('lessons')
(等待tx.objectStore ('lessons'))
.clear();
await list();
}
async function addLesson() {
let name = prompt("Lesson name?");
let price = +prompt("Lesson price?");
let tx = db.transaction('lessons', 'readwrite');
try {
await tx.objectStore('lessons')
(等待tx.objectStore ('lessons'))
.add({
name, price
(姓名|价格)
});
await list();
} catch(err) {
if(err.name == 'ConstraintError') {
alert("Such lesson exists already");
await addLesson();
} else {
throw err;
}
}
}
window.addEventListener('unhandledrejection', event => {
alert("Error: " + event.reason.message);
});
</script>
</body>
</html>
Summary
Summary (概要)
IndexedDB is a straightforward key-value database that is robust enough for offline applications. At the same time, it’s quite easy in usage. (IndexedDB是一个简单的键值数据库,对于离线应用程序来说足够强大。同时,它使用起来非常简单。)
Let’s describe its basic usage with several sentences. (让我们用几个句子来描述它的基本用法。)
It gets a promise wrapper like idb. (-它得到了一个像idb一样的承诺包装器。)
A database can be opened with idb.openDb(name, version, onupgradeneeded). (-可以使用idb.openDb (name, version, onupgradeneeded)打开数据库。)
For the requests are initiated: Creation of a transaction db.transaction(‘books’)
(创建交易db.transaction ( “账簿” )) Receiving the object store transaction.objectStore(‘books’). (接收对象存储transaction.objectStore (‘books’)。)
Afterward, comes the search by a key that can call methods on the object store directly. (-之后,通过可以直接在对象存储上调用方法的键进行搜索。)
In case the memory is short for the data, a cursor can be used. (-如果内存不足,可以使用光标。)