重学 JS | 一文看懂浏览器数据库 IndexedDB 详细操作
IndexedDB 是 Indexed Database API 的简称,是在浏览器中存储大量结构化数据(包括文件/ blob)的一种数据库,是对 Web 存储(存储少量数据)的一种补充,属于事务数据库系统。它的 API 使用索引来对这些数据进行高性能搜索,并且通过 API 可以方便保存和读取 JavaScript 对象。它的操作完全是异步进行的,通常要注册 onerror 或者 onsuccess 事件处理程序,监听操作的结果。
不同于 MySql 或 Oracle 这类用表来保存数据的数据库,IndexedDB 采用的是使用对象保存数据。因此 IndexedDB 可以理解为一组在相同命名空间下的对象的集合。
IndexedDB 相关 API
数据库:IDBDatabase 对象
数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。IndexedDB 数据库有版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除表、索引或者主键),只能通过升级数据库版本完成。
对象仓库:IDBObjectStore 对象
每个数据库包含若干个对象仓库(object store)。它类似于关系型数据库的表格
索引:IDBIndex 对象
为了加速数据的检索,可以在对象仓库里面,为不同的属性建立索引。
事务:IDBTransaction 对象
数据记录的读写和删改,都要通过事务完成。事务对象提供error
、abort
和complete
三个事件,用来监听操作结果。
操作请求:IDBRequest 对象
指针:IDBCursor 对象
主键集合:IDBKeyRange 对象
操作 IndexedDB 的基本模式
打开一个数据库
在数据库中创建对象存储
启动事务并请求执行一些数据库操作,例如添加或检索数据
过侦听事件来等待操作完成
对结果做一些事情(可以在 request object 上找到)
API 使用及流程详解
let req,db;
let version = 2
/*
* 【第一:创建数据库或升级数据库】
* 发送打开'demo'数据库的请求,若不存在先创建,再打开,返回一个IDBOpenDBRequest请求实例
* req解答:
* 原型链: req.__proto__ = IDBOpenDBRequest;IDBOpenDBRequest.__proto__ = IDBRequest
* IndexedDB中的大多数其他异步函数执行都是相同的操作-返回带有onerror或onsuccess的IDBRequest对象
*
* version解答:
* 数据库的版本,确定数据库模式-数据库中存储的对象及其结构.
*/
req = indexedDB.open('demo',version)
req.onerror = function(e){
console.log('发生错误',e.target.errorCode)
}
// 成功
req.onsuccess = function(e){
// 返回的是IDBDatabase数据库实例
db = e.target.result
/*
* 处理错误事件:
* 错误事件是冒泡的。错误事件以生成错误的请求为目标,然后事件冒泡到事务中,最后到数据库对象。
* 如果要避免向每个请求添加错误处理程序,则可以在数据库对象上添加单个错误处理程序。
*/
db.onerror = function(event) {
console.error("Database error: " + event.target.errorCode);
}
}
/*
* 【第二:监听数据库升级】
* 创建或更新数据库的版本将触发此事件
* 是唯一可以更改数据库结构的地方。在其中可以创建和删除对象存储以及构建和删除索引。
*/
req.onupgradeneeded = function(e){
db = e.target.result
/*
* 【第三:创建表】
* 为此数据库创建一个objectStore createObjectStore(存储名,参数对象)
* 可以理解为:创建一张user的表,主键是id,若无合适主键,可配置自增主键{autoIncrement: true
*/
var objectStore
if(!db.objectStoreNames.contains('user')){
// 不存在user表,则创建user表,主键为id
objectStore =db.createObjectStore("user", { keyPath: "id" });
}
/*
* 【第四:创建索引】
* (索引名称,索引所在的属性,索引配置对象)
*/
objectStore.createIndex("id", "id", { unique: true });
/*
* 【第五:写入数据】
* 通过IDBTransaction.objectStore(name)方法,拿到 IDBObjectStore 对象,
* 再通过表格对象的add()方法,向表格写入一条记录
* 写入过程是异步的,通过监听success/error获取写入结果
*/
var addUserReq = db.transaction(['user'],'readwrite')
.objectStore('user').add({id:1,name:'张三',age:18})
addUserReq.onsuccess = function(){console.log('user表写入数据成功')}
addUserReq.onerror = function(){console.log('user表写入数据失败')}
/*
* 【第六:读取数据】
* 读取数据也是通过事务完成
*/
var readTransaction = db.transaction(['user'])
var readUserObjStore = readTransaction.objectStore('user');
// 读取主键为1的数据
var readReq = readUserObjStore.get(1)
readReq.onerror = function(){console.log('读取数据失败')}
readReq.onsuccess = function(event){
if (request.result) {
console.log('Name: ' + request.result.name);
console.log('Age: ' + request.result.age);
} else {
console.log('未获得数据记录');
}
}
/*
* 【第七:更新数据】
*/
var putUserReq = db.transaction(['user'],'readwrite')
.objectStore('user').put({id:1,name:'张三',age:20})
putUserReq.onsuccess = function(){console.log('user表更新数据成功')}
putUserReq.onerror = function(){console.log('user表更新数据失败')}
}
/*
* 【第八:删除数据】
*/
var deleteUserReq = db.transaction(['user'],'readwrite')
.objectStore('user').delete(1)
deleteUserReq.onsuccess = function(){console.log('user表删除数据成功')}
/*
* 【第九:遍历表格数据】
* 使用针对对象IDBCursor
*/
var cursorStore = db.transaction('user').objectStore('user')
cursorStore.openCursor().onsuccess = function(event){
var cursor = event.target.result
if(cursor){
console.log('id:'+cursor.key)
console.log('name:'+cursor.value.name)
console.log('age:'+cursor.value.age)
cursor.continue()
}else{console.log('无更多数据')}
}
/*
* 【第十:自定义索引】
*/
objectStore.createIndex('name', 'name', { unique: false });
var transaction = db.transaction(['user'], 'readonly');
var store = transaction.objectStore('user');
var index = store.index('name');
var request = index.get('张三');
request.onsuccess = function (e) {
var result = e.target.result;
console.log('result',result)
}
简易封装 IndexedDB
import isString from 'lodash/isString'
/**
* @param dbName 数据库名称
* @param version 数据库版本 不传默认为1
* @param primary 数据库表主键
* @param indexList Array 数据库表的字段以及字段的配置,每项为Object,结构为{ name, keyPath, options }
*/
class WebDB {
constructor({dbName, version, primary, indexList}) {
this.db = null
this.objectStore = null
this.request = null
this.primary = primary
this.indexList = indexList
this.version = version
this.intVersion = parseInt(version.replace(/\./g, ''))
this.dbName = dbName
try {
this.open(dbName, this.intVersion)
} catch (e) {
throw e
}
}
open (dbName, version) {
const indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB || window.msIndexedDB;
if (!indexedDB) {
console.error('你的浏览器不支持IndexedDB')
}
this.request = indexedDB.open(dbName, version)
this.request.onsuccess = this.openSuccess.bind(this)
this.request.onerror = this.openError.bind(this)
this.request.onupgradeneeded = this.onupgradeneeded.bind(this)
}
onupgradeneeded (event) {
console.log('onupgradeneeded success!')
this.db = event.target.result
const names = this.db.objectStoreNames
if (names.length) {
for (let i = 0; i< names.length; i++) {
if (this.compareVersion(this.version, names[i]) !== 0) {
this.db.deleteObjectStore(names[i])
}
}
}
if (!names.contains(this.version)) {
this.objectStore = this.db.createObjectStore(this.version, { keyPath: this.primary })
this.indexList.forEach(index => {
const { name, keyPath, options } = index
this.objectStore.createIndex(name, keyPath, options)
})
}
}
openSuccess (event) {
console.log('openSuccess success!')
this.db = event.target.result
}
openError (event) {
console.error('数据库打开报错', event)
// 重新链接数据库
if (event.type === 'error' && event.target.error.name === 'VersionError') {
indexedDB.deleteDatabase(this.dbName);
this.open(this.dbName, this.intVersion)
}
}
compareVersion (v1, v2) {
if (!v1 || !v2 || !isString(v1) || !isString(v2)) {
throw '版本参数错误'
}
const v1Arr = v1.split('.')
const v2Arr = v2.split('.')
if (v1 === v2) {
return 0
}
if (v1Arr.length === v2Arr.length) {
for (let i = 0; i< v1Arr.length; i++) {
if (+v1Arr[i] > +v2Arr[i]) {
return 1
} else if (+v1Arr[i] === +v2Arr[i]) {
continue
} else {
return -1
}
}
}
throw '版本参数错误'
}
/**
* 添加记录
* @param record 结构与indexList 定下的index字段相呼应
* @return Promise
*/
add (record) {
if (!record.key) throw '需要添加的key为必传字段!'
return new Promise((resolve, reject) => {
let request
try {
request = this.db.transaction([this.version], 'readwrite').objectStore(this.version).add(record)
request.onsuccess = function (event) {
resolve(event)
}
request.onerror = function (event) {
console.error(`${record.key},数据写入失败`)
reject(event)
}
} catch (e) {
reject(e)
}
})
}
/**
* 读取记录
* @param key 主键的值
* @return Promise
*/
get (key) {
return new Promise((resolve, reject) => {
setTimeout(() => {
let request
try {
request = this.db.transaction([this.version]).objectStore(this.version).get(key)
request.onerror = function (event) {
console.error(`${key}, 数据读取失败!`)
reject(event)
}
request.onsuccess = function (event) {
if (request.result) {
resolve(request.result)
} else {
reject(event)
}
}
} catch (e) {
reject(e)
}
}, 200)
})
}
/**
* 更新记录
* @param record 结构与indexList 定下的index字段相呼应
* @return Promise
*/
update (record) {
return new Promise((resolve, reject) => {
// const request = this.db.transaction([this.version], 'readwrite').objectStore(this.version).put(record)
let request
try {
request = this.db.transaction([this.version], 'readwrite').objectStore(this.version).put(record)
request.onsuccess = function (event) {
resolve(event)
};
request.onerror = function (event) {
console.error(`${record.key},数据更新失败`)
reject(event)
}
} catch (e) {
reject(e)
}
})
}
/**
* 删除记录
* @param key 主键的值
* @return Promise
*/
remove (key) {
return new Promise((resolve, reject) => {
const request = this.db.transaction([this.version], 'readwrite').objectStore(this.version).delete(key);
request.onsuccess = function (event) {
resolve(event)
}
request.onerror = function (event) {
console.error(`${key}, 数据删除失败`)
reject(event)
}
})
}
}
export default WebDB
总结
至此我们学习的 IndexedDB 数据库的使用,关于使用场景,可以在做离线缓存等场景中使用,而我当时在做 chrome 扩展程序热更新方案时候使用到,用于缓存热更新业务代码到本地提升资源效率。
最后附上:Chrome 扩展程序文章
解决大中型浏览器 (Chrome) 插件开发痛点:自定义热更新方案——1. 原理分析及构建部署实现
解决大中型浏览器 (Chrome) 插件开发痛点:自定义热更新方案——2. 基于双缓存更新功能模块
版权声明: 本文为 InfoQ 作者【梁龙先森】的原创文章。
原文链接:【http://xie.infoq.cn/article/f86e8f2e9181aa531a29ab06e】。文章转载请联系作者。
梁龙先森
脚踏V8引擎的无情写作机器 2018.03.17 加入
还未添加个人简介
评论