HelloIndexedDB开发文档

最便捷的indexedDB操作工具,10分钟上手indexedDB操作

前言

indexedDB是HTML5标准,是前端实现结构化数据存储的最关键因素。基于indexedDB驱动,前端应用可以做到更加厉害的应用体验。和localStorage相比,indexedDB的最主要优势在于,indexedDB是一个完整的数据库模型,它具有数据库应该有的库表结构,丰富的查询方式等等。除此之外,localStorage的容量在5M左右,而indexedDB达到惊人的500M。indexedDB还有可以存储arraybuffer,进程为异步等特征,这些特质都使得使用indexedDB作为前端数据存储的不二之选,localStorage降级为用于存储散列的快速读取配置项的存储工具。

但是,indexedDB有一个缺点,由于它需要支持完整的数据库驱动模型,因此,api较为复杂,概念也是一层套一层。我写过《indexedDB中文教程》较为全面的去介绍它,然鹅,当我发现了新知识之后,还在不断的去补充那篇教程,可见,如果一个初级开发者要使用原生的indexedDB api去写代码,会是一件痛苦的事。为了解决这个痛点,我决意写一个优秀的indexedDB操作库。虽然市面上已经有很多库了,但是它们很多都基于indexedDB原生操作理念去实现。而HelloIndexedDB则将api抽象再抽象,让开发者感觉不到原生api的复杂性,像是学习一个极其简单的存储工具一样使用。

那么HelloIndexedDB到底有多简单呢?这篇文章全面的去介绍它的使用方法。

基础概念

虽然HelloIndexedDB最简化了indexedDB的操作,但是,由于indexedDB和localStorage完全不同,所以,关于这个数据库的基本概念要事先了解。

对于一个数据库引擎,我们要记录一条数据,首先需要打开一个数据库,然后在这个数据库里面选择一个store(也就是sql数据库里面的表),之后才能进行增删改查的工作。indexedDB完整的继承这一模型,你需要了解,在使用HelloIndexedDB时,有“数据库”“store”“记录”这三层概念存在,具体反应到代码中,这三个概念会在实例化参数中反应出来。

其次是关于keyPath的概念。indexedDB存的数据和localStorage结构不一样,localStorage存的是键值对,而indexedDB存的是object,也就是js对象。就像sql里面一样,要找到一条记录需要通过它的字段去查,而在indexedDB中,被存的object的属性名就相当于字段名,属性值就相当于字段值。而keyPath简单的讲就是object的属性名。但是,indexedDB的强大之处在于,keyPath可以是链式的多级属性。比如有一个person对象里面深层级的值可以用person.body.head.left_eye获得,而这个时候"body.head.left_eye"就是这个值相对于person对象的keyPath。

再次是关于key的概念。indexedDB的key的概念有点别扭,它代表的是键值,而非键名(键名是用keyPath表示)。

最后是关于索引的概念。indexedDB如其名,它的特色就是index(索引)。indexedDB具备了强大的索引系统,它根据keyPath创建索引,根据indexName查询对象记录,而且还可以实现值域查询。总而言之,如果你想利用indexeDB快速查询数据,那么索引要好好利用。

以上就是你在使用HelloIndexedDB之前必须了解的概念。其他indexedDB原生概念可以暂时不用了解。

快速入门

本节让你快速使用HelloIndexedDB进行数据的存取。

包的安装和使用

HelloIndexedDB是一个npm包,通过npm进行安装:

npm i -S hello-indexeddb

安装之后,你可以根据你的编程需求选择不同的使用方式:

// ES6
import HelloIndexedDB from 'hello-indexeddb'
// CommonJS
const { HelloIndexedDB } = require('hello-indexeddb')

在HTML中直接使用时,你需要把hello-indexeddb/dist目录下的hello-indexeddb.js文件拷贝到你的本地目录:

<script src="hello-indexeddb/dist/hello-indexeddb.js"></script>
<script>
const { HelloIndexedDB } = window['hello-indexeddb'] // 注意这里的使用方法
</script>

实例化

通过上面的几种方式,你就可以得到一个HelloIndexedDB的类,接下来,就是实例化,得到一个对象。

const store = new HelloIndexedDB()

修改和获取数据

接下来,你就可以像localStorage一样,使用这个store,不过,由于indexedDB是异步操作的,你的所有操作都必须是异步完成。

let obj = await store.getItem('key')
await store.setItem('key', 'value')

这样我们就完成了对HelloIndexedDB的初步认识。下文,我们会分开详细介绍它的各个细节方面。

实例化参数

这一节将会是HelloIndexedDB整个使用中最难的部分,一旦掌握了本节的内容,其他的方面完全就无需担忧了。

在上一节实例化的时候,我没有传入任何参数,这种做法是最简单的做法,但这样不能发挥出indexedDB的强大功能,相当于把它当作一个localStorage的替代品,没意思。本节我们学习实例化参数,这些参数被用来作为对indexedDB进行操作前的准备工作。

参数列表

实际上,只有一个参数,而这个参数必须是一个对象:

{
  name: 数据库的名称,可选,不传时使用'HelloIndexedDB'作为数据库名
  version: 可选,数据库的版本,大于0的整数,当本参数列表内的任何值发生变化时,需要加1
  stores: 可选,一个数组,用来规定这个数据库中拥有哪些store,前面已经讲过了数据库的3层基本概念,store相当于表
  use: store的名字,用来表示当前实例将会选择哪个store进行操作,可选,可不传,后面用.use方法选择
}

上面就是所有参数字段,看上去非常简单。但是不要急,复杂的东西在后面。

store的配置

上面的stores参数是一个数组,而数组的每一个元素,代表要创建怎样的一个store出来。

{
  name: store的名称,必填
  keyPath: 主键名,也就是说,一个object的什么属性(路径)会被作为这个store的最主要查询依据
  indexes: 索引列表
  autoIncrement: 主键是否自增,设置为true时,在向store添加对象的时候,主键值可不传
  isKeyValue: 是否开启key-value模式,只有开启key-value模式的store才会拥有getItem/setItem/removeItem这几个方法,并且用这几个方法像操作localStorage一样操作indexedDB,不开启的情况下,不能使用这几个方法
}

上面比较难理解的是autoIncrement,所以,我的建议是不设置这个值。

index的配置

上面的indexes配置项是个数组,而其中每个元素代表要给这个store创建怎样的索引。

{
  name: index的name,查询的时候用到
  keyPath: 索引对应object的什么属性(路径)
  unique: 是否为唯一索引,为true的时候,整个store里面该keyPath对应的值不能有重复
}

上面比较难理解的是keyPath,index的keyPath和store的keyPath理解上不一样。index的keyPath是指,当你用index的name去查询的时候,实际要查询的object的属性名,indexName相当于是indexKeyPath的一个别名,方便在查询的时候使用较短的字符串去代替。

小结

上面就是所有的参数了,它们之间存在一些内置的关联性,例如设置了isKeyValue为true时keyPath和autoIncrement无效。关于这一部分内容,你可以不用太了解,实在想了解,就得看indexedDB的原生接口和hello-indexeddb的源码。

基础API

本节介绍通用API,利用这些api你可以快速操作indexedDB。这些方法全部为实例化对象的方法,使用时,需先得到实例化对象。每个实例对应一个库,在进行操作时,一个操作只能对应一个store,不能同时用一个实例操作多个store。你可以通过use方法得到对不同store的实例,通过它们来操作不同store。

get

根据store的key获取一个对象。key是keyPath的值。

let value = await store.get(key)

add

向store中插入一条新记录。

await store.add({ id, name, age })

相当于sql中的insert操作,需要注意,indexedDB中,对add的数据格式有要求,必须符合下面其中之一

如果add的对象key值已经存在于store中,会报错,因此,比较常规的操作是用put代替add。

put

更新store中的一条数据,如果传入对象的key值不存在,就插入这条数据。

await store.put({ id, name, age })

put中key值其实也遵循上面add的规律,如果autoIncrement设置为true,那么不给key时,就是添加,给key时,就是更新。

一般,在代码中我们尽量少使用autoIncrement配置,因为我们很难完全掌握数据的实际情况。我们尽量使用put,而不是add,因为代码一旦部署,它就不能修改,如果在执行中add报错,那么很可能阻塞你的程序。

delete

从store中删除一条数据。

await store.delete(key)

传入key值。

以上是HelloIndexedDB中最基础的api,利用这些api,就可以实现最基础的数据增删改查了。

remove

从store中删除一条数据,和delete不同的是,remove直接接收这个对象。

await store.remove(obj)

它会自动去找到obj上的keyPath,然后用delete去删除。

另外,使用delete的时候,你需要传入的是keyPath的值,而如果你的keyPath是多级路径,例如'body.hands'这样的路径,那么要取到key也是挺麻烦的,你需要自己有一套解析keyPath的工具。而HelloIndexedDB内置了这样的功能,使用remove不用担心这个问题,只要你的obj是一个和数据库中记录对应的上的对象,就能被正确删除。

clear

清空store中的所有数据,注意备份!!

awiat store.clear()

查询API

基础api仅仅是最基础功能的实现,还没有涉及indexedDB真正强大的地方。indexedDB真正强大的地方是利用索引进行查询,因此,我们要发挥这些能力,我们就会用到下面的api。

find

从store中找到第一条indexName为某值的记录。

let obj = await store.find(indexName, value)

要使用find的前提是,你的store在配置时传入了对应的index,否则是不能根据索引找到值的。

const store = new HelloIndexedDB({
  name: 'my-idb',
  stores: [
    {
      name: 'my-store',
      keyPath: 'id',
      indexes: [
        {
          name: 'indexName',
          keyPath: 'age',
        },
      ],
    },
  ],
})

注意上面红色的部分,只有当你在创建store的时候,设置了这个索引,才能使用find去使用它:

let obj = await store.find('indexName', 10)

这样就可以找到第一条age=10的数据了,相当于sql里面的where age=10 limit 1的子句。

注意,我这里传入的是indexName,但是indexedDB实际会使用keyPath,也就是'age'字段作为查询条件。关于keyPath前面已经解释过了,这里不再赘述。

query

查询出store中所有满足条件的记录,返回一个数组。

let objs = await store.query(indexName, value, compare)

find仅仅找到第一条数据,而query则可以找出所有满足条件的记录。和find一样,它也必须是index支持的才能使用。

let objs = await store.query('indexName', 10)

这样就可以查出age=10的所有记录。

query比find更强大的地方在于,它支持条件筛选,它的第三个参数可以传入比较运算符。

let objs = await store.query('indexName', 10, '>')

表示查出所有age>10的记录,支持的条件如下:

在使用!=、%和in的时候一定要注意,由于indexedDB内部还不支持这3种查询方式,但HelloIndexedDB可以支持。说到这里,可以将index配置项里面都只配置name属性,这样keyPath属性会自动使用name属性。

const store = new HelloIndexedDB({
  name: 'my-idb',
  stores: [
    {
      name: 'my-store',
      keyPath: 'id',
      indexes: [
        {
          name: 'body.head', // 这样可以保证name和keyPath的值是一样的
        },
      ],
    },
  ],
})

不过上面也只是建议,最后根据你的实际情况来确定。

select

查询出store中所有满足条件的记录,返回一个数组。

let objs = await store.select([
  { key: 'name', value: 'c', compare: '>' },
  { key: 'age', value: 10, compare: '>' },
  { key: 'age', value: 20, compare: '<' },
])

虽然indexedDB很强大,但是仅支持单索引查询,而无法做到多个索引同时联系起来查询。为了实现这一功能,我实现了select方法,这个方法可以让你同时使用多个查询条件,并且取一定的逻辑关系。例如上面这段代码,它可以查询出name>'c'同时10<age<20的所有记录。

select接收一个数组,数组中的每个元素都是一个查询条件,它可以包含如下属性:

optional这个选项有点难理解,简单的说,它就是sql里面的OR,而select方法的查询方式非常简单,没有optional或者optional为false的,被认为是AND子句,先进行查询,所有optional为true的被视为一个整体,最后进行查询,在满足AND的基础上,只有满足任意一条OR的子句的记录才会在结果集合中,否则就不在。而如果你的条件里面,全部都是optional为true,那么相当于只有一条AND语句,也就是说只要记录满足任何一条规则,即可被返回。

let objs = await store.select([
  { key: 'name', value: 'c', compare: '>' },
  { key: 'age', value: 10, compare: '<', optional: true },
  { key: 'age', value: 20, compare: '>', optional: true },
])

这条查询相当于 name>'c' AND (age<10 OR age>20)。所有类似的语句都能转化为A AND B AND C AND (D OR E OR F)这样的句子,你只要记住所有OR子句被视作一个整体最后进行判断,就可以轻松理解了。

使用select需要注意,它不基于indexedDB的索引,因此,key值不能传indexName值,而必须传对象的属性路径/keyPath值。为了使用统一性,我后期会考虑key值可以传indexName值,不过,虽然支持,但由于indexedDB本身的限制,它仍然无法通过索引检索,而是只能遍历整个store,取出符合条件的部分。

first

返回store中的第一条数据。

let firstRecord = await store.first()

last

返回store中的最后一条数据。

let lastRecord = await store.last()

some

返回几条连续的数据。

let someRecords = await store.some(5, 3)

表示从索引值为3开始,返回连续的5条记录。

当offset小于0时,查询将会移到所有记录的尾部,例如:

let someRecords = await store.some(3, -5)

则会从倒数第5个记录开始拿取,一共拿出3个。

all

返回store中的所有数据,结果为数组。

let allRecords = await store.all()

count

返回store中所有数据的条数,结果为数字。

let count = await store.count()

keys

返回store中所有记录的主键值,结果为一个数组。

let keys = await store.keys()

上述就是所有HelloIndexedDB提供的查询api,这些api具有非常简洁易用的特征,而且很多情况下,你并不需要复杂的api,用上面这些api完全可以满足你的查询需求。

Storage API

在HelloIndexedDB里面,有几个特殊的API,它们和原生的Storage api命名保持一致。要使用这些api,你必须在创建store时,在store的参数中使用isKeyValue为true,这样做之后,keyPath配置,autoIncrement配置都将失效,但它可以把当前这个store转化为一个支持Storage api的store。

const store = new HelloIndexedDB({
  name: 'my-idb',
  stores: [
    {
      name: 'my-store',
      isKeyValue: true,
    },
  ],
  use: 'my-store',
})

这样配置之后(注意,在你的代码第一次发布上线之前就必须这样做,而不能等到后面发布新版本的时候再调整,否则你必须通过升级version值来办到),这个store就可以调用如下api。

getItem

获取对应key的值。

let value = await store.getItem(key)

setItem

设置对应key的值。

await store.setItem(key, value)

removeItem

删除对应的key-value。

awiat store.removeItem(key)

以上就是Storage api,这使得使用HelloIndexedDB之后,可以像使用localStorage一样操作indexedDB,非常方便。

扩展API

这一些特殊情况下,你可能还需要更奇特的操作,HelloIndexedDB提供了几个可能对你有帮助的api,利用它们,你可以办到任何你想干的事。

each

遍历当前store。

await store.each((item, i) => {
  // ..
})

通过each,可以遍历整个store,这样,如果当上面的api不能满足你的查询需求的时候,你可以自己写一个遍历操作来搜集所有满足你的条件的记录。

each接收一个函数,函数可以有如下参数:

reverse

逆序遍历整个store。

和each用法一摸一样,只不过迭代的方向是从store的最后一条往第一条。

原生API

HelloIndexedDB提供了一套可以快速进入原生操作的api,它可以帮助你不需要写太多原生代码,就可以拿到indexedDB的原生对象,在这些原生对象的基础上进行下一步操作。

request

基于当前store发起一个request。

这是一个非常强大,但理解起来比较复杂的api,如果你不是很了解原生的indexedDB接口,可能使用起来就有些吃力。

await store.request(prepare, writable)
  • prepare: 基于当前store发起request的准备动作,是一个函数,函数必须返回一个request实例
  • writable: 是否以可写模式运行,默认为只读,设置为true后这个request是可写的
store.request(objectStore => objectStore.get(key)).then(obj => console.log(obj))

HelloIndexedDB内置的很多方法都是通过这种形式调用原生api实现的。

db

快速获取当前database。

let db = await store.db()

objectStore

快速获取当前store。

let objectStore = await store.objectStore()

keyPath

快速获取当前store的主键名。

let keyPath = await store.keyPath()

transaction

基于当前store,快速开启一个事务,而且不用担心事务的可用性。

  • writable: 是否开启一个可写的事务,默认是关闭的
let tx = store.transaction(true)

数据库管理

前面的所有一切,都是只对当前的store进行操作,如果你希望切换到另外一个store进行操作怎么办呢?

use

基于当前的数据库配置,切换到另外一个store。

const store2 = store.use('store2')

这时的store2和store一样,都是一个HelloIndexedDB的实例,拥有以上所有api。在你的项目里面,你不需要同时用HelloIndexedDB去实例化同一个库下的不同store(而且这也是做不到的),而是应该通过use来切换store:

export const store1 = new HelloIndexedDB(options)
export const store2 = store1.use('store2')

注意:use是一个同步方法,和其他大部分方法不同,它不返回一个promise,而是直接返回一个HelloIndexedDB实例。

close

关闭当前数据库连接。几乎不会在HelloIndexedDB里面用到。

version

由于indexedDB的特点是,你一旦创建数据库之后,不可以随意修改数据的任何结构,因此,你不能通过修改代码来实现数据库结构调整。如果哪一天,你真的需要调整数据了怎么办?

你需要明白一个道理,indexedDB是在用户的浏览器的,你不可能像MySQL一样自己去执行一些脚步一次性修改。怎么做呢?就是在你新发布的代码中,增加version,并且修改options。

前端项目的脚步很多都会放在CDN,在你下一次进行发布时,你需要修改你的代码:

export const store = new HelloIndexedDB({
  name: 'my-idb',
  version: 2,
  stores: [
    {
      name: 'my-store',
      isKeyValue: true,
    },
    {
      name: 'my-store2',
      keyPath: 'id',
    },
  ],
  use: 'my-store',
})
export const store2 = store.use('store2')

上面的代码表示,我在新版本中,新增了一个store,并通过升级version来使得这个修改生效。一次修改,必须基于前一个版本进行,否则你会发现,你的数据库管理非常混乱。

结语

我从去年开始不断深入学习indexedDB,甚至对google的leveldb都进行了一定的了解,很多人都不知道,chrome里面的indexedDB有一个特点:读慢写快,如果不是深入了解,根本不会明白为什么会出现这样的特点。在整个学习过程中,我发现,对于开发者而言,了解这些深层次的知识虽然是有必要的,但是却无法快速入门,上手使用,每天要写重复的request代码,会让人崩溃。一个真正好的开发工具,是帮助开发者抹平这些复杂的凹凸不平的底层信息,只给开发者最直观好用的结果。这也是我这两年来越来越注重的事。如果你觉得这个项目还不错,请在我的github上点star,让更多人了解它。

去github点star

(完)

 

2018-09-15