前言
本篇文章是关于indexDB我的一些学习分享,学习内容来自indexDB W3C规范,大家可以直接查看规范学习,如果对英文规范有理解上的困难,亦可查阅此篇文章学习,这里对规范的内容做了提炼总结,并附上一些实践案例,如果通过阅读此文让你对indexDB的开发实践有比较透彻的理解,那真是一件令人快乐的事儿~
背景
在实际业务开发中,我们通常会有一些大量复杂的数据结构的请求和处理,这些数据如果通过接口获取会比较耗费带宽,影响性能,且一次性加载大量数据在内存中,会对浏览器造成较大的内存压力。这时候就需要一个比localStorage存储量更大的,存储结构更灵活的存储机制,Indexed Database就是其中一种。
它通常是通过使用持久性的B树的数据结构来实现的,这些数据结构被认为对于数据的插入、删除以及大量数据的有序遍历更高效。
1. 简介
入门🌰(在线地址):
const request = indexedDB.open('indexdb_init', 1);
let db;
request.onupgradeneeded = function(event) {
// 多版本并行
const db = request.result;
if(event.oldVersion < 1) {
const store = db.createObjectStore("books", { keyPath: 'isbn' });
db.books = store;
const titleIndex = store.createIndex("by_title", "title", { unique: true });
const authorIndex = store.createIndex("by_author", "author");
}
if(event.oldVersion < 2) {
const bookStore = request.transaction?.objectStore("books");
const yearIndex = bookStore?.createIndex("by_year", "year");
}
if(event.oldVersion < 3) {
const magazines = db.createObjectStore("magazines");
const publisherIndex = magazines.createIndex("by_publisher", "publisher");
}
}
request.onsuccess = function() {
db = request.result;
console.log(db, db.parent, 'd-------')
}
request.onerror = function(e) {
console.log("open database error", e);
}
多个客户端(pages 和 workers)可以同时使用一个数据库,transaction(事务)保证他们不会在读取和写入的时候不会发生冲突,如果一个客户端想要升级数据库(通过 upgradeneeded 事件),只有当所有其他客户端关闭他们与数据库当前版本的连接才可以。
为了避免阻止数据库升级,客户端可以监听versionchange
事件,当其中一个页面或者线程想要升级数据库时触发,为了让这种情况继续下去,请通过执行最终关闭此页面(线程)与数据库连接的操作来响应versionchange
。
其中一种方式是重载页面。
db.onversionchange = function() {
// 第一步, 保存数据:
saveUnsavedData().then(function() {
// 如果页面处于未激活状态,那么在没有用户交互的情况下重新加载页面是合适的
if (!document.hasFocus()) {
location.reload();
// 重新加载会关闭数据库,并重新加载新的js和数据库定义
} else {
// 如果页面处于激活状态,重新加载页面可能会造成干扰,可能需要要求用户手动完成
displayMessage("Please reload this page for the latest version.");
}
});};
function saveUnsavedData() {
}
function displayMessage() {
}
另一种方法是调用连接的close方法,但是,你需要确保你的应用知道这一点,因为后续访问数据库的尝试都将失败。
db.onversionchange = function() {
saveUnsavedData().then(function() {
db.close();
stopUsingTheDatabase();
});
};
function stopUsingTheDatabase() {
}
尝试升级的页面可以使用阻塞事件来检测其他客户端是否正在阻塞升级发生,如果其他客户端在versionchange
事件后仍保持与数据库的连接,则会触发阻塞事件。
// 创建基于新版本的数据库连接,触发versionchange事件
const request = indexedDB.open("library", 4);
request.onblocked = function() {
// 检测到有数据库升级事件,执行保存数据的逻辑
blockedTimeout = setTimeout(function() {
displayMessage(
"Upgrade blocked - Please close other tabs displaying this site."
);
}, 1000);};
request.onupgradeneeded = function(event) {
clearTimeout(blockedTimeout);
// 一些善后工作
hideMessage();
// ...
};
function hideMessage() {
// Hide a previously displayed message.
}
2. 结构
数据库的关键属性name
(数据库名称),数据库名称对大小写敏感。
2.1 Database
每个域名下可以创建多个数据库,每个数据库具有零个或多个Object Store
(可以理解为数据表),这些Object Store
被用来存储数据。
Database有name
属性,是一个常量,在数据库的生命周期内保持不变。
Database有version
属性,数据库第一次被创建时,version
默认为0。一个数据库同一时刻只会存在一个版本,使用upgrade transaction
(触发数据库升级的事务)是更改数据库版本的唯一方式。
js通过建立连接来操作数据库,同一时刻,对于给定的数据库可能存在多个连接。只能建立与当前域名作用域下的数据库的连接。
每个连接有一个初始化为false的close pending flag
。当连接关闭后会被设置为true
。
在特殊情况下,浏览器端会关闭连接,例如由于无法访问文件系统,权限更改,或数据库被删除等。如果发生这种情况,浏览器端必须运行关闭数据库连接,并将连接close pending flag
设置为true
。
一个数据库连接可以操作该数据库下的Object Store
,如果尝试升级或删除数据库,会触发打开连接的versionchange
事件,这使得连接有机会关闭以允许升级或删除数据库继续进行。
2.2 Object Store
Object Store
是数据库存储的主要机制,一个Object Store
包含存储在该store中的一系列记录,每个记录包含一个key和一个value,这些数据以key升序的规则存储。
一个Object Store
有以下特性:
- name
Object Store
有name
属性,Object store
的命名是不能重复的。
- 主键
一个Object Store
对象有一个可选的keypath
,如果一个store设置了主键,意思它要使用 in-line-keys 否则它将使用out-of-line keys。
-
当一个Store(数据集合)被创建的时候,它可以使用一个自动生成的主键来区分每一条记录。如果一个数据库没有设置主键,将会为他引入key生成器来区分插入到这个store的记录。一个store的主键有三个来源:
- 主键生成器: 每次新的记录插入的时候为其生成一个自增的数字。
- 主键可以通过keyPath来主动设置。
- 主键可以根据存储的数据来明确的区分。例如: 日期对象,文件对象,二进制对象,图片对象等等。
-
Object Store handle
- js不直接与Object Store交互,而是通过事务中通过Object Store handle间接访问。可以针对同一个Object Store创建多个事务,一个事务仅能有一个关联的Object Store。
一个🌰(在线地址):
const request = indexedDB.open("objectStoreHandle");
let db;
request.onsuccess = function(event) {
db = request.result;
console.log(event, 'success-----')
}
request.onerror = function(event) {
console.log(event, 'error------')
}
request.onblocked = function(event) {
console.log(event, 'error------')
}
request.onupgradeneeded = function(event) {
// 在这个upgrade transaction里可以创建数据表和管理数据表的索引
const db1 = event.target.result;
// object store handle
const objectStore = db1.createObjectStore("idbobjectStore", { autoIncrement: true });
// 使用 object store handle 创建索引
objectStore.createIndex("name", "name", { unique: false });
objectStore.createIndex("age", "age");
}
setTimeout(() => {
const transaction = db.transaction("idbobjectStore", "readwrite");
transaction.oncomplete = function(event) {
console.log('transaction committed-----', event)
}
transaction.onerror = function (event) {
console.log('transaction error-----', event)
}
transaction.onabort = function (event) {
console.log('transaction abort-----', event)
}
console.log(transaction, transaction.parent, 'transaction-----')
// object store handle
const objStore = transaction.objectStore("idbobjectStore");
// 可以调用 objStore的属性和方法操作数据表里存储的数据
console.log(objStore, 'objStore-----')
}, 2000)
console.log(request, 'request----')
2.3 Values
支持存储的数据类型
-
String
-
Number
-
Object
-
Array
-
Date Object
-
File Object
-
Blob Object
-
ImageData Object
value的存取都是基于值而不是引用,所以后面基于该对象的修改都不会影响数据库里存的对应记录。
2.4 Keys
为了更高效的存取数据,object store中
的记录都是按照主键 or 索引 升序存储。
一个key也有一个关联的值,如果类型是number
或者date
,则为不受限制的双精度浮点数(不受限制的双精度类型是浮点数类型,对应于所有可能的双精度64位的IEEE754浮点数,finite
,non-finite
和特殊的非数字值NaN的集合)。
ECMAScript 类型都是合法的keys
-
数字类型: 除了
NaN
都支持,包括Infinity
和-Infinity
-
日期类型:除非这个
DataValue
的inter slot
是NaN
-
字符串类型
-
ArrayBuffer 对象
-
数组对象
keys的比较逻辑:
-infinity
是一个key的最小值,比较逻辑是:
数字类型的键值 < 日期类型的键值,日期类型的键值 < 字符串类型的键值,字符串类型的键值 < 二进制类型的键值,二进制类型的键值 < 数组类型的键值,所以可以设定数据库的最大值是[],最小值是-infinity
二进制keys 的比较范围是无符号数 0-255(包含首尾)而不是有符号数-128-127(包含首尾)
2.5 Key Path
主键是一个字符串或者一个字符串列表,能够唯一确定一条数据记录。合法的主键:
-
一个空字符串
-
一个标识符,符合ECMAScript语法规范的标识符
-
逗号分隔的标识符
-
一个非空的字符串数组,字符串符合以上约束。
空格不允许作为一个组件
2.6 Index
我们可以借助索引来实现对数据库更快的查询、更新和删除。
索引具有以下属性:
- name
一个Store创建的索引名称应该是确定且唯一的,
- unique
如果设置该属性为true,则插入或者更新的索引值和数据库中已有的记录重复会失败。
- multiEntry
如果设置为false,则会为其创建一个数组索引。如果设置为true,则一个记录会为这个数组索引的每个子元素创建索引。
js不直接和索引交互,而是通过创建事务来操作索引。一个事务只能绑定一个index处理函数。
一个index处理函数有一个名字,这个名字知道一个upgrade 事务触发前都是有效的。
2.7 事务
我们通过创建事务来读/写数据库中的数据。
事务可以保证我们多个数据库读写操作的有序进行,一个事务可能被用来存储大量数据或者有条件去修改一些数据,事务表示一组原子的、持久的数据访问和数据变更操作。
一个事务有一个作用域,它是一个事务可以与之交互的对象集合。这个作用域会一直保持有效直到数据库版本升级的事务触发。一个事务有以下模式:
- readonly
这种类型的事务只允许读取数据,数据库打开,即可创建这种类型的事务,这种类型的事务有一个优势就是可以同一时间创建作用域有重叠甚至在同一个作用域的多个事务。
- readwrite
这种类型的事务允许读取、修改和删除数据。数据库打开,即可创建这种类型的事务,这种类型的事务不支持同一时间创建多个作用域有重叠的多个事务。
- versionchange
这种类型的事务允许读取、修改和删除数据。也能够创建和删除Stores(数据表)和索引。这种类型的事务会在一个upgradeneeded
事件被触发的时候自动创建,不能够手动创建。
一个事务有一个持久性标记,这个持久性标记有以下取值:
- strict
只有在验证所有未完成的更改都已成功写入持久存储介质后,用户代理才可以认为事务已成功提交。
- relaxed
一旦所有未完成的修改都成功写入存储介质后,用户代理可以任务事务已成功提交,无需后续验证。
- default
用户代理对storage bucket使用其默认的持久性行为,这是事务在没有另外指定下的默认值。
鼓励 Web 应用程序对临时数据(例如缓存或快速更改的记录)使用relaxed模式,而在降低数据丢失风险大于对性能和电源的影响的情况下使用strict模式。鼓励实现权衡来自应用程序的持久性提示与对用户和设备的影响。
事务还有 waitUntil 方法?
2.7.1 事务的生命周期
一个事务有以下状态:
- active
当事务首次创建,或者基于此事务派发一个事件时该事务会进入这个状态,当事务处于这个状态时,可以针对该事务发起新请求。
- inactive
事务在其创建后控制权返回到事件循环之后,以及不再有新的请求发起时,就处于这种状态。
当事务处于此状态时,不能对事务提出任何请求。
- committing
一旦与事务关联的所有请求都完成后,事务将在尝试提交时进入此状态。当事务处于此状态时,不能对事物提出任何请求。
- finished
一旦一个事物已经提交或者被迫中止,就会进入这个状态。当事务处于此状态时,不能对事物提出任何请求。
我们能够创建一个较长时间的事务,但这不是推荐的做法,因为它可能会带来不好的用户体验。一个事务的生命周期如下:
-
一个事务被创建基于对应的模式和作用域,一个事务在创建的时候就进入激活状态。
-
当一个实现能够对下面定义的事务范围和模式进行约束时,实现必须将任务排队以启动异步事务。
一旦事务创建,就可以开始执行针对事务放置的请求,请求必须按照他们针对交易的顺序执行,他们的结果必须按照针对特定事务请求的顺序返回,无法保证不同的事务中请求的返回顺序。
事务模式保证不同事务的两个请求可以以任何顺序执行,而不会影响存储在数据库中的结果数据。
当处理与事务关联的每个请求时,将触发success 或 error 事件。在调度事件时,事务状态设置为active状态,允许针对事务发出其他请求。一旦事件派发完成,事务的状态将再次设置为inactive状态。
一个事务能够在结束前的任何时间点被终止,不管这个事务当前是active状态还是未开始状态。
当事务中止时(有错误发生),必须撤消(回滚)在该事务期间对数据库所做的任何更改。这包括对对象存储内容的更改以及对象存储和索引的添加和删除。
当针对事务的所有请求都已完成并处理其返回的结果、没有针对事务提出新请求且事务尚未中止时,实现必须尝试提交事务
事务开始之前,发起的这些请求不会执行,但是事务会跟踪记录这些请求的顺序。
就是说一个事务会创建一个事件循环队列,也会对应一个事件循环清除队列。
事务被成功commit后,complete事件会被触发。事务被abort时,abort事件会被触发。
2.7.2 事务调度
以下约束定义了何时开启一个事务:
-
一个只读事务能够开始的条件:没有基于相同
Object Store
的读写事务在其之前创建且不处于finished状态(