IndexedDB 打造靠谱 Web 离线数据库

5,734

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

前端现在还火吗?

这个我只想说:

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

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

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 已经提供了 indexcursor等高效的索引机制,推荐不要直接将所有数据都取回来,再进行筛选,而是直接利用 cursor 进行。
  • 最后推荐几个常用库

更多可以关注我的公众号:前端小吉米 (二维码在文章底部)

离线存储

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

indexedDB 结构图

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

indexDB 基本概念

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

  • Database: 通过 open 方法直接打开,可以得到一个实例的 DB。每个页面可以创建多个 DB,不过一般都是一个。
idb.open(name, version, upgradeCallback)
  • Object store: 这个就是 DB 里面具体存储的对象。这个可以对应于 SQL 里面的 table 内容。其存储的结构为:

image.png-392.5kB

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

image.png-59.8kB

如下 code 为:

// 创建 index
var myIndex = objectStore.index('lName'); 
  • transaction: 事务其实就是一系列 CRUD 的集合内容。如果其中一个环节失败了,那么整个事务的处理都会被取消。例如:
var trans1 = db.transaction("foo", "readwrite");
var trans2 = db.transaction("foo", "readwrite");
var objectStore2 = trans2.objectStore("foo")
var objectStore1 = trans1.objectStore("foo")
objectStore2.put("2", "key");
objectStore1.put("1", "key");
  • cursor: 主要是用来遍历 DB 里面的数据内容。主要是通过 openCursor 来进行控制。
function displayData() {
  var transaction = db.transaction(['rushAlbumList'], "readonly");
  var objectStore = transaction.objectStore('rushAlbumList');

  objectStore.openCursor().onsuccess = function(event) {
    var cursor = event.target.result;
    if(cursor) {
      var listItem = document.createElement('li');
      listItem.innerHTML = cursor.value.albumTitle + ', ' + cursor.value.year;
      list.appendChild(listItem);  

      cursor.continue();
    } else {
      console.log('Entries all displayed.');
    }
  };
}

如何使用 IndexDB

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

  • 打开数据库表
  • 设置指定的 primary Key
  • 定义好索引的 index

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

var request = indexedDB.open(dbName, 2);

request.onerror = function(event) {
  // 错误处理程序在这里。
};
request.onupgradeneeded = function(event) {
  var db = event.target.result;
  // 设置 id 为 primaryKey 参数
  var objectStore = db.createObjectStore("customers", { keyPath: "id",{autoIncrement:true} });
  
  // 设置指定索引,并确保唯一性
  objectStore.createIndex("name", "name", { unique: false });
  objectStore.createIndex("email", "email", { unique: true });

};

上面主要做了 3 件事:

  • 打开数据库表
  • 新建 Store,并设置 primary Key
  • 设置 index

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

创建 Object Store

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

var objectStore = db.createObjectStore("customers", { keyPath: "id",{autoIncrement:true} });

基本函数构造为:

IDBObjectStore createObjectStore(DOMString name,
                                               optional IDBObjectStoreParameters options)
                                               
dictionary IDBObjectStoreParameters {
  (DOMString or sequence<DOMString>)? keyPath = null;
  boolean autoIncrement = false;
};
  • keyPath: 用来设置主键的 key,具体区别可以参考下面的 keyPath 和 generator 的区别。
  • autoIncrement: 是否使用自增 key 的特性。

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

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

设置索引 index

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

objectStore.createIndex('indexName', 'property', options);
  • indexName: 设置当前 index 的名字
  • property: 从存储数据中,指明 index 所指的属性。

其中,options 有三个选项:

  • unique: 当前 key 是否能重复 (最常用)
  • multiEntry: 设置当前的 property 为数组时,会给数组里面每个元素都设置一个 index 值。
# 创建一个名字叫 titleIndex 的 index,并且存储的 index 不能重复
DB.createIndex('titleIndex', 'title', {unique: false});

具体可以参考:MDN createIndex PropgoogleDeveloper Index

增删数据

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

事务的创建

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

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

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

[代码1]
  [NewObject] IDBTransaction transaction((DOMString or sequence<DOMString>) storeNames,
                                         optional IDBTransactionMode mode = "readonly");
                                         
[代码2]
var transaction = db.transaction(["customers"], "readwrite");
var objectStore = transaction.objectStore("customers");
# 遍历存储数据
for (var i in customerData) {
  var request = objectStore.add(customerData[i]);
  request.onsuccess = function(event) {
    // success, done?
  };
}

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

[代码3]
var tx = db.transaction(["books","person"], "readonly");
var books = tx.objectStore("books");
var person = tx.objectStore("person");

操作数据

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

[代码1]
var tx = db.transaction("books", "readwrite");
var store = tx.objectStore("books");

store.put({title: "Quarry Memories", author: "Fred", isbn: 123456});
store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567});
store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678});

tx.oncomplete = function() {
  // All requests have succeeded and the transaction has committed.
};

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

[代码2]
  [NewObject] IDBRequest put(any value, optional any key);
  [NewObject] IDBRequest add(any value, optional any key);
  [NewObject] IDBRequest delete(any query);

索引数据

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

[代码1]
var index = objectStore.index("name");
index.get("Donna").onsuccess = function(event) {
  alert("Donna's SSN is " + event.target.result.ssn);
};

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

keyPath 和 key Generator

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

设置 keyPath

# 设置预先需要存放的数据

const customerData = [
  { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },
  { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" }
];

# 通过 keyPath 设置 Primary Key
var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

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

upgradeDb.createObjectStore('logs', {keyPath: 'id', autoIncrement:true});

使用 generator

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

upgradeDb.createObjectStore('notes', {autoIncrement:true});

indexDB 打开注意事项

检查是否支持 indexDB

if (!('indexedDB' in window)) {
  console.log('This browser doesn\'t support IndexedDB');
  return;
}

版本更新: indexDB

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

idb.open('test-db7', 2, function(upgradeDb) {})

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

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

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

# 版本一定义的内容
db.version(1).stores({friends: "++id,name"});

# 版本二修改结构为:
db.version(2).stores({friends: "++id,name,shoeSize"});

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

参考:

版本更替

版本更新

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

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

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

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

  1. 该表数据库里面的 keyPath 时。
  2. 你需要重新设计数据库表结构时,比如新增 index
# 版本 1 的 DB 设计,有一个主键 id 和 index-name
db
.version(1)
.stores({friends: '++id,name'})

# 如果直接想新增一个 key,例如 male,是无法成功的
db
.version(1)
.stores({friends: '++id,name,male'})

# 正确办法是直接修改版本号更新
db
.version(2)
.stores({friends: '++id,name,male'})

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

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

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

# 打开版本 1 的数据库
var dbPromise = idb.open('db1', 1, function(upgradeDb){...})

# 打开版本 2 的数据库
var dbPromise = idb.open('db2', 2, function(upgradeDb){...})

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

var dbPromise = idb.open('test-db7', 2, function(upgradeDb) {
  switch (upgradeDb.oldVersion) {
    case 0:
      upgradeDb.createObjectStore('store', {keyPath: 'name'});
    case 1:
      var peopleStore = upgradeDb.transaction.objectStore('store');
      peopleStore.createIndex('price', 'price');
  }
});

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

# 将版本二 中的 name 拆分为  firstName 和 lastName
db.version(3).stores({friends: "++id,shoeSize,firstName,lastName"}).upgrade(function(t) {
    
    return t.friends.toCollection().modify(function(friend) {
        // Modify each friend:
        friend.firstName = friend.name.split(' ')[0];
        friend.lastName = friend.name.split(' ')[1];
        delete friend.name;
    });
});

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

  • 保留每个版本时,创建的字段和 stores
  • 在更新 callback 里面,对处理的数据判断是否存在即可。

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

# Dexie.js 保留 DB 数据库
db.version(1).stores({friends: "++id,name"});
db.version(2).stores({friends: "++id,name,shoeSize"});
db.version(3).stores({friends: "++id,shoeSize,firstName,lastName"}).upgrade(...)

# 内部原理,直接添加 switch case 完成版本更新
var dbPromise = idb.open('test-db7', 2, function(upgradeDb) {
  switch (upgradeDb.oldVersion) {
    case 0:
      upgradeDb.createObjectStore('store', {keyPath: 'name'});
    case 1:
      var peopleStore = upgradeDb.transaction.objectStore('store');
      peopleStore.createIndex('price', 'price');
  }
});

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

openReq.onupgradeneeded = function(event) {
  // 所有其它数据库都已经被关掉了,直接更新代码
  db.createObjectStore(/* ... */);
  db.onversionchange = function(event) {
    db.close();
  };

}  
  

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

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

存储加密特性

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

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

参考:

WebCrypto 加密手段

存储上限值

基本限制为:

浏览器 限制
Chrome 可用空间 < 6%
Firebox 可用空间 < 10%
Safari < 50MB
IE10 < 250MB

逐出策略为:

浏览器 逐出政策
Chrome 在 Chrome 耗尽空间后采用 LRU 策略
Firebox 在整个磁盘已装满时采用 LRU 策略
Safari 无逐出
Edge 无逐出

参考:

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

数据索引方式

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

  • 固定的 key 值
  • 索引外键(index)
  • 游标(cursor)

固定 key 索引

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

[代码1]
  [NewObject] IDBRequest get(any query);
  [NewObject] IDBRequest getKey(any query);
  [NewObject] IDBRequest getAll(optional any query,
                                optional [EnforceRange] unsigned long count);
  [NewObject] IDBRequest getAllKeys(optional any query,
                                    optional [EnforceRange] unsigned long count);

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

db.transaction("customers").objectStore("customers").get("id_card_1118899").onsuccess = function(event) {
    // data is event.target.result.name
};

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

index 索引

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

[代码1]
 IDBIndex index(DOMString name);

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

[代码2]
var index = objectStore.index("name");
index.get("Donna").onsuccess = function(event) {
  alert("Donna's SSN is " + event.target.result.ssn);
};

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

objectStore.getAll().onsuccess = function(event) {
      printf(event.target.result); // Array
    };

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

参考:

getAll() 和 openCursor 实例

游标索引

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

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

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

objectStore.openCursor().onsuccess = function(event) {
    var cursor = event.target.result;
    if(cursor) {
        // cursor.key 
        // cursor.value
      cursor.continue();
    } else {
      console.log('Entries all displayed.');
    }
  };

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

  • Object.store: 如果在该对象上使用游标,那么会根据 primaryKey 遍历整个数据,注意,这里不会存在重复的情况,因为 primaryKey 是唯一的。
  • index: 在 index 上使用游标的话,会以当前的 index 来进行遍历,其中可能会存在重复的现象。

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

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

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

PersonIndex.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    customers.push(cursor.value);
    cursor.continue();
  }
  else {
    alert("Got all customers: " + customers);
  }
};

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

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

[代码1]
IDBRequest openCursor(optional any query,
                                    optional IDBCursorDirection direction = "next");

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

/ 匹配所有在 “Bill” 前面的, 但是不需要包括 "Bill"
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

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

# 都包括 villainhr 和 jimmyVV 的数据
var boundKeyRange = IDBKeyRange.bound("villainhr", "jimmyVV", true, true);

 PersonIndex.openCursor(boundKeyRange).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};

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

[代码2]
enum IDBCursorDirection {
  "next",
  "nextunique",
  "prev",
  "prevunique"
};

[代码3]
objectStore.openCursor(null, IDBCursor.prev).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.value 
    cursor.continue();
  }
};

事务读取性能

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

[代码1]
  [NewObject] IDBTransaction transaction((DOMString or sequence<DOMString>) storeNames,
                                         optional IDBTransactionMode mode = "readonly");

[代码2]
  attribute EventHandler onabort;
  attribute EventHandler oncomplete;
  attribute EventHandler onerror;

例如:

var request = db.transaction(["customers"], "readwrite")
                .objectStore("customers")
                .delete("gg");
request.onsuccess = function(event) {
  // delete, done
};

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

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

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

常用技巧

生成 id++ 的主键

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

db.createObjectStore('table1', {keyPath: 'id', autoIncrement: true});

推荐

阅读推荐

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 generatorkey path

    • key generator: 简单来说就是在存储数据时,主动生成一个 id++ 来区分每条记录。这种情况下 存储数据的 key 是和 value 分开进行存储的,也就是 (out of line)。
    • key path: 需要用户主动来设置储存数据的 key 内容,
    • request: 每次读写操作,可以当做一次 request.
    • transaction: 一系列读写请求的集合。
    • index: 一个特殊的 Object Store,用来索引另外一个 Store 的数据。
  • 具体数据 key/value

    • key: 这个 key 的值,可以通过三种方式生成。 a key generator, a key path, 用户指定的值。并且,这个 key 在当前的 Object Store 是唯一的。一个 key 类型可以是 string, date, float, and array 类型。不过,在老版本的时候,一般只支持 string or integer。(现在,版本应该都 OK 了)
      • key generator: 相当于以一种 id++ 的形式来生成一个 key 值。
      • key path: 当前指定的 key 可以根据 value 里面的内容来指定。里面可以为一些分隔符。
      • 指定的 key:这个就是需要用户手动来指定生成。
    • 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

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

[Exposed=(Window,Worker)]
interface IDBFactory {
  [NewObject] IDBOpenDBRequest open(DOMString name,
                                    optional [EnforceRange] unsigned long long version);
  [NewObject] IDBOpenDBRequest deleteDatabase(DOMString name);

  short cmp(any first, any second);
};

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

var request = indexedDB.open('AddressBook', 15);
request.onsuccess = function(evt) {...};
request.onerror = function(evt) {...};

参考:

IndexDB Factory API

IDBRequest

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

[Exposed=(Window,Worker)]
interface IDBRequest : EventTarget {
  readonly attribute any result; // 通过 open 打开过后的 IDBObjectStore 实例 
  readonly attribute DOMException? error;
  readonly attribute (IDBObjectStore or IDBIndex or IDBCursor)? source;
  readonly attribute IDBTransaction? transaction;
  readonly attribute IDBRequestReadyState readyState;

  // Event handlers:
  attribute EventHandler onsuccess;
  attribute EventHandler onerror;
};

enum IDBRequestReadyState {
  "pending",
  "done"
};

[Exposed=(Window,Worker)]
interface IDBOpenDBRequest : IDBRequest {
  // Event handlers:
  attribute EventHandler onblocked;
  attribute EventHandler onupgradeneeded;
};

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

db.onerror = function(event) {
  // Generic error handler for all errors targeted at this database's
  // requests!
  alert("Database error: " + event.target.errorCode);
};

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

request.onupgradeneeded = function(event) { 
   // 更新对象存储空间和索引 .... 
};

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

  var dbPromise = idb.open('test-db3', 1, function(upgradeDb) {
    if (!upgradeDb.objectStoreNames.contains('people')) {
      upgradeDb.createObjectStore('people', {keyPath: 'email'});
    }
    if (!upgradeDb.objectStoreNames.contains('notes')) {
      upgradeDb.createObjectStore('notes', {autoIncrement: true});
    }
    if (!upgradeDb.objectStoreNames.contains('logs')) {
      upgradeDb.createObjectStore('logs', {keyPath: 'id', autoIncrement: true});
    }
  });

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

参考:

IDBRequest API

IDBDatabase

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

request.onupgradeneeded = function(event) { 
   // 更新对象存储空间和索引 .... 
   // event.target.result 对象
};

具体 API 内容如下:

[Exposed=(Window,Worker)]
interface IDBDatabase : EventTarget {
  readonly attribute DOMString name;
  readonly attribute unsigned long long version;
  readonly attribute DOMStringList objectStoreNames;

  [NewObject] IDBTransaction transaction((DOMString or sequence<DOMString>) storeNames,
                                         optional IDBTransactionMode mode = "readonly");
  void close();

  [NewObject] IDBObjectStore createObjectStore(DOMString name,
                                               optional IDBObjectStoreParameters options);
  void deleteObjectStore(DOMString name);

  // Event handlers:
  attribute EventHandler onabort;
  attribute EventHandler onclose;
  attribute EventHandler onerror;
  attribute EventHandler onversionchange;
};

dictionary IDBObjectStoreParameters {
  (DOMString or sequence<DOMString>)? keyPath = null;
  boolean autoIncrement = false;
};

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

IDBObjectStore

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

可以参考:

[Exposed=(Window,Worker)]
interface IDBObjectStore {
  attribute DOMString name;
  readonly attribute any keyPath;
  readonly attribute DOMStringList indexNames;
  [SameObject] readonly attribute IDBTransaction transaction;
  readonly attribute boolean autoIncrement;

  [NewObject] IDBRequest put(any value, optional any key);
  [NewObject] IDBRequest add(any value, optional any key);
  [NewObject] IDBRequest delete(any query);
  [NewObject] IDBRequest clear();
  [NewObject] IDBRequest get(any query);
  [NewObject] IDBRequest getKey(any query);
  [NewObject] IDBRequest getAll(optional any query,
                                optional [EnforceRange] unsigned long count);
  [NewObject] IDBRequest getAllKeys(optional any query,
                                    optional [EnforceRange] unsigned long count);
  [NewObject] IDBRequest count(optional any query);

  [NewObject] IDBRequest openCursor(optional any query,
                                    optional IDBCursorDirection direction = "next");
  [NewObject] IDBRequest openKeyCursor(optional any query,
                                       optional IDBCursorDirection direction = "next");

  IDBIndex index(DOMString name);

  [NewObject] IDBIndex createIndex(DOMString name,
                                   (DOMString or sequence<DOMString>) keyPath,
                                   optional IDBIndexParameters options);
  void deleteIndex(DOMString name);
};

dictionary IDBIndexParameters {
  boolean unique = false;
  boolean multiEntry = false;
};

IDBIndex

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

[Exposed=(Window,Worker)]
interface IDBIndex {
  attribute DOMString name;
  [SameObject] readonly attribute IDBObjectStore objectStore;
  readonly attribute any keyPath;
  readonly attribute boolean multiEntry;
  readonly attribute boolean unique;

  [NewObject] IDBRequest get(any query);
  [NewObject] IDBRequest getKey(any query);
  [NewObject] IDBRequest getAll(optional any query,
                                optional [EnforceRange] unsigned long count);
  [NewObject] IDBRequest getAllKeys(optional any query,
                                    optional [EnforceRange] unsigned long count);
  [NewObject] IDBRequest count(optional any query);

  [NewObject] IDBRequest openCursor(optional any query,
                                    optional IDBCursorDirection direction = "next");
  [NewObject] IDBRequest openKeyCursor(optional any query,
                                       optional IDBCursorDirection direction = "next");
};

参考:

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