背景
在本地离线存储的场景中,IndexedDB作为一个nosql的本地DB存储一直发挥着重要的作用。在需要存储大量数据时,IndexedDB能够有效地满足你的需求。下面,我将简单介绍下IndexedDB的简单使用方法和在我阅读W3C规范文档时看到的一些需要注意的细节。实例大部分参照MDN中的介绍实例,同时对整体逻辑和实例做了一些改动。
使用方法
打开数据库
我们使用window.indexedDB.open(DBName)来打开数据库。具体实例如下:
- const request = window.indexedDB.open(DBName);
- request.onsuccess = function(event) {
- //request === event.target;
- }
- request.onerror = function(event) {}
当数据库连接时,会返回一个IDBOpenDBRequest对象。在连接建立成功时,会触发onsuccess事件,其中函数参数event的target属性就是request对象。
创建和更新数据库版本号
window.indexedDB.open的第二个参数即为版本号。在不指定的情况下,默认版本号为1。具体实例如下:
- const request = window.indexedDB.open(DBName, 2);
在需要更新数据库的schema(模式)时,需要更新版本号。此时我们指定一个高于之前版本的版本号,就会触发onupgradeneeded事件。类似的,在***次创建时,也会触发此事件。
我们需要注意的是,版本号是一个unsigned long long数字,这意味着它可以是一个非常大的整数。但是,它不能是一个小数,否则它将会被转为最近的整数,同时有可能导致onUpgradeneeded事件不触发(bug)。
构建数据库
我们使用createObjectStore来创建一个存储空间。同时,使用createIndex来创建索引。具体实例如下:
- const objectStore = db.createObjectStore('customers');
- objectStore.createIndex('name', 'name', {unique:false});
我们从***个函数createObjectStore开始说起。该函数接受两个参数,***个参数为存储空间的名称,即我们上面的customers。同时,它还有第二个可选参数,keyPath指定存储的key值为存储对象的某个属性,autoIncrement指定了key值是否自增(当key值为默认的从1开始到2^53的整数时)。具体实例如下:
- const objectStore = db.createObjectStore('customers',{keyPath:'id', autoIncrement:true});
第二个函数为createIndex,它的***个参数为索引的名称,第二个参数是指定根据存储数据的哪一个属性来构建索引,第三个属性unique为是否允许指定的索引值是否可以相等。具体示例如下:
- objectStore.createIndex('by_name', 'name', {unique:false});
- objectStore.createIndex('by_email', 'email', {unique:true});
当存储空间初始化完成后,我们需要把数据放入存储空间中,我们可以直接调用add方法将数据放入存储空间内,具体实例如下:
- //方法1,规范文档推荐使用, key值如果指定自增,可以不填
- objectStore.put(data, key);
- //方法2,key值同上
- objectStore.add(data, key);
事务
在IndexedDB中,我们也能够使用事务来进行数据库的操作。事务有三个模式:
- readOnly,只读
- readwrite,读写
- versionchange,数据库版本变化
我们创建一个事务时,需要从上面选择一种模式,如果不指定的话,则默认为只读模式。具体实例如下:
- const transition = db.transition(['customers'], 'readwrite');
事务函数transition的***个参数为需要关联的存储空间,第二个可选参数为事务模式。与上面类似,事务成功时也会触发onsuccess函数,失败时触发onerror函数。
事务的操作都是原子性的。
删除
使用delete函数即可删除数据。具体使用方法如下:
- const request = db.transaction(['customers'], 'readwrite').objectStore('customers').delete(author);
获取数据
get方法
最简单的方法就是使用自带的get函数来获取存储空间的值,具体实例如下:
- db.transition(['customer']).objectStore('customer').get(keyName).onuccess = function(){};
游标方法
我们使用openCursor来创建游标。
- const objectStore = db.transaction('customers').objectStore('customers');
- objectStore.openCursor().onsuccess = function(event) {
- const cursor = event.target.result;
- if (cursor) {
- alert(cursor.value.name);
- cursor.continue();
- }
- else {
- alert('No more entries!');
- }
- };
使用游标时有一个需要注意的地方,当游标便利整个存储空间但是并未找到给定条件的值时,仍然会触发onsuccess函数。
同时,如果你想要限定你在游标中看到的值的范围,你可以使用一个key range对象然后把它作为***个参数传给openCursor或是openKeyCursor方法。你可以构造一个只允许一个单一key的 key range,或者一个具有下限或上限,或者一个既有上限也有下限。边界可以是闭合的(也就是说key range包含给定的值)或者是“开放的”(也就是说key range不包括给定的值)。具体实例如下:
- // 只匹配 'Donna'
- const singleKeyRange = IDBKeyRange.only('Donna');
- // 匹配所有在 'Bill' 前面的, 包括 'Bill'
- const lowerBoundKeyRange = IDBKeyRange.lowerBound('Bill');
- // 匹配所有在 “Bill” 前面的, 但是不需要包括 'Bill'
- const lowerBoundOpenKeyRange = IDBKeyRange.lowerBound('Bill', true);
- // Match anything up to, but not including, 'Donna'
- const upperBoundOpenKeyRange = IDBKeyRange.upperBound('Donna', true);
- //Match anything between 'Bill' and 'Donna', but not including 'Donna'
- const boundKeyRange = IDBKeyRange.bound('Bill', 'Donna', false, true);
- index.openCursor(boundKeyRange).onsuccess = function(event) {
- const cursor = event.target.result;
- if (cursor) {
- // Do something with the matches.
- cursor.continue();
- }
- };
IDBKeyRange对象的lowerBound和upperBound方法分别表示检索指定key值前或者后的范围(第二个参数指定边界是否闭合,如果为true则不闭合)。
有时候你可能想要以倒序而不是正序(所有游标的默认顺序)来遍历。切换方向是通过传递prev到 openCursor方法来实现的。
- objectStore.openCursor(null, IDBCursor.prev).onsuccess = function(event) {
- const cursor = event.target.result;
- if (cursor) {
- // Do something with the entries.
- cursor.continue();
- }
- };
因为 “name” 索引不是***的,那就有可能存在具有相同 name 的多条记录。要注意的是这种情况不可能发生在对象存储空间上,因为键必须永远是***的。如果你想要在游标在索引迭代过程中过滤出重复的,你可以传递 nextunique (或 prevunique 如果你正在向后寻找)作为方向参数。 当 nextunique 或是 prevunique 被使用时,被返回的那个总是键最小的记录。
索引方法
在前面构建数据库时,我们创建了两个索引。现在,我们就通过这两个索引来演示如何通过索引进行搜索。具体实例如下:
- const index = objectStore.index('by_name');
- //***种,使用get方法
- index.get(name).onsuccess = function(){
- alert(event.target.result.name);
- }
- //第二种,使用游标
- //普通游标
- index.openCursor().onsuccess = function(event) {
- const cursor = event.target.result;
- if (cursor) {
- // cursor.key 是一个 name, 就像 'Bill', 然后 cursor.value 是整个对象。
- alert('Name: ' + cursor.key + ', id' + cursor.value.id + ', email: ' + cursor.value.email);
- cursor.continue();
- }
- };
- //键游标
- index.openKeyCursor().onsuccess = function(event) {
- const cursor = event.target.result;
- if (cursor) {
- // cursor.key is 一个 name, 就像 'Bill', 然后 cursor.value 是那个id,即存储对象的key值。
- alert('Name: ' + cursor.key + ', 'id: ' + cursor.value);
- cursor.continue();
- }
- };
异常处理
在浏览器有如下操作的情况下,indexedDB可能会出现异常:
- 用户清除浏览器缓存
- 存储空间超过大小限制
W3C规范文档需要注意的应用点
key值能够接受的数据类型
在IndexedDB中,key值可以接受一下几种类型的值:
- Number
- Date
- String
- ArrayBuffer
- Array
具体说明可以参考此处。
key path能够接受的数据类型
在IndexedDB中,key path(主键)能够接接受如下数据类型:
- Blob
- File
- Array
- StringI
注:空格不能出现在key path中。
value能够接受的数据类型
在IndexedDB中,value能够接受ECMA-262中所有的值,例如String,Date,ImageDate等。
事务中断后,会不会影响key值的自增
IndexedDB在没有指定key值的时候就会采用自增的key值,不受其他任何影响。如果一个事务在中途中断,那么key值的自增将会从事务开始前的key开始。
IndexedDB的安全相关问题
IndexedDB也受到浏览器同源策略的限制。
总结
IndexedDB在本地存储中有着无可替代的作用,是替代关系型数据库websql的产品。在许多需要运用离线存储的场景下,IndexedDB能够给我们提供有效的支撑。但是,IndexedDB毕竟存在于客户端,会出现诸如用户清理缓存(Windows下更为常见)等破坏数据的情况,因此建议作为一个优化用户体验的方案,不能过多依赖。
参考文献
后续计划
W3C的规范文档还在学习中。在有时间的情况下,后续可能会尝试翻译IndexedDB 2.0的官方文档,如果有需要的后续可以继续关注。有什么问题也欢迎随时交流。