目录
参考资料
https://wangdoc.com/javascript/bom/indexeddb.html#%E6%89%93%E5%BC%80%E6%95%B0%E6%8D%AE%E5%BA%93
https://cloud.tencent.com/developer/article/1341973
《HTML5与CSS3权威指南》 - 陆凌牛
Github(一些小demo)
https://github.com/ChenMingK/indexedDB
相信大部分人都会有这么个疑问,这个类似于sql数据库的indexedDB到底可以用来干嘛。比如做一个移动端电子书阅读的项目,要做一个书架缓存和离线阅读之类的功能就能用到indexedDB;一个epub格式的电子书大概几M用localStorage显然就是不行的。
背景
现有的浏览器数据储存方案,都不适合储存大量数据:Cookie 的大小不超过 4KB,且每次请求都会发送回服务器;LocalStorage 在 2.5MB 到 10MB 之间(各家浏览器不同),而且不提供搜索功能,不能建立自定义的索引。所以,需要一种新的解决方案,这就是 IndexedDB 诞生的背景。
通俗地说,IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。
特点
(1)键值对储存。 IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以“键值对”的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。
(2)异步。 IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
(3)支持事务。 IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
(4)同源限制 IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。(看这里http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html)
(5)储存空间大 IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。
(6)支持二进制储存。 IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。
术语
IndexedDB 是一个比较复杂的 API,涉及不少概念。它把不同的实体,抽象成一个个对象接口。学习这个 API,就是学习它的各种对象接口。
- 数据库:IDBDatabase 对象
- 对象仓库:IDBObjectStore 对象
- 索引: IDBIndex 对象
- 事务: IDBTransaction 对象
- 操作请求:IDBRequest 对象
- 指针: IDBCursor 对象
- 主键集合:IDBKeyRange 对象
(1)数据库
数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。IndexedDB 数据库有版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除数据仓库、索引或者主键),只能通过升级数据库版本完成。
(2)对象仓库
每个数据库包含若干个对象仓库(object store)。它类似于关系型数据库的表格。
(3)数据记录
对象仓库保存的是数据记录。每条记录类似于关系型数据库的行,但是只有主键和数据体两部分。主键用来建立默认的索引,必须是不同的,否则会报错。主键可以是数据记录里面的一个属性,也可以指定为一个递增的整数编号。
{ id: 1, text: 'foo' }
上面的对象中,id
属性可以当作主键。数据体可以是任意数据类型,不限于对象。
(4)索引
为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。
(5)事务
数据记录的读写和删改,都要通过事务完成。事务对象提供error
、abort
和complete
三个事件,用来监听操作结果。
创建数据库、对象仓库、索引
首先通过一个简单的demo来介绍如何创建/连接indexedDB数据库、对象仓库和索引
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>连接indexedDB数据库</title>
<script>
function connectDatabase() {
let dbName = 'indexedDBTest'; //数据库名
let dbVersion = 20190315; //版本号
let idb;
// 连接数据库,dbConnect对象为一个IDBOpenDBRequest对象,代表数据库连接的请求对象
let dbConnect = indexedDB.open(dbName, dbVersion);
dbConnect.onsuccess = function (e) {
// e.target.result为一个IDBDatabase对象,代表连接成功的数据库
idb = e.target.result;
console.log('数据库连接成功');
};
dbConnect.onerror = function (e) {
console.log('数据库连接失败')
};
/*
监听数据库连接请求对象的onupgradeneeded事件,当连接数据库时发现指定的版本号大于数据库当前版本号时触发该事件,
当该事件被触发时一个数据库的版本更新事务已经被开启,同时数据库的版本号已经被自动更新完毕,下面是指定其回调函数
要执行哪些操作
*/
dbConnect.onupgradeneeded = function (e) {
//数据库版本更新
//e.target.result为一个IDBDatabase对象,代表连接成功的数据库对象
idb = e.target.result;
//对象仓库创建部分
let tx = e.target.transaction; //e.target.transaction属性值为一个IDBTransaction事务对象,此处代表版本更新事务
let name = 'Users';
let optionalParameters = {
keyPath: 'userId',
autoIncrement: false
};
let store = idb.createObjectStore(name, optionalParameters); //参数:对象仓库名, JavasScript对象(可选参数)
//返回一个IDBObjectStore对象,该对象代表被创建成功的对象仓库
//索引创建部分
let IndexName = 'userNameIndex';
let keyPath = 'userName';
let IndexOptionalParameters = {
unique: false,
multiEntry: false
};
let idx = store.createIndex(IndexName, keyPath, IndexOptionalParameters); //参数:索引名,
console.log('索引创建成功');
}
}
</script>
</head>
<body>
<input type="button" value="创建对象仓库和索引" οnclick="connectDatabase();">
</body>
</html>
const request = window.indexedDB.open(databaseName, version); //连接数据库
这个方法接受两个参数,第一个参数是字符串,表示数据库的名字。如果指定的数据库不存在,就会新建数据库。第二个参数是整数,表示数据库的版本。如果省略,打开已有数据库时,默认为当前版本;新建数据库时,默认为1
。
indexedDB.open()
方法返回一个 IDBRequest 对象。这个对象通过三种事件error
、success
、upgradeneeded
,处理打开数据库的操作结果。
onerror 连接失败
onsuccess 连接成功
onupgradeneeded 版本更新
如果指定的版本号,大于数据库的实际版本号,就会发生数据库升级事件 onupgradeneeded
如果要修改数据库结构(新增或删除数据仓库、索引或者主键),只能写在这个事件的回调函数中。
上面的代码在Chrome浏览器中的运行结果,可以看到我们新建了一个叫"indexedDBTest"的数据库,创建了一个名为"Users"的数据仓库(表),同时创建了一个名为"userNameIndex"的索引。下面详细介绍下新建对象仓库和新建索引的参数是如何设置的。
indexedDB.createObjectStore(name, optionalParameters)
该方法返回一个IDBObjectStore对象,该对象代表被创建成功的对象仓库。
第一个参数为对象仓库名,第二个参数为可选参数,参数值为一个JavaScript对象,该对象的KeyPath属性值用于指定对象仓库中的每一条记录使用哪个属性值来作为记录的主键值。
注意:
一个对象仓库中只能有一个主键,但是主键值可以重复,一条记录的主键为数据仓库中该记录的唯一标识符。
如果主键在每条记录的内部,则称为内联主键(inline key)
如果主键通过其他途径指定,存在于每条记录之外,则称为外部主键(out-of-line key)
optionalParameters对象的autoIncrement属性值为true,相当于关系型数据库中将主键值指定为自增主键,如果添加数据记录时不指定主键值,则在数据仓库内部将自动指定该主键值为既存的最大主键值+1;也可以在添加数据记录时显示地指定主键值。如果该值为false,则必须在添加记录时显示地指定主键值。
store.createIndex(name, keyPath, optionalParameters)
该方法返回一个IDBIndex对象,代表创建索引成功。
store为一个IDBObjectStore对象,即由createObjectStore这个创建对象仓库的方法返回的对象。
第一个参数值为一个字符串,代表索引名
第二个参数值表示使用数据仓库中的数据记录对象的哪个属性来创建索引
第三个参数为可选参数,参数值为一个JavaScript对象,该对象的unique属性为true时表示同一对象仓库不能有第二个与该索引属性值(keyPath)相同的索引,如果创建会失败。multiEntry属性值为true时表示如果数据记录的索引属性值(keyPath)为一个数组,可以将数组中的每一个元素添加到索引中,为false则表示只能将该数组整体添加在索引中。
关于multiEntry属性,举这么个例子:
假设有如下一个记录对象
{
id: 12345,
title: '文章标题',
body: '文章正文',
tags: ['HTML','JavaScript','PHP']
}
tags属性值为一个数组,在indexedDB数据库中是可以将属性值保存为一个数组的。
如果将tags属性创建为一个索引且该索引的multiEntry设定为true,则无论使用HTML、JavaScript、还是PHP都可以检索出该条记录;如果设定为false,则必须使用“['HTML','JavaScript','PHP']”这种数组的形式对tags属性进行检索才可以检索出该记录。
数据的增、删、查、改
首先介绍如何往数据仓库新增记录,然后是如何获取记录(使用主键或使用索引),接着是如何检索记录(游标的使用)
请结合下面的demo来看
<!-- 请先使用前面的创建对象仓库和索引.html创建数据库和表以及索引先 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>连接indexedDB数据库</title>
</head>
<body>
<input type="button" id="btnConnectDataBase" value="连接数据库" οnclick="ConnectDataBase()">
<input type="button" id="btnSaveData" value="保存数据" οnclick="SaveData()">
<input type="button" id="btnSearchData" value="获取数据" οnclick="getData()">
<script>
let dbName = 'indexedDBTest'; //数据库名
let dbVersion = '20190316'; //版本号
let idb;
function ConnectDataBase(){
//连接数据库
let dbConnect = indexedDB.open(dbName, dbVersion);
dbConnect.onsuccess = function(e){
idb = e.target.result; //引用IDBDatabase对象
console.log('数据库连接成功');
};
dbConnect.onerror = function(e){
console.log('数据库连接失败');
};
dbConnect.onupgradeneeded = function(e){
//数据库版本更新
console.log('数据库版本更新成功');
idb = e.target.result;
let tx = e.target.transaction;
let name = 'Users2';
let optionalParameters = {
keyPath: 'userId', //主键值
autoIncrement: false
};
let store = idb.createObjectStore(name, optionalParameters);
console.log('对象仓库User2创建成功');
let idxName = 'userNameAddressIndex'; //索引名
let idxKeyPath = ['userName', 'address']; //索引属性
let idxOptionalParameters = {
unique:false,
multiEntry: false
};
let idx = store.createIndex(idxName, idxKeyPath, idxOptionalParameters);
console.log('索引创建成功');
}
}
function SaveData(){
//开启读写事务
let tx = idb.transaction(['Users2'],'readwrite');
tx.oncomplete = function(){
console.log('保存数据成功');
}
tx.onabort = function(){
console.log('保存数据失败');
}
let store = tx.objectStore('Users2');
let data1 = {
userId: 1,
userName: '张三',
address: '住址1'
};
store.put(data1); //使用put而不是add直接更新数据
let data2 = {
userId: 2,
userName: '用户B',
address: '住址2'
};
store.put(data2);
let data3 = {
userId: 3,
userName: '用户C',
address: '住址3'
};
store.put(data3);
let data4 = {
userId: 4,
userName: '用户D',
address: '住址4'
};
store.put(data4);
}
function getData(){
let tx = idb.transaction(['Users2'], 'readonly');
let store = tx.objectStore('Users2');
let idx = store.index('userNameAddressIndex');
let req = idx.get(['用户D','住址4']); //复合索引
req.onsuccess = function(){
if(this.result == undefined){
console.log('没有复合的数据');
}
else{
console.log('获取数据成功,主键值为: ' + this.result.userId);
}
}
req.onerror = function(){
console.log('获取数据失败');
}
}
</script>
</body>
</html>
看看我们保存的数据是什么样的
在indexedDB API中,所有针对数据的操作都只能在一个事务中被执行。
let tx = idb.transaction(storeNames, mode) //idb为某个已连接的数据库
该方法返回一个IDBTransaction对象,代表被开启的事务。
第一个参数值可以为由数据仓库名组成的一个字符串数组,用于限定该事务所运行的读写操作范围(只能针对某个/某些数据仓库进行),该参数也可以是一个由对象仓库名所构成的字符串值,如下:
let tx = idb.transaction(‘Users’, ‘readonly’);
可以使用objectStoreNames表示针对所有数据仓库进行的事务
let tx = idb.transaction(idb.objectStoreNames, ‘readonly’)
第二个参数为可选参数,用于定义事务的读写模式,省略时为只读事务。
有两个常量值:“readonly” – 只读事务 “readwrite” – 读写事务
注意:
在indexedDB API中,用于开启事务的transaction函数必须被书写到某一函数中,而且该事务将在函数结束时被自动提交(commit),所以不需要显式调用事务的commit方法来提交事务,但是可以在需要的时候显式调用事务的abort方法来终止事务。
可以通过监听事务对象的success事件(事务成功)complete事件(事务结束时触发)及abort事件(事务被终止时触发)并定义事件处理函数来定义相关处理。
添加数据
IDBObject.put()
1.连接某个indexedDB数据库
2.连接成功后使用该数据库对象的transaction方法开启一个读写事务
3.使用transaction方法返回的开启的事务对象的objectStore方法获取该事务对象的作用范围内的某个对象仓库
4.用该对象仓库的put方法向数据库发出保存数据到对象仓库的请求
function update() {
var request = db.transaction(['person'], 'readwrite')
.objectStore('person')
.put({ id: 1, name: '李四', age: 35, email: 'lisi@example.com' });
request.onsuccess = function (event) {
console.log('数据更新成功');
};
request.onerror = function (event) {
console.log('数据更新失败');
}
}
update();
put方法使用一个参数(一般),参数值为一个需要被保存到对象仓库的对象。
put方法返回一个IDBRequest对象,代表一个向数据库发出的请求。
该请求发出后立即被异步执行,可以通过监听请求对象的success和error事件对请求执行成功或失败做相应的处理。
根据对象仓库的主键是内联主键(主键为某个属性)还是外部主键,主键是否被指定为自增主键,对象仓库的put方法的第一个参数值的指定方法也各不相同,如下:
与put类似的还有一个add方法,区别是:
使用put方法时,如果指定的主键值在对象仓库中已存在,那么该主键值所在数据被更新为使用put方法所保存的数据。
使用add方法时,如果遇到上述情况,则保存失败,不会更新数据。
出于某些原因只能向对象仓库中追加数据而不能更新原有数据时,应该用add方法。
删除数据
IDBObjectStore.delete()
function remove() {
var request = db.transaction(['person'], 'readwrite')
.objectStore('person')
.delete(1);
request.onsuccess = function (event) {
console.log('数据删除成功');
};
}
remove();
这里是根据主键值来删除的,跟put差不多
获取数据
1.通过主键值获取数据(一条数据)
function read() {
var transaction = db.transaction(['person']);
var objectStore = transaction.objectStore('person');
var request = objectStore.get(1);
request.onerror = function(event) {
console.log('事务失败');
};
request.onsuccess = function( event) {
if (request.result) {
console.log('Name: ' + request.result.name);
console.log('Age: ' + request.result.age);
console.log('Email: ' + request.result.email);
} else {
console.log('未获得数据记录');
}
};
}
read();
通过对象仓库:IDBObjectStore 对象的get(key)方法,参数为要获取的数据的主键值
2.利用索引获取数据(一条数据)
假定新建表格的时候,对name
字段建立了索引。
objectStore.createIndex('name', 'name', { unique: false });
现在,就可以从name
找到对应的数据记录了。
var transaction = db.transaction(['person'], 'readonly');
var store = transaction.objectStore('person');
var index = store.index('name');
var request = index.get('李四');
request.onsuccess = function (e) {
var result = e.target.result;
if (result) {
// ...
} else {
// ...
}
}
首先利用对象仓库的index方法获取某个索引,然后利用该索引对象的get方法从对象仓库中获取数据,参数为所需获取对象的索引属性值。
上面的demo使用的是复合索引,即我们创建的索引列表是下面这样的(用数据的userName和address属性作为索引属性)
查询的时候就是像下面这样,index.get(['用户D','住址4'])
遍历数据(使用游标)
如果要对批量的数据进行CRUD操作,就必须使用游标了,或者叫指针对象 IDBCursor
使用游标需要以下步骤
1.连接数据库
2.使用当前连接的数据库对象的transaction方法开启一个事务
3.使用事务对象的objectStore方法获取对象仓库
4.通过对象仓库的openCursor方法创建并打开一个游标
let range = IDBKeyRange.bound(1,4)
let direction = “next”;
let req = store.openCursor(range,direction)
openCursor方法有两个参数,第一个参数为一个IDBKeyRange对象,指定一个对象集合;第二个参数指定游标的读取方向。新建指针对象的openCursor()
方法是一个异步操作,所以要监听success
事件。
IDBKeyRange对象有以下创建方法:
1.使用bound方法
let range = IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen)
该方法返回一个由一批数据的主键值组成的IDBKeyRange集合对象,当游标打开时该集合中包括的所有主键值指向的数据均被读取到游标中。
第一个参数为一个整数值,表示最小主键值;
第二个参数为一个整数值,表示最大主键值;
第三个参数为可选参数,参数值为一个布尔值,默认值为false;false表示返回的集合对象包括最小主键值,true则表示排除。
第四个参数同第三个参数,针对最大主键值设定。
2.使用only方法
let range = IDBKeyRange.only(value)
该方法返回一个由一条数据的主键值组成的IDBKeyRange集合对象。
3.使用lowerBound方法
let range = IDBKeyRange.lowerBound(lower, lowerOpen)
第一个参数指定最小主键值,第二个参数为布尔值,false则包含最小主键值,true则排除,默认为false;
该方法返回的IDBKeyRange集合对象数据中所有的主键值均大于或大于等于参数中指定的主键值。
(设定个下限,返回主键大于或大于等于这个下限的数据)
4.使用upperBound方法
与lowerBound方法类似……
第二个参数用于指定游标的读取方向,有以下取值
“next”:游标中数据按主键值升序排列,主键值相等的数据均被读取到游标中
“nextunique”:游标中数据按主键值升序排列,主键值相等时只读取第一条数据
“prev”:游标中数据按主键值降序排列,主键值相等的数据均被读取到游标中
“prevunique”:游标中数据按主键值降序排列,主键值相等时只读取第一条数据
检索成功后,如果不存在符合检索条件的记录,那么请求对象的result属性值为null或undefined,检索终止。可通过判断该属性值是否为null或undefined来判断监所是否终止并指定检索终止时的处理。
如果存在符合检索条件的数据,那么请求对象的result属性值为一个IDBCursorWithValue对象,该对象的key属性值保存了游标中当前指向的数据记录(游标首次打开时指向游标中的第一条数据记录)的主键值,该对象的value属性值为一个对象,代表该数据记录,可通过访问该对象的各个属性值来获取数据记录的对应属性值。
IDBCursorWithValue对象有以下方法:
update:更新该条记录(事务需为读写事务)
delete:删除该条记录(读写事务)
continue:读取游标中下一条记录
cursor.update({
userID: cursor.key,
userName: 'test',
address: 'test'
})
cursor.delete();
cursor.continue();
具体的demo见“使用游标检索.html”
总结:上面介绍的都是些基本的API,更多的每个indexedDB抽象出来的对象的属性和方法见参考资料的文档;如果有错误欢迎大家指出......
localforage的使用(简化的IndexedDB)
https://www.zhangxinxu.com/wordpress/2018/06/js-localforage-localstorage-indexdb/
下面是基于localforage库做的一些封装可以参考下
// 基于localforage库做封装
// 该库全部采用异步方式操作
import localForage from 'localforage'
// set方法: cb:成功的回调 cb2:失败的回调
export function setLocalForage(key, data, cb, cb2) {
//
localForage.setItem(key, data).then((value) => {
if (cb) cb(value)
}).catch(function(err) {
if (cb2) cb2(err)
})
}
export function getLocalForage(key, cb) {
localForage.getItem(key, (err, value) => {
cb(err, value)
})
}
// 根据给定的key删除指定的值
export function removeLocalForage(key, cb, cb2) {
localForage.removeItem(key).then(function() {
if (cb) cb()
}).catch(function(err) {
if (cb2) cb2(err)
})
}
// 清空
export function clearLocalForage(cb, cb2) {
localForage.clear().then(function() {
if (cb) cb()
}).catch(function(err) {
if (cb2) cb2(err)
})
}
// 获取IndexedDB数据库一共有多少个key
export function lengthLocalForage(cb) {
localForage.length().then(
numberOfKeys => {
if (cb) cb(numberOfKeys)
// console.log(numberOfKeys)
}).catch(function(err) {
// console.log(err)
if (err) {}
})
}
// 遍历:遍历每个元素
export function iteratorLocalForage() {
localForage.iterate(function(value, key, iterationNumber) {
// console.log([key, value])
}).then(function() {
// console.log('Iteration has completed')
}).catch(function(err) {
// console.log(err)
if (err) {}
})
}
export function support() {
const indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || null
if (indexedDB) {
return true
} else {
return false
}
}