indexedDB中文入门教程详解

本文通过对IndexedDB的概念、API进行详解,讲清楚IndexedDB中的事务机制,以及objectStore这个核心概念。和一般的教程不同,这篇文章除了一步一步搭建起一个IndexedDB数据库操作的流程,连接、选择、读取、更新等,而且还把最常用的API接口列举出来,方便开发者在一个地方就可以完成所有查询。

indexedDB是html5标准引入的web数据持久化方案之一,现代浏览器大多按照标准对其进行了实现,我在新的项目中用到它来作为持久化数据存储,于是详细研究了一番,MDN上的解释虽然权威,但是由于知识不成体系,细节也讲的不透,所以我打算自己写一篇教程,对indexedDB的各个细节进行详解。

结构体系

这里说的结构体系,是指在indexedDB中,几个重要的概念之间的关系。

简单的说,indexedDB是非关系型数据库,数据组织不像SQL数据库一样,有表、记录,indexedDB里面没有表和记录的概念,它的组织单位(unit)是js对象(object),object在indexedDB里的地位就跟SQL数据库里面的记录一样,是数据的最终体现形式。我们通过下面的图来阐述indexedDB和SQL数据库结构体系的不同,以帮助理解。

IndexedDB概念结构示意图

在level 1,两者都有database的概念,要存储数据,首先要创建一个数据库。在level 2,两者就有了区别,SQLDB有表的概念,而indexedDB对应的是objectStore。简单的说就是,在数据库中开辟一块store用来存储object,同样,一个数据库中可以有多个(甚至无限个)objectStore。在level 3则差别更大,SQLDB有记录的概念,而indexedDB直接存放js的object数据对象。

indexedDB存储的object是结构化数据。简单理解就是,不能存function等非结构化的数据,object必须是以键值对组成的字面对象。并且支持嵌套结构,也就是说object里面嵌套了object,和js实现无缝对接,而同样是本地化存储的localStorage也需要对数据格式化为字符串后才能保存。

打开/创建数据库

level 1层面的操作,indexedDB对象被挂载在window上,直接调用open方法进行打开,如果该数据库不存在,则创建一个新的数据打开:

let request = window.indexedDB.open('mydb', 1)

open有两个参数:

数据库版本

上面的version参数对于初学者而言不是很好理解。如果你用过docker的话,知道有一个东西叫镜像(image),在镜像的基础上构建自己的容器,可以再创建一个新的镜像,那么这个新的镜像其实就包含了老的镜像。这里的version也有这个意思。version为1时,创建了一些objectStore,当你需要添加新的objectStore或者修改某些objectStore的时候,就需要升级version。这时你可能有两个不同的version,1和2. 当你在用open方法打开一个version的时候,你要知道,你得到的db容器对应的objectStore是不同的,如果你还要打开version=1,那么你在version=2中创建的objectStore和新增的object都是不存在的。所以说,新的version一般包含了老的version。

这时你会想,如果我从新的version切换为老的version,还可以在老的version里面添加数据吗?这个问题我们暂时保留。

现在你需要记住的是,在后面的代码操作中,你要时刻保证你使用对了version,它的使用场景只有两种:

从代码的层面来看,并非这两个事情发生才触发了version的改变,恰恰相反,如果你要修改或添加objectStore,你必须通过传递新的version参数到open方法中,触发onupgradeneeded,在onupgradeneeded的回调函数中才能实现目的。

获得db容器

indexedDB中非常重要的概念是transaction(事务),不过我们会在下一节讲什么是事务。这里我们需要快速了解,怎么从上面的open方法之后,获得可以用来进行操作的数据库对象容器:

let request = window.indexedDB.open('mydb', 1)
request.onsuccess = e => {
  let db = e.target.result
}

这样就获得了db的容器,利用该容器,我们就可以进行数据库的进一步操作。

当然,作为jser,你早就发现,这是一个回调操作。request.onsuccess在open方法中被调用,假如在调用之前你没有用一个函数去覆盖onsuccess方法,那么你是得不到db容器的,因此,onsuccess要在仅接着open之后写。

而所有后续的操作,必须在得到db容器之后才能进行,因此,这有是一个异步的操作,你不能在上面代码之后直接使用db变量,而是因为在onsuccess的函数体内继续写代码。而且indexedDB的所有编程都是这样,这会导致嵌套层层加深,因此,我们这篇文章后面还会提供使用Promise进行封装的类,使用该类你无需做复杂的操作。

 

创建objectStore及其索引

objectStore是indexedDB中非常核心的概念,它是数据的存储仓库,一个objectStore类似于SQL数据库中的表,存放着相关的所有数据。所谓的“相关”是指,这些object必须具备相同的一个属性名,也就是“主键”,在indexedDB中被成为keyPath。这还有点像SQL数据库中的primaryKey,不过SQL数据库中不必一定有primaryKey,而objectStore中的keyPath必须有。如果你存入的某个object不存在那个属性,而该属性在indexedDB中又不是autoIncrement,那么就会报错,如果autoIncrement被设置为true,在没有该key的情况下,存入数据库的时候,会被自动添加上,这个效果跟SQL数据的自增字段是一样的。

创建objectStore

在我们使用事务对objectStore进行操作前,我们需要创建对应的objectStore,否则都没有,怎么操作呢?创建objectStore和修改objectStore都只能在db的onupgradeneeded事件中进行,因此,要创建objectStore,必须在前面的open操作那个时候来进行。

let request = window.indexedDB.open('mydb', 1)
request.onupgradeneeded = e => {
    let db = e.target.result
    db.createObjectStore('mystore', {keyPath: 'id'})
}
request.onsuccess = e => {
    let db = e.target.result
    // ...
}

上面的红色代码中,我们使用createObjectStore方法来实现objectStore的创建。但是,需要注意的是,一个database中,只允许存在一个同名的objectStore,因此,如果你第二次createObjectStore相同名的objectStore,程序会报错。比如你的程序如果像上面这样写,必然会遇到一个问题,就是当你更新version的时候,会再次执行createObjectStore,那么就会报错,程序就会中断。另一个注意点是,一旦一个objectStore被创建,它的name和keyPath是不能修改的。那么有什么办法来控制呢?通过一个判断,判断是否已经存在同名的objectStore即可。

let request = window.indexedDB.open('mydb', 1)
request.onupgradeneeded = e => {
    let db = e.target.result
    if (!db.objectStoreNames.contains(storeName)) { // 注意,contains是ES6才有的api
        db.createObjectStore('mystore', {keyPath: 'id'})
    }
}
request.onsuccess = e => {
    let db = e.target.result
    // ...
}

后面我们还会讲到,如何修改objectStore的索引时,应该怎么处理。

objectStore的索引

在indexedDB中也存在索引,和SQL中索引的作用不同,SQL中的索引是对指定字段进行特殊记录,以方便在检索时提高检索性能。indexedDB中的索引,是指在除了设置的keyPath之外,提高其他的检索方式。在indexedDB中,后面会讲到,objectStore有get方法,但是它的参数是keyPath对应的值。而如果要用其他的字段来检索某个object,那就麻烦了,所以indexedDB提高了索引的方式,通过一个index方法来实现索引检索。所以,实际上,objectStore的索引,等效于SQL表的字段。

什么是keyPath?

前面反复提到keyPath这个概念。在前面的代码里面可以发现在createObjectStore的时候,可以规定一个keyPath。

实际上,keyPath的概念非常简单,它规定了你把object的那个属性作为检索的入口。比如你有一堆object:

{
  id: 1,
  name: 'tome'
}
{
  id: 2,
  name: 'sue'
}
...

你会发现,如果你想通过一个get方法来获取第一个object,你必须传入一个参数,而indexedDB怎么处理的呢?如下:

objectStore.get(1)

上面的1,就是id=1。当keyPath=id的时候,get方法去找的就是id=1的那个object,因此,id对于所有object而言,应该是唯一的。

但是怎么确保唯一呢?需要在建立索引的时候,传入一个unique参数,所以,实际上,createObjectStore的时候传入的keyPath是一个特殊的索引。

如何创建索引?

创建索引这个动作,实际上是对objectStore进行修改,因此,只能在db的onupgradeneeded事件中处理。

let request = window.indexedDB.open('mydb', 1)
request.onupgradeneeded = e => {
    let db = e.target.result
    let objectStore = db.createObjectStore('mystore', {keyPath: 'id'})
    objectStore.createIndex('id', 'id', {unique: true})
}

objectStore对象有一个createIndex方法,它可以创建索引。它有三个参数,第一个参数是这个索引的name;第二个参数是key,这个key对应的就是object的属性名,name是可以自己定的,它会用在后面的index方法中进行检索,也会被记录在objectStore的indexNames属性里面,但是key必须和object的属性对应;第三个参数是options,其中unique选项被放在这里面。

如何修改索引?

虽然objectStore本身的信息是不能修改的,比如name和keyPath都是不能修改的,但是它所拥有的索引可以被修改,修改其实就是删除+添加操作。用到的就是deleteIndex这个方法,所以,如果你想修改一个索引,要做的就是先删除掉原来的同名索引,然后添加新的索引。举个例子,如果你知道数据库中已经存在mystore这个objectStore了:

request.onupgradeneeded = e => {
  let objectStore = e.target.transaction.objectStore('mystore')
  let indexNames = objectStore.indexNames
  if (indexNames.contains('name')) {
    objectStore.deleteIndex('name')
  }
  objectStore.createIndex('name', 'name', {unique: false})
}

上面这段代码通过对已有的objectStore的index进行操作,如果存在某个index时,就删除它再添加,如果没有就直接添加。

你可能看代码看到这里会有点晕,这是因为我只提取了代码片段在这里,后文会有完整的代码,根据完整的逻辑去看你会比较清晰。

事务-transaction

所有数据库中都有这个概念,它是为了确保当并非执行某些操作时,不致混乱。举个简单的例子,当你在像好友打钱的时候,发起了一个请求,这个请求发起后,就建立了你打钱的事务,后面一大堆数据库写入的操作,但是,假如中间突然机房停电,银行系统发生故障,你又发起了第二个打钱的请求,那么会不会有这样一种可能,你第一次打出去的钱你朋友根本没收到?数据库系统为了避免这种情况,采用事务机制,如果出错,那就回滚,把你打出去但对方没收到的钱回到你账上,重新再执行一次打钱的操作,执行完这个操作之后,再执行你第二次打钱的操作。这样就保证了数据库增删改有序不混乱。

在代码层面,我们必须通过transaction方法,向数据库容器提出事务要求,才能往具体的objectStore进行数据处理:

let transaction = db.transaction(['myObjectStore'], 'readonly')
let objectStore = transaction.objectStore('myObjectStore')

上面这段代码的操作,我们得到了具体要进行操作的objectStore,这和我们预期直接通过db.objectStore('myObjectStore')这样简洁的方法完全不同,indexedDB中不能这么直接去获取objectStore,而必须通过transaction。

transaction方法有两个参数:

而通过transaction的objectStore方法可以获取想要操作的objectStore,但是它的参数必须存在于上面的objectStores数组中,毕竟你的这个事务已经规定了要对哪些objectStore进行操作。

因为objectStore是在事务中获取,因此一个objectStore实例,如果有一个transaction属性的话,那么可以通过这个属性找出它的事务的实例。在indexedDB中,你只能在事务中得到一个objectStore实例,如果通过db的话,最多只能得到objectStore的名字列表,要获得objectStore的实例,必须在transaction中。这样说来,一个objectStore,可能同时存在于多个事务中?这一点也暂时存疑。

知道有事务的存在之后,你如果感兴趣,应该在继续学习indexedDB事务的生命周期。简单总结注意点就是,和open一样,要在事务操作生命过程中去进行后续操作,不能在onsuccess外部用变量去存储事务在外部使用。举一个错误的例子:

var myOBJ;
cn.onsuccess=function(e){
 db=e.target.result;
 myOBJ=db.transaction(["MyOBJ"],"readwrite").objectStore("MyOBJ");
};
btn.onclick=function(){
 myOBJ.add({}); // 这个操作在事务生命周期结束后执行,myOBJ已经不是你想要的事务实例了
};

正确的做法是,在onclick事件中,重新打开数据库,在打开的数据库onsuccess方法中发起事务,执行这个操作。

数据的存取

当一个事务开始之后,在它的生命周期以内,你可以对objectStore进行数据操作,数据操作无非是增删查。前面介绍过如何获取事务中的objectStore,现在,我们就用获取到的objectStore进行数据操作。

获取数据

let transaction = db.transaction(['myObjectStore'], 'readonly')
let objectStore = transaction.objectStore('myObjectStore')
let request = objectStore.get('100001')
request.onsuccess = e => {
    let obj = e.target.result
}

的确,在indexedDB事务机制下进行操作是很麻烦的,上面代码中我们使用了get方法获取主键值为100001的object,但是获取过程也是在一个事务中,是一个Request,后文会详细讲Request是什么东东,只有在其onsuccess事件中才能得到获取到的结果。

添加数据

let transaction = db.transaction(['myObjectStore'], 'readonly')
let objectStore = transaction.objectStore('myObjectStore')
let request = objectStore.add({
  id: '100002',
  name: 'Zhang Fei',
})

添加数据使用add方法,传入一个object。但是这个object有限制,它的主键值,也就是id值,不能是已存在的,如果objectStore中已经有了这个id,那么会报错。因此,在某些程序中,为了避免这种情况的发生,我们使用put方法。

更新数据

let transaction = db.transaction(['myObjectStore'], 'readonly')
let objectStore = transaction.objectStore('myObjectStore')
let request = objectStore.put({
 id: '100002',
 name: 'Zhang Fei',
})

put方法和add方法有两大区别。一,如果objectStore中已经有了该id,则表示更新这个object,如果没有,则添加这个object。二,在另一种情况下,也就是设置了autoIncrement为true的时候,也就是主键自增的时候,put方法必须传第二个参数,第二个参数是主键的值,以此来确定你要更新的是哪一个主键对应的object,如果不传的话,可能会直接增加一个object到数据库中。从这一点上讲,自增字段确实比较难把握,因此我建议开发者严格自己在传入时保证object中存在主键值。

删除数据

let transaction = db.transaction(['myObjectStore'], 'readonly')
let objectStore = transaction.objectStore('myObjectStore')
let request = objectStore.delete('100001')

delete方法把你传入的主键值对应的object从数据库中删除。

游标 Cursor

在indexedDB中,也存在游标的概念,所谓游标,简单的理解,就是“一个用来记录数组正在被操作的某个下标位置的变量”,举个例子,你有一个数组[1,2,3,4],现在你要对它进行遍历,使用forEach方法,那么forEach方法怎么知道你上次操作到第几个元素,现在应该操作第几个元素呢?就是通过游标。游标是一个机制,你无法把游标打印出来看,而是通过游标得到你当前操作的元素,同时,游标也就意味着有类似于next的方法,可以用来移动游标到下一个位置。

获取全部object

想要获取一个objectStore中的全部object可不是一件简单的事。indexedDB没有直接提供类似的方法来获取。那么我们应该怎么办呢?利用游标。

let transaction = db.transaction(['myObjectStore'], 'readonly')
let objectStore = transaction.objectStore('myObjectStore')
let request = objectStore.openCursor()
let results = []
request.onsuccess = e => {
  let cursor = e.target.result
  if (cursor) {
    results.push(cursor.value)
    cursor.continue()
  }
  else {
    // 所有的object都在results里面
  }
}

通过openCursor方法打开游标机制,再其onsuccess事件中,如果cursor没有遍历完所有object,那么通过执行cursor.continue()来让游标滑动到下一个object,onsucess会被再次触发。而如果所有的object都遍历完了,cursor变量会是undefined。

注意上面蓝色的results,它的声明必须放在onsuccess回调函数的外部,因为该回调函数会在遍历过程中反复执行。

在Firefox中,浏览器自主实现了一个getAll方法,但是它不是标准的indexedDB的接口,因此不推荐使用,而本例的操作方法,则是获取全部object的标准做法。

通过index查询集合

前文说过,在创建objectStore的时候,可以为其创建index,在查询时,可以利用这些index来进行object的获取。

let objectStore = db.transaction([storeName], 'readonly').objectStore(storeName)
let objectIndex = objectStore.index('name')
let request = objectIndex.get('Li Hua')
let results = []
request.onsuccess = e => {
  let item = e.target.result
}

(index方法我们在后面介绍。)但是,你可以发现,这里的get方法只能获取一个object,假如同name的object有多个,应该怎么办呢?这时可以利用游标的特性。

let objectStore = db.transaction([storeName], 'readonly').objectStore(storeName)
let objectIndex = objectStore.index('name')
let request = objectIndex.openCursor()
request.onsuccess = e => {
  let cursor = e.target.result
  if (cursor) {
    results.push(cursor.value)
    cursor.continue()
  }
  else {
    // 所有的object都在results里面
  }
}

你可以发现,整个操作跟前面的获取所有object几乎一样,只不过这里通过index方法利用index进行了筛选。

你也可以总结出游标的使用,简单的说,就是对已知的集合对象(比如objectStore或indexView)进行遍历,在onsuccess中使用continue来进行控制。

IndexedDB API

本节主要把indexedDB中最常用的所有api进行列举,起到快速查阅的作用。注意,这里仅是最常用,基本可以覆盖90%的使用场景,但并不代表所有的api都在这里,你可以通过这里查阅全部。

IDBDatabase

也就是本文最开始使用open打开indexedBD得到的db对象idb,通过这节,了解这个idb有些什么接口可以被调用。

name

通过idb.name获取当前连接到的数据库的名字。这和你在open的时候传入的name是一致的。

version

和你在open的时候传入的version是一致的。

objectStoreNames

获取当前数据库的所有objectStore的name列表,是一个数组。

createObjectStore()

创建一个objectStore,有两个参数:

例子:

idb.createObjectStore('students', {keyPaht: 'id', autoIncrement: false})

deleteObjectStore()

删除一个objectStore,参数只有一个:

删除的时候,index也被删除了。

close()

关闭当前打开的这个数据库。关闭之后,任何操作都会报错。

transaction()

重头戏,开启一个事务,是后续操作的开始。前文已经讲过了,任何操作都是要在事务中进行的,对于一个database而言,如果要获取里面的数据,或者修改其中的某一个objectStore,都要通过这个方法来开启一个事务,在事务中进行操作。

它有两个参数:

我们用代码来看下用法:

let trans = idb.transaction(['students'], 'readonly')

这样就获得了一个database的事务容器。

database的api主要用到的就这些了,接下来,我们来看下,开启事务之后,我们可以干什么。

IDBTransaction

关于transaction的概念就不说了,这里主要看下,得到一个transaction之后可以干什么。

db

获取这个transaction是对哪个database进行操作的事务。

objectStoreNames

这个事务要对哪些objectStore进行操作,和你传入的objectStoreNames是一致的,是一个数组。

mode

读写权限,和你传入的mode一致。

abort()

终止该事务,一旦人为终止,你程序中的某些操作可能就不会再执行了。

objectStore()

获取事务中的某个objectStore的容器。它有一个参数:

获得该objectStore容器之后,就可以利用它进行objectStore的数据增删改查了。

let trans = idb.transaction(['students'], 'readonly')
let objStore = trans.objectStore('students')
// 利用objStore进行查询:
let request = objStore.get('100001')

注意,它的参数必须是在transaction的第一个参数数组中的。

IDBObjectStore

重头戏,objectStore是最核心的概念了,它的容器,也就是上面这段代码中的objStore,都可以进行哪些后续操作呢?

name

获取该objectStore的name。和创建的时候传入的name一致。

keyPath

和创建的时候传入的keyPath一致。

autoIncrement

和创建的时候传入的autoIncrement一致。

indexNames

获取该objectStore的所有索引的命名列表,和createIndex传入的第一个参数一致。

transaction

获取该objectStore所属的transaction的容器。一个objectStore的容器,只有在事务中才能得到,因此这个objectStore的容器一定属于某个事务,那么也就有对应的transaction容器。通过transaction容器,其实可以做很多操作,比如获取和该objectStore一起操作的其他objectStore的name。

get()

发起一个获取object的Request。Request我们还没有讲到,后面会讲。它有一个参数:

代码说话:

let objStore = trans.objectStore('students')
let request = objStore.get(100001)

而要得到最后查询到的object,需要在request的onsuccess中获取。所以后面讲完Request之后,你就能正确使用代码了。

add()

发起一个添加object的Request。它有一个参数:

还是用代码来看下:

let objStore = trans.objectStore('students')
let request = objStore.add({
  id: '100002',
  name: 'Li Hua',
})

delete()

发起一个删除object的Request。它有一个参数:

put()

发起一个更新某个object的Request。它有两个参数:

关于key非常难理解。当你在创建一个objectStore的时候,你可能会传入autoIncrement为true,这时,这个objectStore和我们经常使用的有点不同。比如你的主键是id,那么如果你在add或put的时候,不传这个id,id值会自动加1,你get到的object也会包含id属性。

当你在使用put方法去更新的时候,如果你的objectStore的autoIncrement是true,就必须传入第二个参数key,put方法会先通过key找到该object,然后在用object的内容去更新。而如果不传key,那么你传入的第一个参数object中必须包含id,否则会报错。

但是如果你的autoIncrement设置的是false,那就可以考虑忽略key。但是效果还是不一样,当你传入key值的时候,会更新传入的key对应的那个object。不传的时候,根据你object里面的主键来更新,没有的话会被认为是add操作,不会报错。

所以比较好的一种操作是,不设置autoIncrement,无论是添加还是修改object,都使用put,只要开发者自己注意,传入的object一定要有一个主键即可。这样当存在该主键值时,就更新,不存在时就插入。这比使用add好很多,因为add的时候,如果存在会报错。

如果你还有不理解的地方,欢迎在下方留言,对这个问题进行探讨。

count()

没有参数,发起一个查询当前objectStore的所有object的数量的Request。

clear()

没有参数,发起一个删除objectStore里面的所有object的Request。

清除数据之前请备份好数据。

openCursor()

发起一个打开游标的Request。它的参数统一在IDBCursor中讲。

index()

通过index方法,可以建立一个类似SQL数据库中视图。它先得到一个IDBIndex,然后你在对这个IDBIndex进行操作。因此它不是发起一个Request,这有别于前面的方法。返回的IDBIndex会在下文详细描述它的API。index方法由一个参数:

看下代码怎么用:

let objStore = trans.objectStore('students')
let objIndex = objStore.index('name')
let request = objIndex.get('Li Hua') // 下文会讲IDBIndex的api

createIndex()

创建一个索引,和前面的方法不同,它不是发起一个Request,而是直接进行操作,并且返回一个IDBIndex。关于IDBIndex,我会在Request后面再讲。它有三个参数:

虽然objectParameters还有其他选项,但是常用的就是unique。

deleteIndex()

删除一个索引,也不用发起Request。它有一个参数:

删除索引之后,就不能再根据索引查询数据了。

另外需要注意,创建和删除索引,都需要在打开db的Request的onupgradeneeded事件中完成。

IDBRequest

先来说下Request的概念。前面一直强调事务的概念,没有对Request进行介绍。Request是在事务过程中,发起某项操作的请求。一个事务过程中,可以有多个Request,Request一定存在与事务中,因此它肯定会有一个transaction属性来获取它所属于的那个事务的容器。

为什么要有Request呢?你可以把transaction当做一个队列,在这个队列中,Request在进行排队,每一个Request都只包含一个操作,比如添加,修改,删除之类的。这些操作不能马上进行,比如修改操作,如果你马上进行,就会导致大家同时修改怎么办的问题,把多个修改操作放在Request中,这些Request在transaction中排队,一个一个处理,这样就会有执行的顺序,修改就有前后之分。同时,transaction都可以被abort,这样当一系列的操作被放弃之后,后续的操作也不会进行。

而且非常重要的思想是,Request是异步的,它有状态,一个Request处于什么状态,可以通过readyStates属性查到,这对开发者而言也更可控。

目前,在indexedDB中,有四种可能产生Request:open database,objectStore request, cursor request, index request。

readyState

Request的状态。只有两种状态:pending, done。

transaction

获取该Request所属于的transaction。

source

获取该Request是由谁发起的,它可能有四种情况:objStore, cursor, index, null. 当该Request是open database的时候发起,source值为null。

通过该source值,其实可以获取更多信息,比如objectStore的其他信息。

result

获取该Request的输出结果。该值最开始是undefined,只有当Request成功之后,该值才会出现。因此,要获取一个get的最终结果,必须在Request的onsuccess事件中调用:

let request = objectStore.get(10001)
request.onsuccess = e => {
  let item = request.result
  // 等价于
  let item = e.tareget.result
}

IDBIndex

索引的概念就不讲了,这里主要是把最常用的有关所有的api列举出来,方便使用。

什么情况下会产生IDBIndex呢?当调用一个objectStore的index方法时,前文在讲IDBObjectStore的时候已经讲到过了。

let objStore = trans.objectStore('students')
let objIndex = objStore.index('name')

通过index方法之后,这个objIndex可以进行哪些操作呢?

name

该属性可以获取index的name。

keyPath

该属性可以获取index的keyPath,也就是object的某个属性的名字。这个你在createIndex的时候传入的keyPath是一致的。

unique

返回是否是唯一的,和你createIndex的时候传入的值是一致的。

isAutoLocale

返回一个boolean值,它表示这个index是否是自增的,和前面的autoIncrement有关。

locale

如果这个index对应的字段是自增的,那么现在的自增基础值是多少?比如你的id是自增的,现在objectStore里面有10个object,这时objIndex.locale应该是10.

objectStore

因为IDBIndex总是通过objectStore的index方法产生的,所以它自然会有对应的那个objectStore,而自己的objectStore属性正好是一个引用,指向那个产生自己的objectStore。

count()

发起得到当前index视图总共有多少个object的Request。

get()

发起从当前index获取一个值为传入参数的object的Request。它有一个参数:

这里的key有点难理解,前面objectStore的get方法传入的是主键的值,而这类的get传入的是你选择的这个索引对应的属性的值。比如说:

{
  id: 1,
  name: 'tom',
}

id是主键,name是一个索引。那么这个时候,如果你想得到这个object,应该使用:

let request = objIndex.get('tom') // 这个tom是name属性的值

openCursor()

发起一个打开游标的Request。它的参数统一在IDBCursor中讲。

可以看出,objIndex对于objStore而言,方法少了很多,它不能更新、删除等。但是实际上,通过objectStore属性,就可以反过来对objectStore进行操作。

IDBCursor

游标通过前文的阐述,应该比较容易理解了。这里我们来看下,一个cursor都可以进行哪些操作。

但是在开始之前,你要回头看下游标是怎么得到的?

openCursor()

objectStore或objectIndex可以使用这个方法打开一个游标的Request。它有两个参数:

打开游标后,游标怎么获得呢?在Request的onsuccess中得到的:

objStore.openCursor().onsuccess = e => {
  let cursor = e.target.result // 这个才是我们要的游标
}

接下来就是看看,这个cursor有哪些接口。

direction

游标遍历的方向,方向前面说过了,和你openCursor的时候传入的是一致的。

key

把当前游标所在的那个object的值返回给你,这个值是你使用index方法时,选择的那个索引对应的属性的值。有点绕,举个例子,如果游标当前停留在:

{
  id: 2,
  name: 'sue',
}

而你开启这个游标,又是通过objStore.index('name').openCursor()开启的,那么这个时候这个key值,就是sue了。

primaryKey

游标遍历所在位置的object的主键。这个比较难理解,key和primaryKey的区别主要体现在你用索引进行检索的时候,如果你openCursor是在非索引集合情况下,其实key和primaryKey是一样的,因为都是以主键作为索引。但是当你用index方法筛选出子集,那么这个时候primaryKey在某些情况下就非常有必要知道。

source

该属性返回开启游标的容器的引用,也就是objStore或objIndex。

value

改属性返回当前遍历到的object,严格的说,这个属性是IDBCursorWithValue的属性。

continue()

游标往下移动一格或移动到你规定的位置。注意,它是根据你规定的方向进行移动的。它可以有一个参数:

advance()

continue()是只移动一格,advance()可以自己规定移动的格数。它有一个参数:

continuePrimaryKey()

前面讲了primaryKey这个属性,也讲了continue(key)这个方法的参数key。但是,当你在openCursor的时候,索引的值可重复时,那就会出现尴尬的情况。在objectStore里面存储数据的时候,primaryKey和其他索引的key,它们的值可能是重复的,但是如果通过两个key的值来确定呢?那么就能更准确的定位某一个object。因此,当你使用continuPrimaryKey的时候,是为了解决这个问题,即continue()只会根据参数,往下移动到下一个给定参数值的object,而如果使用continuePrimaryKey(),那么在往下一个给定值移动时,还会再考虑primaryKey的值。

一般来说primaryKey的值都是唯一的,但也不排除有些情况不唯一的时候,这个时候,使用游标结合continuePrimaryKey才能正确获得你想要的那个object,通过普通的get只能得到第一个object。

delete()

发起一个删除当前游标所在的object的Request。删除之后,就要考虑用continuePrimaryKey(),而不是continue()。

update()

发起一个更新当前游标所在的这个object的对应字段的值的Request。它有一个参数:

用Promise对事务进行封装

既然indexedDB的事务机制这么复杂,又有transaction,又有Request,都需要在它们的onsuccess中的回调函数中去获取result之类的。既然是回调,那么我们就可以使用Promise来进行封装,让回调嵌套不再那么痛苦。

基于这个想法,我封装了一个类IndexDB,你可以在GitHub上获得源码。接下来我们我们来看下class IndexDB的接口。

constructor

实例化类,要么是创建一个不存在的数据库,要么打开一个已经存在的数据库,打开的时候,还可以对已有的数据库结构进行修改。

创建一个数据库

通过下面的方式创建一个数据库。

let db = new IndexDB({
  name: 'mydb',
  version: 1,
  stores: [
    {
      name: 'store1',
      primaryKey: 'id',
      indexes: [
        {
          name: 'id',
          key: 'id',
          unique: true,
        },
        {
          name: 'user',
        },
      ],
    },
    {
      name: 'store2',
      primaryKey: 'id',
    }
  ],
  defaultStoreName: 'store1',
})

通过上面的代码,可以创建一个名为mydb的数据库,version为1,包含了两个objectStore,其中store1还有两个索引。当数据库打开之后,默认操作store1.

打开一个已有的数据库

上面我们已经创建了mydb这个数据库。当你刷新当前页面的时候,上面这段代码会打开mydb这个数据库,无需做任何改动。

这里你要回到真实的使用场景,我们是用Javascript把代码写在一个网页中的,indexedDB是用户浏览器中的数据库,用户可能会反复打开这个网页,因此,上面这段代码并没有变化,它的作用是如果存在mydb,就打开它。

修改已有数据库结构

有一天,你作为开发者,想要调整你的数据库结构,那么怎么办?你需要发布新的代码,新代码中,需要把version进行调整,升级为新的version,代码调整如下:

let db = new IndexDB({
  name: 'mydb',
  version: 2,
  stores: [
    {
      name: 'store1',
      primaryKey: 'id',
      indexes: [
        {
          name: 'id',
          key: 'id',
          unique: true,
        },
        {
          name: 'user',
          unique: true,
        },
        {
          name: 'email',
          unique: true,
        },
      ],
    },
    {
      name: 'store2',
      primaryKey: 'id',
    }
  ],
  defaultStoreName: 'store1',
})

当用户再次访问你的应用的时候,会加载新的代码,新的代码里面version值变大了,这个时候会触发修改数据库结构的动作,修改的部分用红色标注出来了。

options

在实例化的时候,传入的options如下:

{
  name: 'mydb', // 必须,数据库名
  version: 1, // 必须,数据库版本
  stores: [ // 可选,创建的时候必须,包含数据库中所有的objectStore信息,数组中的一个元素就是一个objectStore
    {
      name: 'store1', // 必须,objectStore的name
      primaryKey: 'id', // 必须,objectStore的keyPath
      indexes: [ // 可选的,objectStore的索引信息
        {
          name: 'id', // 必须,索引的名称
          key: 'id', // 可选,如果不传,则直接使用index的name的值
          unique: true, // 可选,默认false
        },
        {
            ...
        },
      ],
    },
    ...
  ],
  defaultStoreName: 'store1', // 必须,数据库打开之后,默认连接到哪个objectStore
}

get(key)

通过get方法获取当前选中的objectStore中的object。这个key是primaryKey的值。

用代码说话:

db.get('100001').then(obj => {
   console.log(obj)
})

前面我们实例化出来的db直接调用get方法,返回一个Promise实例。key就是我们前面定义的primaryKey的值,比如在store1中有一个object:

{
  id: '100001',
  user: 'Li Hua',
  email: 'lihua@xxx.com',
}

那么,这个object就会被返回到then中。

query(key, value)

查询出某个索引键为某值的object的集合,返回这个集合为一个数组。两个参数:

上代码:

db.query('email', 'lihua@xxx.com').then(objs => {
  console.log(objs)
})

那么上面那个object又被检索出来了。因为主键一般都会加入到索引中,所以也可以通过这个方法来实现主键的get方法。

all()

没有参数,检索出objectStore中的所有object。也是Promise用法。then中返回的是object的数组集合。

add(item)

添加一个对象到objectStore中。

put(item)

更新一个对象,如果该对象不存在,则添加这个对象。

根据primaryKey,如果objectStore中已经有一个object的primaryKey为传入的item的primaryKey的值,那么表示更新,否则就是添加。

del(key)

删除一个对象。

close()

关闭当前打开的数据库。注意,它不返回任何值。而且,它是异步的。

use(storeName)

切换objectStore。当你打算向另外一个objectStore写入数据时,要使用use方法进行切换。

$store(mode)

获取当前操作的objectStore的容器。

代码说话:

db.$store().then(objectStore => {
  let request = objectStore.get(1001)
  request.onsuccess = e => {
    ...
  }
})

之所以要提供这个方法,是为了当IndexDB class本身不提供直接api时,利用功能扩展。

$connect()

获得当前打开的数据库容器。

db.$connect().then(db => {
  console.log(db.objectStoreNames) // 你可以知道当前数据库的所有objectStore
})

扩展

因为IndexDB是一个ES6的class,因此可以方便的使用extends关键字对它进行扩展,比如你想添加一个借口:

class MyIndexDB extends IndexDB {
  clear() {
    this.$store().then(objectStore => objectStore.clear())
  }
}

这样就添加了一个接口,用来清除当前活动的objectStore的所有数据。实例化的时候,实例化MyIndexDB即可。

小结

本文所有的内容就讲完了,本文从IndexedDB的概念和结构开始,把所有常用的API都讲了一遍,并且还给出了很多例子。

有人做过一个预测,Web SQL数据库已经从标准中移除了,有可能在后面的浏览器版本中被去除,IndexedDB将有可能统一浏览器本地化存储的数据库。

虽然本文对大部分IndexedDB的接口都进行了讲解,但是还只是入门教程,还有很多东西没讲。举个例子,作为数据库,最重要的就是检索功能,也就是索引检索的那个部分,但是本文并没有对这个部分展开。索引检索还可以实现阈值检索,比如检索某一段日期内的所有object,比如检索所有同姓氏的同学,这些问题都是经常遇到的,但都不再本文的讲解范围内。想要完全了解indexedDB,还需要你继续深入学习。

如果你觉得本书对你有帮助,通过下方的二维码向我打赏吧,帮助我写出更多有用的内容。

2017-09-09