// In the following line, you should include the prefixes of implementations you want to test.
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
// DON'T use "var indexedDB = ..." if you're not in a function.
// Moreover, you may need references to some window.IDB* objects:
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
// (Mozilla has never prefixed these objects, so we don't need window.mozIDB*)
来自中外各大名宿的吐槽,IndexedDB的API是出了名的膈应人。就比如说新建一个数据库,为啥我要给它取名叫request而不是idb之类的。事实上,对IndexedDB来说,新建数据库是一个request请求,用MDN的原话是,IndexedDB uses a lot of requests。通过这些请求,它能够获取到相应的DOM事件,从而判断该操作是成功了还是失败了。同时,这些请求也有readyState,result和errorCode等一系列属性。这怎么看都像是一个XMLHTTPRequest对象,简直不能再坑了。
// All keys ≤ x
var r1 = IDBKeyRange.upperBound(x);
// All keys < x
var r2 = IDBKeyRange.upperBound(x, true);
// All keys ≥ y
var r3 = IDBKeyRange.lowerBound(y);
// All keys > y
var r4 = IDBKeyRange.lowerBound(y, true);
// All keys ≥ x && ≤ y
var r5 = IDBKeyRange.bound(x, y);
// All keys > x &&< y
var r6 = IDBKeyRange.bound(x, y, true, true);
// All keys > x && ≤ y
var r7 = IDBKeyRange.bound(x, y, true, false);
// All keys ≥ x &&< y
var r8 = IDBKeyRange.bound(x, y, false, true);
// The key = z
var r9 = IDBKeyRange.only(z);
通过IDBKeyRange,结合cursor,便可以实现在一定范围内读取数据的操作了。
// 想要遍历数据,就要openCursor方法,它在当前对象仓库里面建立一个读取光标(cursor)
// 绑定range
var range = IDBKeyRange.bound('3', '20');
const cursor = db.trasaction(['todo-item'], 'readOnly').objectStore('todo-item').openCursor(range);
//回调函数接受一个事件对象作为参数,该对象的target.result属性指向当前数据对象。当前数据对象的key和value分别返回键名和键值(即实际存入的数据)。continue方法将光标移到下一个数据对象,如果当前数据对象已经是最后一个数据了,则光标指向null。
cursor.onsuccess = function(e) {
var res = e.target.result;
if(res) {
console.log("Key", res.key);
console.dir("Data", res.value);
res.continue();
}
}
async function getAllData() {
let db = await idb.open('db-name', 1)
let tx = db.transaction('objectStoreName', 'readonly')
let store = tx.objectStore('objectStoreName')
// add, clear, count, delete, get, getAll, getAllKeys, getKey, put
let allSavedItems = await store.getAll()
console.log(allSavedItems)
db.close()
}
想要构建离线应用,除了使用service worker,另一个绕不开的话题便是IndexedDB。
IndexedDB是浏览器端的一个基于键值对存储的事务型数据库。为什么要用它,因为想要做到在离线情况下展示数据,数据的持久化是离线应用绕不过去的一个坎。以前使用的是Web SQL,不过它已经被废弃掉了,所以never mind。什么,你说localstorage?确实有很多人会使用localstorage来存储数据,但是相比IndexedDB,它存在很大的不足,原因有三:
localstorage 存储有大小限制,限制5MB,不能存储大量数据,尤其是带有结构的数据。
没有查询语句,没有schema,基本上没有任何有关数据库的操作。每次的写入和写出都要字符串化和对象化,何其麻烦。所以在处理带结构的大型数据上基本毫无扩展性。
最关键的一点是,localstorage的API是同步的,这就意味着它会阻塞DOM操作。并且很多时候,离线应用的数据操作需要在service worker中进行,service worker只接受异步的API,所以相较而言,IndexedDB是更好的选择。
下面这张表摘自张鑫旭的博文,改装了一下,可以快速了解IndexeDB的一些基础特性。
也许,上表中说到的一些概念你还不是很懂。嘿,您先别急,先坐下,且听我慢慢给您说。
Q1:什么是NoSQL数据库? A1:非关系型数据库,其中的一大类便是通过键值(key-value)存储数据。IndexedDB便是属于这一类。
Q2:什么是事务? A2:指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。它有以下四点特性:
Q3:Cursor是干嘛用的? A3:cursor即游标,类似于现实中的游标,一个刻度表示一行数据,游标就是尺子上的一片区域,想要获得数据库一行一行的数据,我们可以遍历这个游标就好了。
前菜上的差不多了,现在进入我们的正餐部分。
如何使用IndexedDB?
使用IndexedDB其实还蛮简单的,你只需要做两件事:
创建或打开一个数据库
创建一个Object Store对象仓库(它是IndexedDB存储数据的机制,习惯了关系型数据库的同学可以把它想象成一张表)
遵循上面两步,我们便可以开始愉快的使用IndexedDB了。代码如下:
好吧,我撒谎了,从代码上看,使用IndexedDB并不是那么简单啊,这还是在我没有考虑兼容性的情况下,而忽略了以下这么一大段代码。
来自中外各大名宿的吐槽,IndexedDB的API是出了名的膈应人。就比如说新建一个数据库,为啥我要给它取名叫request而不是idb之类的。事实上,对IndexedDB来说,新建数据库是一个request请求,用MDN的原话是,
IndexedDB uses a lot of requests
。通过这些请求,它能够获取到相应的DOM事件,从而判断该操作是成功了还是失败了。同时,这些请求也有readyState,result和errorCode等一系列属性。这怎么看都像是一个XMLHTTPRequest对象,简直不能再坑了。吐槽归吐槽,我们还是认真看一下上一段代码做了什么。在连接数据库后,我们通过createObjectStore方法新建了三个对象仓储,第一个参数即为仓储名,第二个参数即为配置项,包含两个属性:1. keyPath,2. autoIncrement。keyPath用于指定对象的键,如果未指定,则对象的创建使用的是out-of-line keys;指定了,则使用in-line keys。至于什么是out-of-line keys和in-line keys,我们通过一图流来进行详细的说明。
可以这么理解,out-of-line keys即为单独生成的一个key,可能需要我们自己指定。而in-line keys则是指定对象的一个属性作为key值,由数据库自动绑定。
至于autoIncrement属性,可以看成一个key generator,自动生成key值。一般来说,keyPath和autoIncrement属性只要使用一个就够了,如果两个同时使用,表示键名为递增的整数,且对象不得缺少指定属性。
当然,除了使用key存储对象,也可以为对象指定index。就像上面代码中的createIndex做的那样,我们依然通过一图流来说明key和index的区别。
可以看到,两者其实是同一份数据,只是由不同的属性索引,当需要检索某一特定属性的数据时,index格外有用。
下面讲讲如何进行数据库的CRUD操作,毕竟这才是我们真正关心的。
执行IndexedDB的CRUD操作只需要如下五步:
1. 添加数据
2. 更新数据
3. 删除数据
4. 读取数据
读取操作有那么点不同,因为它会新建一个request,读取数据在request的回调中。
5. 遍历数据
get()和getAll()可以获取单个数据或全部数据,但如果想进行更精细的读取操作,比如读取3-20范围的数据,则需要用到cursor及IDBKeyRange两个对象了。
索引的有用之处,在于可以指定读取数据的范围
IDBKeyRange对象的作用则是生成一个表示范围的Range对象。它的生成方法有四种
下面的代码直接摘自阮一峰老师的文章,仅供参考
通过IDBKeyRange,结合cursor,便可以实现在一定范围内读取数据的操作了。
结语
终于把IndexedDB的API给走了一遍,基本上涵盖了我们日常开发的大部分操作,当然还有一部分API可以直接从MDN上查阅。可以看到,这些API不可谓不繁琐,如果直接使用这些API,估计你们不是累死就是被气死。
正所谓哪里有压迫哪里就有反抗,我们的Jake Archibald大神在官方的基础上封装了一个Promise风格的库--idb,可以方便开发者们按照现代JavaScript的方式使用IndexedDB。这里有一篇文章便是基于这个库来介绍IndexedDB的,写的相当不错。
下面这行代码大概展示了使用idb来写出promise及async风格的代码,来源于medium。
感兴趣的同学也可以看看这个简单的使用idb实现的todoList。
以上,XD。
by zhangxueai@corp.netease.com