IndexedDB 打造靠谱 Web 离线数据库

在知乎和我在平常工作中,常常会看到一个问题:

前端现在还火吗?

这个我只想说:

隔岸观火的人永远无法明白起火的原因,只有置身风暴,才能找到风眼之所在 ——『秦时明月』

你 TM 看都不看前端现在的发展,怎么去评判前端火不火,我该不该尝试一下其他方面的内容呢?本人为啥为这么热衷于新的技术呢?主要原因在于,生怕会被某一项颠覆性的内容淘汰掉,从前沿领域掉队下来。说句人话就是: 。所以本文会从头剖析一下 在前端里面的应用的发展。

indexedDB 目前在前端慢慢得到普及和应用。它正朝着前端离线数据库技术的步伐前进。以前一开始是 manifest、localStorage、cookie 再到 webSQL,现在 indexedDB 逐渐被各大浏览器认可。我们也可以针对它来进行技术上创新的开发。比如,现在小视频非常流行,那么我们可以在用户观看时,通过 cacheStorage 缓存,然后利用 WebRTC 技术实现 P2P 分发的控制,不过需要注意,一定要合理利用大小,不然后果真的很严重。

indexedDB 的整体架构,是由一系列单独的概念串联而成,全部概念如下列表。一眼看去会发现没有任何逻辑,不过,这里我顺手画了一幅逻辑图,中间会根据 函数 的调用而相互串联起来。

IDBRequest

IDBFactory

IDBDatabase

IDBObjectStore

IDBIndex

IDBKeyRange

IDBCursor

IDBTransaction

整体逻辑图如下:

TL;DR

下文主要介绍了 indexedDB 的基本概念,以及在实际应用中的实操代码。

indexedDB 基础概念。在 indexedDB 里面会根据索引 index 来进行整体数据结构的划分。

indexedDB 数据库的更新是一个非常蛋疼的事情,因为,Web 的灵活性,你既需要做好向上版本的更新,也需要完善向下版本的容错性。

indexedDB 高效索引机制,在内部,indexedDB 已经提供了 、 等高效的索引机制,推荐不要直接将所有数据都取回来,再进行筛选,而是直接利用 进行。

最后推荐几个常用库

离线存储

IndexedDB 可以存储非常多的数据,比如 Object,files,blobs 等,里面的存储结构是根据 Database 来进行存储的。每个 DB 里面可以有不同的 object stores。具体结构如下图:

并且,我们可以给 设定相关特定的值,然后在索引的时候,可以直接通过 key 得到具体的内容。使用 IndexDB 需要注意,其遵循的是同域原则。

indexDB 基本概念

在 indexDB 中,有几个基本的操作对象:

Database: 通过 方法直接打开,可以得到一个实例的 DB。每个页面可以创建多个 DB,不过一般都是一个。

Object store: 这个就是 DB 里面具体存储的对象。这个可以对应于 SQL 里面的 table 内容。其存储的结构为:

index: 有点类似于外链,它本身是一种 Object store,主要是用来在本体的 store 中,索引另外 object store 里面的数据。需要区别的是,key 和 index 是不一样的。可以参考: index DEMO,mdn index。如下图表示:

如下 code 为:

transaction: 事务其实就是一系列 CRUD 的集合内容。如果其中一个环节失败了,那么整个事务的处理都会被取消。例如:

cursor: 主要是用来遍历 DB 里面的数据内容。主要是通过 来进行控制。

如何使用 IndexDB

上面说了几个基本的概念。那接下来我们实践一下 IndexDB。实际上入门 IndexDB 就是做几个基本的内容

打开数据库表

设置指定的 primary Key

定义好索引的 index

前期搭建一个 IndexedDB 很简单的代码如下:

上面主要做了 3 件事:

打开数据库表

新建 Store,并设置 primary Key

设置 index

打开数据库表主要就是版本号和名字,没有太多讲的,我们直接从创建 store 开始吧。

创建 Object Store

使用的方法就是 IDBDatabase 上的 方法。

基本函数构造为:

keyPath: 用来设置主键的 key,具体区别可以参考下面的 keyPath 和 generator 的区别。

autoIncrement: 是否使用自增 key 的特性。

创建的 key 主要是为了保证,在数据插入时唯一性的标识。

不过,往往一个主键(key),是没办法很好的完成索引,在具体实践时,就还需要辅键 (aid-key) 来完成辅助索引工作,这个在 IndexDB 就映射为 。

设置索引 index

在完成 PK(Primary key) 创建完毕后,为了更好的搜索性能我们还需要额外创建 。这里可以直接使用:

indexName: 设置当前 index 的名字

property: 从存储数据中,指明 index 所指的属性。

其中,options 有三个选项:

unique: 当前 key 是否能重复 (最常用)

multiEntry: 设置当前的 property 为数组时,会给数组里面每个元素都设置一个 index 值。

具体可以参考:MDN createIndex Prop 和 googleDeveloper Index。

增删数据

在 IndexedDB 里面进行数据的增删,都需要在 中完成。而这个增删数据,大家可以理解为一次 ,相当于在一个 里面管理所有当前逻辑操作的 。所以,在正式开始进行数据操作之前,还需要给大家简单介绍一些如果创建一个事务。

事务的创建

API,如下 [代码1]。在创建时,你需要手动指定当前 transaction 是那种类型的操作,基本的内容有:

"readonly":只读

"readwrite":读写

"versionchange":这个不能手动指定,会在 回调事件里面自动创建。它可以用来修改现有 object store 的结构数据,比如 index 等。

你可以通过在数据库打开之后,通过 上的 方法创建,如 [代码2]。

事务在创建的时候不仅仅可以制定执行的模式,还可以指定本次事务能够影响的 ObjectStore 范围,具体细节就是在第一个 参数里面传入的是一个数据,然后通过 方法打开多个 OS 进行操作,如下 [代码3]。

操作数据

完成了事务的创建之后,我们就可以正式的开始进行数据的交互操作了,也就是写我们具体的业务逻辑。如下 [代码1],一个完整数据事务的操作。

通过 回调得到的 IDBObjectStore 对象,我们就可以进行一些列的增删查改操作了。可以参考 [代码2]。详细的可以参考文末的 。

索引数据

索引数据是所有数据库里面最重要的一个。这里,我们可以使用游标,index 来做。例如,通过 index 来快速索引 key 值,参考 [代码1]。

更详细的内容,可以参考下文 数据索引方式。

keyPath 和 key Generator

何谓 keyPath 和 keyGenerator 应该算是 IndexedDB 里面比较难以理解的概念。简单来说,IndexedDB 在创建 Store 的时候,必须保证里面的数据是唯一的,那么得需要像其它数据库一样设置一个 来区分不同数据。而 keyPath 和 Generator 就是两种不同的设置 key 的方式。

设置 keyPath

因为 ssn 在该数据集是唯一的,所以,我们可以利用它来作为 保证 的特性。或者,可以设置为自增的键值,比如 类似的。

使用 generator

generator 会每次在添加数据时,自动创建一个 unique value。这个 unique value 是和你的实际数据是分开的。里面直接通过 来设置即可。

indexDB 打开注意事项

检查是否支持 indexDB

版本更新: indexDB

在生成一个 indexDB 实例时,需要手动指定一个版本号。而最常用的

这样会造成一个问题,比如上线过程中,用户A第一次请求返回了新版本的网页,连接了版本2。之后又刷新网页命中了另一台未上线的机器,连接了旧版本1 出错。主要原因是:

indexedDB API 中不允许数据库中的数据仓库在同一版本中发生变化. 并且当前 DB 版本不能和低版本的 version 连接。

比如,你一开始定义的 DB 版本内容为:

如果此时,用户先打开了 version(1),但是后面,又得到的是 version(2) 版本的 HTML,这时就会出现 error 的错误。

参考:

版本更替

版本更新

这个在 IndexDB 是一个很重要的问题。主要原因在于

indexedDB API 中不允许数据库中的数据仓库在同一版本中发生变化. 并且当前 DB 版本不能和低版本的 version 连接。

上面就可以抽象为一个问题:

你什么情况下需要更新 IndexDB 的版本呢?

该表数据库里面的 时。

你需要重新设计数据库表结构时,比如新增 index

不过,如果直接修改版本号,会出现这样一个 case:

由于原始 HTML 更新问题,用户首先访问的是版本 1 的 A 页面,然后,访问更新过后的 B 页面。这时,IndexDB 成功更新为高版本。但是,用户下次又命中了老版本的 A 页面,此时 A 中还是连接低版本的 IndexDB ,就会报错,导致你访问失败。

解决办法就是,设置过滤,在 的时候,手动传入版本号:

不过,这样又会造成另外一个问题,即,数据迁移(老版本数据,不可能不要吧)。这里,IndexDB 会有一个 updateCallback 给你触发,你可以直接在里面做相关的数据迁移处理。

在使用的时候,一定要注意 DB 版本的升级处理,比如有这样一个 case,你的版本已经是 3,不过,你需要处理版本二的数据:

对于存在版本 2 数据库的用户来说是 OK 的,但是对于某些还没有访问过你数据库的用户来说,这无疑就报错了。解决办法有:

保留每个版本时,创建的字段和 stores

在更新 callback 里面,对处理的数据判断是否存在即可。

在 Dexie.js DB 数据库中,需要你保留每次 DB 创建的方法,实际上是通过 添加 swtich case ,来完成每个版本的更新:

如果遇到一个页面打开,但是另外一个页面拉取到新的代码进行更新时,这个时候还需要将低版本 indexedDB 进行显式的关闭。具体操作办法就是监听 事件,当版本升级时,通知当前 DB 进行关闭,然后在新的页面进行更新操作。

最后,更新是还有几个注意事项:

版本更新不能改变 primary key

回退代码时,千万注意版本是否已经更新。否则,只能增量更新,重新修改版本号来修复。

存储加密特性

有时候,我们存储时,想得到一个由一串 String 生成的 hash key,那在 Web 上应该如何实现呢?

这里可以直接利用 Web 上已经实现的 WebCrypto,为了实现上述需求,我们可以直接利用里面的 方法即可。这里 MDN 上,已经有现成的办法,我们直接使用即可。

参考:

WebCrypto 加密手段

存储上限值

基本限制为:

逐出策略为:

参考:

存储上限值浏览器内核存储上限值处理

数据索引方式

在数据库中除了基本的 CRUD 外,一个高效的索引架构,则是里面的重中之重。在 indexedDB 中,我们一共可以通过三种方式来索引数据:

固定的 key 值

索引外键(index)

游标(cursor)

固定 key 索引

IDBObjectStore 提供给了我们直接通过 来索引数据,参考 [代码1],这种方式需要我们一开始就知道目标的 内容。当然,也可以通过 全部索引数据。

比如,我们通过 primaryKey 得到一条具体的数据:

也可以 fetch 整个 Object Store 的数据。这些场景用处比较少,这里就不过多讲解。我们主要来了解一下 index 的索引方式。

index 索引

如果想要查询某个数据,直接通过整个对象来进行遍历的话,这样做性能耗时是非常大的。如果我们结合 来将 key 加以分类,就可以很快速的实现指定数据的索引。这里,我们可以直接利用 IDBObjectStore 上面的 方法来获取指定 index 的值,具体方法可以参考 [代码1]。

该方法会直接返回一个 IDBIndex 对象。这你也可以理解为一个类似 ObjectStore 的微型 index 数据内容。接着,我们可以使用 方法来获得指定 index 的数据,参考[代码2]。

使用 方法不管你的 index 是否是 的都会只会返回第一个数据。如果想得到多个数据的话,可以使用 来做。通过 得到的回调函数,直接通过 可以得到对应的 value 内容。

除了通过 得到所有数据外,还可以采用更高效的 方法遍历得到的数据。

参考:

getAll() 和 openCursor 实例

游标索引

所谓的游标,大家心里应该可以有一个初步的印象,就像我们物理尺子上的那个东西,可以自由的移动,来标识指向的对象内容。cursor 里面有两个核心的方法:

advance(count): 将当前游标位置向前移动 count 位置

continue(key): 将当前游标位置移动到指定 key 的位置,如果没提供 key 则代表的移动下一个位置。

比如,我们使用 cursor 来遍历 Object Store 的具体数据。

通常,游标可以用来遍历两个类型的数据,一个是 ObjectStore、一个是 Index。

Object.store: 如果在该对象上使用游标,那么会根据 遍历整个数据,注意,这里不会存在重复的情况,因为 是唯一的。

index: 在 index 上使用游标的话,会以当前的 index 来进行遍历,其中可能会存在重复的现象。

在 IDBObjectStore 对象上有两种方法来打开游标:

openCursor: 遍历的对象是 具体的数据值,最常用的方法

openKeyCursor: 遍历的对象是 数据 key 值

这里,我们通过 来直接打开一个 index 数据集,然后进行遍历。

在游标中,还提供给了一个 和 方法,我们可以用它来进行数据的更新操作,否则的话就直接使用 ObjectStore 提供的 方法。

游标里面我们还可以限定其遍历的范围和方向。这个设置是我们直接在 方法里面传参完成的,该方法的构造函数参考 [代码1]。他里面可以传入两个参数,第一个用来指定范围,第二个用来指定 移动的方向。

如果需要对 cursor 设置范围的话,就需要使用到 这个对象,使用样板可以参考 [代码2]。IDBKeyRange 里面 key 参考的对象 因使用者的不同而不同。如果是针对 ObjectStore 的话,则是针对 primaryKey,如果是针对 Index 的话,则是针对当前的 indexKey

比如,我们这里对 PersonIndex 设置一个 index 范围,即,索引 在 和 之间的数据集合。

如果你还想设置遍历的方向和是否排除重复数据,还可以根据 [代码2] 的枚举类型来设置。比如,在 [代码3] 中,我们改变默认的 cursor 遍历数据的方向为 ,从末尾开始。

事务读取性能

在 indexDB 里面的读写全部是基于 模式来的。也就是 IDBDataBase 里面的 方法,如下 [代码1]。所有的读写都可以比作在 作用域下的请求,只有当所有请求完成之后,该次 才会生效,否则就会抛出异常或者错误。 会根据监听 error,abort,以及 complete 三个事件来完成整个事务的流程管理,参考[代码2]。

例如:

你可以在 方法里面手动传入 或者其他表示事务的 参数,来表示本次事务你会进行如何的操作。IndexedDB 在初始设计时,就已经决定了它的性能问题。

只含有 readonly 模式的 transaction 可以并发进行执行含有 write 模式的 transaction 必须按照队列 来 执行

这就意味着,如果你使用了 模式的话,那么后续不管是不是 都必须等待该次 transaction 完成才行。

常用技巧

生成 id++ 的主键

指定 primaryKey 生成时,是通过 方法来操作的。有时候,我们会遇到想直接得到一个 key,并且存在于当前数据集中,可以在 options 中同时加上 和 属性。该 key 的范围是 [1- $ 2^ $],参考 keygenerator key 的大小

推荐

阅读推荐

indexedDB W3C 文档 indexedDB 入门MDN indexedDB 入门

好用库推荐

idb: 一个 promise 的 DB 库

Indexed Appendix

IndexedDB 数据库使用key-value键值对储存数据.你可以对对象的某个属性创建索引(index)以实现快速查询和列举排序。.key可以使二进制对象

IndexedDB 是事务模式的数据库. IndexedDB API提供了索引(indexes), 表(tables), 指针(cursors)等等, 但是所有这些必须是依赖于某种事务的。

The IndexedDB API 基本上是异步的.

IndexedDB 数据库的请求都会包含 onsuccess和onerror事件属性。

IndexedDB 在结果准备好之后通过DOM事件通知用户

IndexedDB是面向对象的。indexedDB不是用二维表来表示集合的关系型数据库。这一点非常重要,将影响你设计和建立你的应用程序。

indexedDB不使用结构化查询语言(SQL)。它通过索引(index)所产生的指针(cursor)来完成查询操作,从而使你可以迭代遍历到结果集合。

IndexedDB遵循同源(same-origin)策略

局限和移除 case

全球多种语言混合存储。国际化支持不好。需要自己处理。

和服务器端数据库同步。你得自己写同步代码。

全文搜索。

在以下情况下,数据库可能被清除:

用户请求清除数据。

浏览器处于隐私模式。最后退出浏览器的时候,数据会被清除。

硬盘等存储设备的容量到限。

不正确的

不完整的改变.

常规概念

数据库

数据库: 通常包含一个或多个 object stores. 每个数据库必须包含以下内容:

名字(Name): 它标识了一个特定源中的数据库,并且在数据库的整个生命周期内保持不变. 此名字可以为任意字符串值(包括空字符串).

当前版本(version). 当一个数据库首次创建时,它的 version 为1,除非另外指定. 每个数据库在任意时刻只能有一个 version

对象存储(object store): 用来承载数据的一个分区.数据以键值对形式被对象存储永久持有。在 OS 中,创建一个 key 可以使用 和 。

key generator: 简单来说就是在存储数据时,主动生成一个 id++ 来区分每条记录。这种情况下 存储数据的 key 是和 value 分开进行存储的,也就是 (out of line)。

key path: 需要用户主动来设置储存数据的 key 内容,

request: 每次读写操作,可以当做一次 request.

transaction: 一系列读写请求的集合。

index: 一个特殊的 Object Store,用来索引另外一个 Store 的数据。

具体数据 key/value

key generator: 相当于以一种 的形式来生成一个 key 值。

key path: 当前指定的 key 可以根据 value 里面的内容来指定。里面可以为一些分隔符。

指定的 key:这个就是需要用户手动来指定生成。

key: 这个 key 的值,可以通过三种方式生成。 a key generator, a key path, 用户指定的值。并且,这个 key 在当前的 Object Store 是唯一的。一个 key 类型可以是 string, date, float, and array 类型。不过,在老版本的时候,一般只支持 string or integer。(现在,版本应该都 OK 了)

value: 可以存储 boolean, number, string, date, object, array, regexp, undefined, and null。现在还可以存储 files and blob 对象。

操作作用域

scope:这可以比作 transaction 的作用域,即,一系列 transaction 执行的顺序。该规定,多个 reading transaction 能够同时执行。但是 writing 则只能排队进行。

key range: 用来设置取出数据的 key 的范围内容。

参考:

原生概念 IndexedDB

IDBFactory

这其实就是 上面挂载的对象。主要 API 如下:

你可以直接通过 来打开一个数据库。通过 返回一个 Request 对象,来进行结果监听的回调:

参考:

IndexDB Factory API

IDBRequest

当你通过 方法处理过后,就会得到一个 Request 回调对象。这个就是 IDBRequest 的实例。

你可以通过 得到当前数据库操作的结果。如果你打开更新后的版本号的话,还需要监听 事件来实现。最常通过 indexedDB.open 遇见的错误就是 版本错误。这表明存储在磁盘上的数据库的版本高于你试图打开的版本。

所以,一般在创建 IndexDB 时,还需要管理它版本的更新操作,这里就需要监听 onupgradeneeded 来是实现。

或者我们可以直接使用 微型库来实现读取操作。

其中通过 回调得到的 event.result 就是 的实例,常常用来设置 index 和插入数据。参考下面内容。

参考:

IDBRequest API

IDBDatabase

该对象常常用来做 Object Store 和 transaction 的创建和删除。该部分是 事件获得的 对象:

具体 API 内容如下:

如果它通过 createObjectStore 方法,那么得到的就是一个 实例对象。如果是 transaction 方法,那么就是 对象。

IDBObjectStore

该对象一般是用来创建 index 和插入数据使用。

可以参考:

IDBIndex

该对象是用来进行 Index 索引的操作对象,里面也会存在 和 等方法。详细内容如下:

参考:

idb 开源库,微型代码库treo 开源库dexie.js 开源库indexeddb原生概念 IndexedDB

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180511G1W1L600?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券