小议IndexedDB中的主要对象

avatar
开发 @网易智企

IndexedDB是在客户端的浏览器里存储大量数据的一个方案,网易云信的IM也使用了IndexedDB来存储客户的本地消息,而且随着PWA的兴起,学会使用IndexedDB是必不可少的。本文主要将IndexedDB里的一些主要概念抽解出来,帮助大家巩固。

好~ 大家可以把浏览器的开发者工具打开,测试一下下面的示例。

IDBFactory对象

window.indexedDB 这个是在全局环境可以访问到的一个IndexedDB的工厂对象,在Console里输入 indexedDB 然后回车,就可以看到这个对象是 IDBFactory 类。类似的, localStorage 是 Storage 类的实例对象,   caches  是 CacheStorage 类的一个实例对象。

在console里展开这个IDBFactory,展开 __proto__ ,就可以看到indexedDB这个对象的原型上有哪些方法了。

image.png

我们可以通过 var request = indexDB.open("dbName", versionCode) 来打开一个存储对象来存储数据。

IDBRequest对象

这个对象是indexdDB中对所有异步操作的请求对象,类比XMLHttpRequest对象,对请求的结果都需要监听success事件。后文中凡是绑定onsuccess函数的都是在IDBRequest对象上进行,所有的返回结果都在该对象的result属性上,所以可以不用event.target.result来引用结果值,类比xhr.responseText属性。
request.onerror/onupgradeneeded/onsuccess = function(event){}

request.onerror/onupgradeneeded/onsuccess = function(event){
	db = event.target.result; // event.target.result === resust.result
  // 这里的db就是我们的数据库对象
}
  1. onupgradeneeded是在创建数据库 或者 数据库version比原来高的时候会触发.在此函数内部进行一些数据库的初始化,例如建立索引和主键.
  2. IDBFactory.open 返回的是IDBOpenDBRequest ,继承自IDBRequest , 只有 IDBOpenDBRequest 上才会有 onupgradeneeded 事件回调.
  3. onupgradeneeded 是我们唯一可以修改数据库结构的地方。在这里面,我们可以创建和删除对象存储空间以及构建和删除索引.
  4. 执行完onupgredeneeded后将继续触发onsuccess事件.

IDBDatabase对象

在上面我们调用open方法后,在request.result上可以获取到一个IDBDatabase对象,依然在console里展开这个对象,如下图。

image.png

可以看到db的原型上存在createObjectStore和deleteObjectStore等方法,ObjectStore相当于SQL里的table,MongoDB里的Collection.

var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
  1. 创建一个名为 customers 的对象存储空间,主键是ssn; 第一个参数是名称 ,第二个参数是配置参数.
  2. 创建一个已存在的对象存储空间或者删除一个不存在的对象存储空间 都会抛出异常.

IDBObjectStore对象

再展开上面的objectStore对象,可以看到下图所示ObjectStore对象上的变量和方法。

image.png

  • objectStore.creatIndex
objectStore.createIndex("indexName", "keyPath", { unique: false });
  1. 对象存储空间上新增索引。
  2. 创建并返回新的IDBIndex对象,该方法只能从versionchange事务模式的回调方法中被调用。
  3. 若存储的对象没有该属性,将不会出现在该索引的集合里。
  • objectStore.add(Object) —— 对象存储空间中新增对象.
  • objectStore.put(Object [, key] ) —— 更新对象
  • objectStore.get/delete —— 根据key来查找对象
  • objectStore.count(keyOrKeyRange) —— 如果传入null,返回0;传入其他值,返回共享该key值或者key range的object的数量.

到目前为止,我们可以知道如何在创建数据库或者更新数据库时,对objectStore上的数据进行变更了。下面用代码展示一下刚才介绍的各种对象及操作方法。

const dbVersion = 1;// 默认的db版本号是0,如果你要传入新的数据库,那么版本号必须大于0;
const dbName = 'test'

let db;
// IDBOpenReqeust对象
const openDBReq = window.indexedDB.open(dbName, dbVersion)
openDBReq.onerror = event =>  console.error(event)
//	创建数据库或者数据库版本变更时会触发
openDBReq.onupgradeneeded = (event) => {
	
  // 如果是数据库版本变更,会有版本号
  const { newVersion, oldVersion } = event;
  
  //	IDBDatabse对象
  db = event.target.result;
  // 在这个函数里,我们需要创建一个叫customer的表
  let objectStore = db.createObjectStore('customers',{keyPath:'ssn'})
  // 创建一个索引,便于快速查找数据
  objectStore.createIndex(":)indexName", "name", { unique: false });
  
  // 可以在初始化的时候放入一些初始数据
  let reqPut = objectStore.put({ssn:123,name:"yunxinim"}) 
  reqPut.onerror = e => console.error('初始化时放入数据失败')
  reqPut.onsuccess = e => console.log('放置初始化数据成功')
  
}
openDBReq.onsuccess = event => db = event.target.result;
// 如果数据库不使用了,可以调用 openDBReq.close()关闭数据库


那么,在openDBReq.onsuccess后又怎样操作objectStore呢? 我们可以调用IDBDatabse对象上 transaction 方法来操作ObjectStore,该方法会返回一个IDBTransaction对象。

IDBTransaction对象

在数据库里有个概念叫事务,一个事务执行过程中如果错误,那么在这个事务执行的过程中所做的操作都会失效,这样保证代码执行到一半出错时,不会对原来的数据造成影响。

var transaction = db.transaction(['customers', 'orders'],'readwrite');
  • transaction() 方法接受三个参数(虽然两个是可选的)并返回一个事务对象。
  • 第一个参数是事务希望跨越的对象存储空间的列表。如果你希望事务能够跨越所有的对象存储空间你可以传入一个空数组。
  • 如果你没有为第二个参数指定任何内容,你得到的是只读事务。因为这里我们是想要写入所以我们需要传入 "readwrite" 标识。


你懂的,在console里展开一下transaction,看看有啥属性和方法。

image.png

事务的生命周期

事务和事件循环队列的联系非常密切。如果你创建了一个事务但是并没有使用它就返回了,那么这个事务将会在下一个事件循环的时候就过期了,也就是说需要重新创建事务。保持事务活跃的唯一方法就是在其上构建一个请求(IDBRequest),这个请求会加入到事件循环队列里,这个事务也就保活了。当请求完成时(onsuccess)你将会得到一个 DOM 事件,你可以选择在这个事件回调里构建新的请求来延长事务。如果你没有延长事务就返回到了事件循环,那么事务将会过去,依此类推。只要还有待处理的请求,事务就会保持活跃。事务生命周期真的很简单但是可能需要一点时间你才能对它变得习惯。如果你开始看到 TRANSACTION_INACTIVE_ERR 错误代码,说明这个生命周期没有续上。

PS: 当你要操作多个ObjectStore时,最好使用transaction来保证在失败的情况下不会修改数据。
PSS: onupgradeneeded事件里( event.target 或 openDBReq)上存在一个属性transaction,其实这是一个 versionchange 的事务,在数据库版本变更的时候,如果你需要对旧版本数据进行迁移,那么你可能会需要使用这个事务来延长 onupgradeneeded 时间.

下面用代码来展示一下如何使用事务来操作ObjectStore

const dbVersion = 1;// 默认的db版本号是0,如果你要传入新的数据库,那么版本号必须大于0;
const dbName = 'test'

let db;
// IDBOpenReqeust对象
const openDBReq = window.indexedDB.open(dbName, dbVersion)
openDBReq.onerror = event =>  console.error(event)
//	创建数据库或者数据库版本变更时会触发
openDBReq.onupgradeneeded = (event) => {
	
  // 如果是数据库版本变更,会有版本号
  const { newVersion, oldVersion } = event;
  
  //	IDBDatabse对象
  db = event.target.result;
  // 在这个函数里,我们需要创建一个叫customer的表
  let objectStore = db.createObjectStore('customers',{keyPath:'ssn'})
  // 创建一个索引,便于快速查找数据
  objectStore.createIndex(":)indexName", "name", { unique: false });
  
  // 可以在初始化的时候放入一些初始数据
  let reqPut = objectStore.put({ssn:123,name:"yunxinim"}) 
  reqPut.onerror = e => console.error('初始化时放入数据失败')
  reqPut.onsuccess = e => console.log('放置初始化数据成功')
  
}
openDBReq.onsuccess = event => {
	db = event.target.result;
  
  // 创建一个读写customers里的事务
  const tx = db.transaction(['customers'],'readwrite');
  let customers = tx.objectStore('customers')
  customers.get(123).onsuccess = evt => {
  	let theCustomer = evt.target.result;
    console.log('获取数据', theCustomer)
    theCustomer.phone = theCustomer.phone + 130;
    customers.put(theCustomer).onsuccess = evt => {
    	console.log('更新数据成功', theCustomer)
    }
  }
}


IDBCursor对象

我们可以使用游标来遍历ObjectStore里的所有数据。

objectStore.openCursor().onsuccess = function(e){
    var cursor = e.target.result;
    if (cursor) {
        alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
        cursor.continue();
    }
    else {
         alert("No more entries!");
    }
}

openCursor() 函数需要几个参数。首先,你可以使用一个 key range对象来限制被检索的项目的范围。第二,你可以指定你希望进行迭代的方向。在上面的示例中,我们在以升序迭代所有的对象。

// IDBCursorWithValue 继承自IDBCursor
{
    direction:"next",
    key:"444-44-4444",
    primaryKey:"444-44-4444",
    source: IDBObjectStore,
    value: Object
}

image.png

e.target.result是IDBCursorWithValue对象,该对象中的key是key,value是key对应的对象;

如果要得到所有的objectStore中的对象,可以使用objectStore.getAll() .(ps:getAll方法可能有兼容性问题)

keyRage相当于sql里的whereClause.

IDBIndex对象

IDBIndex对象是索引,在创建ObjectStore时可以为一个属性创建一个索引,便于快速查找。
使用索引: objectStore.index("indexName")

var index = objectStore.index(":)name");
// 这里要使用在onupgradeneeded里创建的索引的名称,不是对象的name属性名称
index.get("yunxinim").onsuccess = function(event) {
  alert("Donna's primaryKey is " + event.target.result.primaryKey);
};

image.png

name游标不是唯一的,因此name被设成 Donna的记录可能不止一条。在这种情况下,你总是得到键值最小的那个(PS:直接在objectStore上调用get,如果同一个key有多个条记录,那么只会返回一条记录)。如果你需要访问带有给定 name的所有的记录你可以使用一个游标。你可以在索引上打开两个不同类型的游标。一个常规游标映射索引属性到对象存储空间中的对象。一个键索引映射索引属性到用来存储对象存储空间中的对象的键。objectStore的游标和索引的游标的不同之处被展示如下:

objectStore.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key 是一个 primaryKey,即createObjectStore时指定的keyPath属性对应的值, 就像 "Bill", 
    // 然后 cursor.value 是整个对象。
    alert("primary key: " + cursor.key + ", SSN: " + cursor.value.ssn + ", name: " + cursor.value.name);
    cursor.continue();
  }
};
index.openKeyCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key is 一个 当前索引的值,如果索引的是对象上的name,那么cursor.key就是name,
    // primaryKey是createObjectStore时指定的keyPath属性对应的值,
    // cursor.value 没有值。
    // 在这个cursor上没有办法可以得到存储对象的其余部分。
    alert("Name: " + cursor.key + ", value is undefined: " + cursor.value);
    cursor.continue();
  }
};

如果要从游标上跳到指定次数的游标处,可以使用cursor.advance(Number biggerThan0)方法,直接前进指定次数的游标。传入的参数要求必须大于0。

image.png

使用Cursor的range和direction

如果你想要限定你在游标中看到的值的范围,你可以使用一个 key range 对象然后把它作为第一个参数传给 openCursor()或是 openKeyCursor()。你可以构造一个只允许一个单一 key 的 key range,或者一个具有下限或上限,或者一个既有上限也有下限。边界可以是闭合的(也就是说 key range 包含给定的值)或者是“开放的”(也就是说 **key range **不包括给定的值)。这里是它如何工作的:

// 只匹配 "Donna"
var singleKeyRange = IDBKeyRange.only("Donna");
// 匹配所有在 "Bill" 前面的, 包括 "Bill"
var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");
// 匹配所有在 “Bill” 前面的, 但是不需要包括 "Bill"
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);
// Match anything up to, but not including, "Donna"
var upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);
//Match anything between "Bill" and "Donna", but not including "Donna"
var boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);
index.openCursor(boundKeyRange).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};

有时候你可能想要以倒序而不是正序(所有游标的默认顺序)来遍历。切换方向是通过传递 prev到 openCursor() 方法来实现的:

objectStore.openCursor(null, IDBCursor.prev).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

一些option对象

//新建objectStore时
db.createObjectStore("name",{
    (DOMString or sequence<DOMString>)? keyPath = null;
    boolean                             autoIncrement = false;
});
//新增`index`时
dictionary IDBIndexParameters {
    boolean unique = false;
    boolean multiEntry = false;
};
//除了upgradeneeded  还有 onblocked 是在另外的标签页打开数据库时触发
//该事件包含两个参数
{
    readonly    attribute unsigned long long  oldVersion;
    readonly    attribute unsigned long long? newVersion;
};

transaction打开objectStore有三个选项**readonly**,**readwrite**,**versionchange**;

类型为**versionchange****transaction**可以修改**index**,**keypath**,但是这种类型的**transaction**只能由**upgradeneeded**事件生成.

indexDB的方法有 open, deleteDatabase, cmp
**cmp**比较传入的参数,如果数值出错,将抛出异常. 第一个数比第二个数大,返回1;比第二个数小,返回-1;相同,返回0;

Database的属性:

readonly    attribute DOMString          name;
readonly    attribute unsigned long long version;
readonly    attribute DOMStringList      objectStoreNames;
IDBObjectStore createObjectStore (DOMString name, optional IDBObjectStoreParameters optionalParameters);
void           deleteObjectStore (DOMString name);
IDBTransaction transaction ((DOMString or sequence<DOMString>) storeNames, optional IDBTransactionMode mode = "readonly");
void           close ();

需要注意的点

直接open一个数据库,如果数据库不存在,将新建该数据库,version为0;

思考

如何实现跨表查询?

目前只有通过indexRange实现简单的多条件查询.跨表只能通过在游标里面循环. 建议修改自己存储的数据结构,参考No-SQL数据库的数据结构设计,存储的值可以有多层。

最后,原生的接口很难用,建议使用一些封装好的库,例如Dexie.js,idb等等。

参考:
[developer.mozilla.org/zh-CN/docs/…](developer.mozilla.org/zh-CN/docs/…)