在知乎和我在平常工作中,常常会看到一个问题:
前端现在还火吗?
这个我只想说:
隔岸观火的人永远无法明白起火的原因,只有置身风暴,才能找到风眼之所在 ——『秦时明月』
你 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 已经提供了
index
、cursor
等高效的索引机制,推荐不要直接将所有数据都取回来,再进行筛选,而是直接利用cursor
进行。 - 最后推荐几个常用库
离线存储
IndexedDB 可以存储非常多的数据,比如 Object,files,blobs 等,里面的存储结构是根据 Database 来进行存储的。每个 DB 里面可以有不同的 object stores。具体结构如下图:
并且,我们可以给 key
设定相关特定的值,然后在索引的时候,可以直接通过 key 得到具体的内容。使用 IndexDB 需要注意,其遵循的是同域原则。
indexDB 基本概念
在 indexDB 中,有几个基本的操作对象:
- Database: 通过
open
方法直接打开,可以得到一个实例的 DB。每个页面可以创建多个 DB,不过一般都是一个。
idb.open(name, version, upgradeCallback)
- Object store: 这个就是 DB 里面具体存储的对象。这个可以对应于 SQL 里面的 table 内容。其存储的结构为:
- index: 有点类似于外链,它本身是一种 Object store,主要是用来在本体的 store 中,索引另外 object store 里面的数据。需要区别的是,key 和 index 是不一样的。可以参考: index DEMO,mdn index。如下图表示:
如下 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 Prop 和 googleDeveloper 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 的版本呢?
- 该表数据库里面的
keyPath
时。 - 你需要重新设计数据库表结构时,比如新增 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 上,已经有现成的办法,我们直接使用即可。
参考:
存储上限值
基本限制为:
浏览器 | 限制 |
---|---|
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
方法遍历得到的数据。
参考:
游标索引
所谓的游标,大家心里应该可以有一个初步的印象,就像我们物理尺子上的那个东西,可以自由的移动,来标识指向的对象内容。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);
}
};
在游标中,还提供给了一个 update
和 delete
方法,我们可以用它来进行数据的更新操作,否则的话就直接使用 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 范围,即,索引 在 villainhr
和 jimmyVV
之间的数据集合。
# 都包括 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 中同时加上 keyPath
和 autoIncrement
属性。该 key 的范围是 [1- $ 2^{53} $],参考 keygenerator key 的大小
db.createObjectStore('table1', {keyPath: 'id', autoIncrement: true});
推荐
阅读推荐
indexedDB W3C 文档
indexedDB 入门
MDN indexedDB 入门
好用库推荐
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
和key 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 的范围内容。
参考:
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) {...};
参考:
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 和插入数据。参考下面内容。
参考:
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 索引的操作对象,里面也会存在 get
和 getAll
等方法。详细内容如下:
[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
也欢迎大家关注我的公众号:前端小吉米 获得一手的技术文章以及未来技术的发展内容。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。