IndexedDB 是一个为了能够在客户端存储可观数量的结构化数据,并且在这些数据上使用索引进行高性能检索的 API。虽然 DOM 存储 对于存储少量数据是非常有用的,但是它对大量结构化数据的存储就显得力不从心了。IndexedDB 则提供了这样的一个解决方案。
IndexedDB 分别为同步和异步访问提供了单独的 API 。同步 API 本来是要用于仅供 Web Workers 内部使用,但是还没有被任何浏览器所实现。异步 API 在 Web Workers 内部和外部都可以使用。所以现在可以使用的基本就是异步API了,因为在h5手机项目中用到了indexedDB存储app的相关数据,所以写一些实例供参考:
1.异步API
首先了解一下什么是异步 API:异步 API 方法调用完后会立即返回,而不会阻塞调用线程。要异步访问数据库,需要要调用 window 对象 indexedDB 属性的 open() 方法。该方法返回一个 IDBRequest 对象 (IDBOpenDBRequest);异步操作通过在 IDBRequest 对象上触发事件来和调用程序进行通信。IDBOpenDBRequest接口定义了几个重要属性:
- onerror: 请求失败的回调函数句柄
- onsuccess:请求成功的回调函数句柄
- onupgradeneeded:请求数据库版本变化句柄
通过不同回调函数可以实现数据库调用成功,失败和升级的方法,下面从打开数据库开始。
2.创建(打开)数据库
以最简单的例子来说,创建一个数据库至少要有数据库名称和数据库版本两个参数,为了方便,我们创建一个简单的数据库管理类:
class DBmanager {
//数据库管理类的构造函数,包含两个构造参数,数据库名称和数据库版本,数据库版本如果不指定默认为1;
constructor(DBname, DBversion) {
this.DBname = DBname;
this.DBversion = DBversion || 1;
this.currentDB = null; //当前数据库
}
//打开(没有创建过则创建)数据库的方法
openDB() {
//调用API打开数据库
var request = window.indexedDB.open(this.DBname, this.DBversion);
//打开数据库失败的回调
request.onerror = function (e) {
console.log(e.currentTarget.error.message);
}.bind(this);
//打开数据库成功的回调
request.onsuccess = function (e) {
this.currentDB = e.target.result;
console.log(this.currentDB.name + ' database is already opened!');
}.bind(this);
//数据库升级的回调
request.onupgradeneeded = function (e) {
console.log('database version is already upgrade to '+this.DBversion);
}.bind(this);
}
}
其中onupgradeneeded回调会在两种情况下调用:
- 数据库第一次创建的时候
- 数据库版本变化的时候
调用onupgradeneeded时,onupgradeneeded的执行会在onsuccess和onerror之前,因为在打开数据库时,先检测是否有版本变化,如果版本有变化则先升级数据库再打开,所以数据库的升级方法我们可以在onupgradeneeded里实现。
为了看到具体的执行效果,我们运行一下:
window.onload = function () {
var db = new DBmanager('testDB', 1);
db.openDB();
}
在chromium浏览器中运行,按F12可以看到在APPlication选项下的indexedDB中有了我们刚刚新建的数据库“testDB”,版本为1,log为打开数据库成功时的回调打出的log,说明数据库已经成功打开:
3.关闭和删除数据库
关闭数据库可以直接调用数据库对象的close方法,我们给DBmanager中添加一个成员函数:
//关闭数据库的方法
closeDB() {
this.currentDB.close();
console.log(this.currentDB.name + ' database is already closed!');
}
删除数据库使用indexedDB对象的deleteDatabase方法, 我们给DBmanager中添加一个成员函数:
//删除数据库的方法
deleteDB() {
window.indexedDB.deleteDatabase(this.DBname);
console.log(this.DBname + ' database is already deleted!');
}
由于是异步API,不能保证在closeDB方法调用通过openDB获取到DB对象,所以我们在这里用setTimeout延时一下,保证获取到DB对象后再关闭数据库。
window.onload = function () {
var db = new DBmanager('testDB', 1);
db.openDB();
setTimeout(function () {
db.closeDB();
db.deleteDB();
}, 500);
}
打出的log可以运行后在console看到
4.object store
在关系型数据库中,我们把数据以表的形式存储,但在indexedDB中没有表的概念,而是object store,一个数据库中可以有多个object store,一个object store相当于一个表,可以存储多种数据类型,每条数据都对应一个Key值,我们可以指定某个字段作为Key值,也可以自动生成Key值,也可以指定。比如下面的例子,我们在onupgradeneed里新建一个名为“students”的object store,指定”id”为key值。
request.onupgradeneeded = function (e) {
console.log('database version is already upgrade to '+this.DBversion);
this.currentDB = e.target.result;
if(!this.currentDB.objectStoreNames.contains('students')){
//指定一个键为主键
this.currentDB.createObjectStore('students',{keyPath:"id"});
//指定为主键自增模式
//db.createObjectStore('students',{autoIncrement: true});
}
}.bind(this);
5.事务transaction
在关系型数据库中也有事务的概念,但是这里的事务和关系型数据库中的事务不太一样,在indexedDB中,在对数据库数据进行增删改查数据之前,都需要先开始一个事务,事务中指定需要那些object store进行操作,获取到对应的object store后我们便可以对获取到的object store进行操作,在这里我们给DBmanager中添加一个成员函数,用来根据object store的名称来开启一个事务,获取对应的object store:
//根据storeName获取对应store的方法
getStoreByName(storeName){
var transaction = this.currentDB.transaction(storeName,'readwrite');
return transaction.objectStore(storeName);
}
其中事务拥有三种模式:
- 只读:read,不能修改数据库数据,可以并发执行
- 读写:readwrite,可以进行读写操作
- 版本变更:verionchange
6.添加数据
我们在调用createObjectStore方法时已经创建了一个名为students的object store,并指定主键为id,现在我们可以向store中添加数据了,我们准备一些数据:
//名为students的ObjectStore
const datas=[{
id:11,
name:"zhangsan",
age:24
},{
id:12,
name:"lisi",
age:30
},{
id:13,
name:"wangwu",
age:26
},{
id:14,
name:"zhaoliu",
age:26
}];
之后我们给DBmanager添加一个用于添加数据的成员函数:
//添加数据的方法
addData(storeName,data){
var store = this.getStoreByName(storeName);
for(var i = 0; i<data.length ; i++){
store.add(data[i]);
}
}
调用:
window.onload = function () {
var db = new DBmanager('testDB', 2);
db.openDB();
setTimeout(function () {
db.addData('students',datas);
db.closeDB();
}, 500);
}
打开chromium浏览器,我们可以在indexedDB中看到我们添加的数据:
7.删除数据
删除一条数据:
//删除一条数据(根据主键key(id))
deleteDataByKey(storeName,key){
var store = this.getStoreByName(storeName);
store.delete(key);
}
删除所有数据:
//删除整个ObjectStore的数据
deleteAllData(storeName){
var store = this.getStoreByName(storeName);
store.clear();
}
是否删除成功可以运行后在chromium浏览器进行验证
8.更新数据
这里我们写一个根据key值更新age值的方法供参考:
//根据键值修改数据
updateDataByKey(storeName,key,age){
var store = this.getStoreByName(storeName);
var request = store.get(key);
request.onsuccess = function (e) {
var student = e.target.result;
student.age = age;
store.put(student);
}
}
调用:
window.onload = function () {
var db = new DBmanager('testDB', 1);
db.openDB();
setTimeout(function () {
db.updateDataByKey('students',11,25);
db.closeDB();
}, 500);
}
这样就把名为students的store里的id为11的那条数据对应的age值修改为25.
9.查找数据
根据id值将对应的name和age查询出来:
//根据键值查询数据
getDataByKey(storeName,key){
var store = this.getStoreByName(storeName);
var request = store.get(key);
request.onsuccess = function (e) {
var student = e.target.result;
console.log('key is '+key+',name:'+student.name+',age:'+student.age);
}
}
10.索引
以上介绍了indexedDB的基本使用,接下来我们介绍一下indexedDB的索引,索引的好处是可以快速定位数据,提高搜索速度。在indexedDB中有两种索引,一种是自增长的int值,由indexedDB自己定义;另一种是keyPath,由我们自己指定,重点来看看keyPath方式索引的使用
11.建立索引
可以在创建object store的时候指明索引,使用object store的createIndex创建索引,有三个参数
- 索引名称
- 索引属性字段名
- 索引属性值是否唯一
例:我们为name和age两个属性建立对应的索引,可以将onupgradeneeded写为下面的方式:
request.onupgradeneeded = function (e) {
console.log('database version is already upgrade to '+this.DBversion);
this.currentDB = e.target.result;
if(!this.currentDB.objectStoreNames.contains('students')){
//指定一个键为主键
var store = this.currentDB.createObjectStore('students',{keyPath:"id"});
//指定为主键自增模式
//db.createObjectStore('students',{autoIncrement: true});
store.createIndex('nameIndex','name',{unique:true});
store.createIndex('ageIndex','age',{unique:false});
}
}.bind(this);
索引建立成功后在chromium可以看到对应的索引:
12.根据索引查询数据
//根据索引查询数据
getDataByIndex(storeName,indexName,index){
var store = this.getStoreByName(storeName);
var indexStore = store.index(indexName);
var request = indexStore.get(index);
request.onsuccess = function (e) {
var student = e.target.result;
console.log('index is '+index+',id:'+student.id+',age:'+student.age);
}
}
例如我们要查询nameIndex为zhangsan的那条数据:
db.getDataByIndex('students','nameIndex','zhangsan');
13.游标cursor
在indexedDB中使用索引和游标是分不开的,如果查询结果是多个数据,我们就可以使用游标来遍历查询结果了:
//通过游标获取所有数据
fetchStoreByCurser(storeName) {
var store = this.getStoreByName(storeName);
var request = store.openCursor();
request.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
var currentStudent = cursor.value;
console.log('currentStudent:' + currentStudent.name);
//curson.contine()会使游标下移,直至没有数据则返回undefined
cursor.continue();
}
}
}
14.游标和索引结合查询
//通过游标和index查询
getMultipleData(storeName, indexName, age) {
var store = this.getStoreByName(storeName);
var indexStore = store.index(indexName);
var request = indexStore.openCursor(IDBKeyRange.only(age));
request.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
var student = cursor.value;
console.log(student.name);
cursor.continue();
}
};
}
例如我们要查询所有age为26的数据:
db.getMultipleData('students', 'ageIndex', 26);
15.指定游标范围
index.openCursor()/index.openKeyCursor()方法在不传递参数的时候会获取object store所有记录,像上面例子一样我们可以对搜索进行筛选
可以使用key range 限制游标中值的范围,把它作为第一个参数传给 openCursor() 或openKeyCursor()
- IDBKeyRange.only(value):只获取指定数据
- IDBKeyRange.lowerBound(value,isOpen):获取最小是value的数据,第二个参数用来指示是否排除value值本身,也就是数学中的是否是开区间
- IDBKeyRange.upperBound(value,isOpen):和上面类似,用于获取最大值是value的数据
- IDBKeyRange.bound(value1,value2,isOpen1,isOpen2):获取位于 value1和
value2之间的数据, isOpen1和 isOpen2也是是否开区间,值为true或者false,
举个例子:
//指定游标范围查询
getDataBetweenTwoData(storeName,indexName,start,end,isStartOpen,isEndOpen) {
var store = this.getStoreByName(storeName);
var indexStore = store.index(indexName);
//true是不包括,false是包括
var request = indexStore.openCursor(IDBKeyRange.bound(start, end, isStartOpen, isEndOpen));
request.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
var student = cursor.value;
console.log(student.name);
cursor.continue();
}
}
}
比如查询age在24和30之间的所有数据,包括24但不包括30,可以这么查:
db.getDataBetweenTwoData('students','ageIndex',24,30,false,true);
16.获取某个store中存储条目的总数
getStoreDataCount(storeName) {
var store = this.getStoreByName(storeName);
var req = store.count();
req.onsuccess = function (e) {
console.log(e.target.result);
}
}
17.获取store中满足多个条件的条目的总数
在多个条件的前提下查询我们首先要有一个多条件查询的索引。由于数据有限,我们将创建一个由name和age组成的多条件索引,在onupgradeneeded中,为‘students 创建一个索引:
store.createIndex('myIndex',['name','age'],{unique:false});
之后我们写一个函数:
//获取store中满足多个条件的条目的总数
getDataCountByMultiCondition(storeName,indexName,condition) {
var store = this.getStoreByName(storeName)
var indexStore = store.index(indexName);
var request = indexStore.openCursor(IDBKeyRange.only(condition));
request.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
var student = cursor.value;
console.log(student.id);
cursor.continue();
}
}
}
然后查询所有名为“lisi”,年龄为30的数据:
db.getDataCountByMultiCondition('students','myIndex',['lisi',30]);
18.如何获取到返回值
在上面的例子中,我们所获取到的数据都是用在函数中用log打印出结果,但是在实际中,我们需要将查询到的结果获取到其他地方,但是在request的回调里我们是无法直接将结果作为函数返回值直接return给函数调用者的,这里也要使用回调将获取到的值传递出去,比如我们重写获取某个store中存储条目的总数这个方法:
//获取某个store中存储条目的总数
fetchStoreDataCount(storeName,callback) {
var count = 0;
var store = this.getStoreByName(storeName);
var req = store.count();
req.onsuccess = function (e) {
count = e.target.result;
callback(count);
}
}
其中callback为回调函数,我们将返回值用callback传递出去:
db.fetchStoreDataCount('students',function (count) {
console.log(count);
});
用这种方法,我们可以将任意查询获得的值用callback传递出去,可以试下用这个方法将上面所有查询到的数据传递出去,只需要在调用的地方实现callback函数即可
19.其他
有些同学可能注意到了一个地方,在openDB的方法中,request的onsuccess,onerror以及onupgradeneeded后都加了.bind(this),原因如下:
因为我用的是ES6中的class来实现了整个DBmanager,但是js中的this并不完全和java中的this一样指的就是当前对象,比如说这里:request的onsuccess,onerror以及onupgradeneeded回调函数中的this指的并不是DBmanager对象,而是指request,所以为了在onsuccess中改变DBmanager改变成员变量的值,所以将指向DBmanager的this值bind到onsuccess中。
除此之外,在DBmanager实现过程中,为了方便并没有用get,set方法去获取和设置成员变量的值,而且很多地方还需进一步改进,这里只是给各位一个简单的参考,欢迎批评指正。
20.源代码
indexedDB.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
class DBmanager {
//数据库管理类的构造函数,包含两个构造参数,数据库名称和数据库版本,数据库版本如果不指定默认为1;
constructor(DBname, DBversion) {
this.DBname = DBname;
this.DBversion = DBversion || 1;
this.currentDB = null; //当前数据库
}
//打开(没有创建过则创建)数据库
openDB() {
//调用API打开数据库
var request = window.indexedDB.open(this.DBname, this.DBversion);
//打开数据库失败的回调
request.onerror = function (e) {
console.log(e.currentTarget.error.message);
}.bind(this);
//打开数据库成功的回调
request.onsuccess = function (e) {
this.currentDB = e.target.result;
console.log(this.currentDB.name + ' database is already opened!');
}.bind(this);
//数据库升级的回调,此回调在数据库版本变化时和数据库第一次创建时执行
request.onupgradeneeded = function (e) {
console.log('database version is already upgrade to ' + this.DBversion);
this.currentDB = e.target.result;
if (!this.currentDB.objectStoreNames.contains('students')) {
//指定一个键为主键
var store = this.currentDB.createObjectStore('students', {keyPath: "id"});
//指定为主键自增模式
//db.createObjectStore('students',{autoIncrement: true});
store.createIndex('nameIndex', 'name', {unique: false});
store.createIndex('ageIndex', 'age', {unique: false});
store.createIndex('myIndex', ['name', 'age'], {unique: false});
}
}.bind(this);
}
//关闭数据库
closeDB() {
this.currentDB.close();
console.log(this.currentDB.name + ' database is already closed!');
}
//删除数据库
deleteDB() {
window.indexedDB.deleteDatabase(this.DBname);
console.log(this.DBname + ' database is already deleted!');
}
//根据storeName获取对应store的方法
getStoreByName(storeName) {
var transaction = this.currentDB.transaction(storeName, 'readwrite');
return transaction.objectStore(storeName);
}
//添加数据
addData(storeName, data) {
var store = this.getStoreByName(storeName);
for (var i = 0; i < data.length; i++) {
store.add(data[i]);
}
}
//根据键值修改数据
updateDataByKey(storeName, key, age) {
var store = this.getStoreByName(storeName);
var request = store.get(key);
request.onsuccess = function (e) {
var student = e.target.result;
student.age = age;
store.put(student);
}
}
//根据键值查询数据
getDataByKey(storeName, key) {
var store = this.getStoreByName(storeName);
var request = store.get(key);
request.onsuccess = function (e) {
var student = e.target.result;
console.log('key is ' + key + ',name:' + student.name + ',age:' + student.age);
}
}
//根据索引查询数据
getDataByIndex(storeName, indexName, index) {
var store = this.getStoreByName(storeName);
var indexStore = store.index(indexName);
var request = indexStore.get(index);
request.onsuccess = function (e) {
var student = e.target.result;
console.log('index is ' + index + ',id:' + student.id + ',age:' + student.age);
}
}
//通过游标获取所有数据
fetchStoreByCurser(storeName) {
var store = this.getStoreByName(storeName);
var request = store.openCursor();
request.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
var currentStudent = cursor.value;
console.log('currentStudent:' + currentStudent.name);
//curson.contine()会使游标下移,直至没有数据则返回undefined
cursor.continue();
}
}
}
//通过游标和index查询
getMultipleData(storeName, indexName, age) {
var store = this.getStoreByName(storeName);
var indexStore = store.index(indexName);
var request = indexStore.openCursor(IDBKeyRange.only(age));
request.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
var student = cursor.value;
console.log(student.name);
cursor.continue();
}
};
}
//指定游标范围查询
getDataBetweenTwoData(storeName, indexName, start, end, isStartOpen, isEndOpen) {
var store = this.getStoreByName(storeName);
var indexStore = store.index(indexName);
//true是不包括,false是包括
var request = indexStore.openCursor(IDBKeyRange.bound(start, end, isStartOpen, isEndOpen));
request.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
var student = cursor.value;
console.log(student.name);
cursor.continue();
}
}
}
//获取某个store中存储条目的总数
getStoreDataCount(storeName) {
var store = this.getStoreByName(storeName);
var req = store.count();
req.onsuccess = function (e) {
console.log(e.target.result);
}
}
//获取store中满足多个条件的条目的总数
getDataCountByMultiCondition(storeName, indexName, condition) {
var store = this.getStoreByName(storeName)
var indexStore = store.index(indexName);
var request = indexStore.openCursor(IDBKeyRange.only(condition));
request.onsuccess = function (e) {
var cursor = e.target.result;
if (cursor) {
var student = cursor.value;
console.log(student.id);
cursor.continue();
}
}
}
//获取某个store中存储条目的总数
fetchStoreDataCount(storeName, callback) {
var count = 0;
var store = this.getStoreByName(storeName);
var req = store.count();
req.onsuccess = function (e) {
count = e.target.result;
callback(count);
}
}
}
//名为students的ObjectStore
const datas = [{
id: 11,
name: "zhangsan",
age: 24
}, {
id: 12,
name: "lisi",
age: 30
}, {
id: 13,
name: "wangwu",
age: 26
}, {
id: 14,
name: "zhaoliu",
age: 26
},];
window.onload = function () {
var db = new DBmanager('testDB', 4);
db.openDB();
setTimeout(function () {
// db.getDataCountByMultiCondition('students','myIndex',['lisi',30]);
// db.getDataBetweenTwoData('students','ageIndex',24,30,false,true);
// db.getMultipleData('students', 'ageIndex', 26);
// db.fetchStoreByCurser('students');
// db.getDataByIndex('students','nameIndex','zhangsan');
// db.addData('students',datas);
// db.getDataByKey('students',11);
db.fetchStoreDataCount('students', function (count) {
console.log(count);
});
db.closeDB();
// db.deleteDB();
}, 500);
}
</script>
</body>
</html>