简介IndexedDB
详细文档请参看 MDN 文档链接
IndexedDB能做什么:
- 它真的很能存东西!对比cookie,local storeage,session storage 等受到大小限制的web存储方式,IndexedDB在理论上并无大小限制只与本地的磁盘相关。这也是选择它作为web本地存储工具最大的理由。
- 完整的API文档(虽然大部分是英文Orz),不懂的直接翻文档。
- 异步,这意味着不会锁死浏览器,也意味着在进行多库多表操作时会很麻烦,下文中会详细介绍。
废话不多说,让我们直接用一个实际的例子来看 IndexedDB 如何使用。(PS:这一部分算是对 IndexedDB 的简介和科普,本文真正的核心在后面,如果不想看科普可以直接跳到后面)
IndexedDB 需要理解以下几个重要的概念:
数据库: IDBDatabase
对象仓库:IDBObjectStore (我更愿意称之为:表)
索引: IDBIndex
事务: IDBTransaction
操作请求:IDBRequest
指针: IDBCursor
实际项目中一般正常的流程为:
开库 → 建表 → 创建索引 → 存入/删除数据 → 获取数据
这里我们先使用文档中的一个例子(后面再来说哪里存在问题)
const dbName = "the_name";
const customerData = [{ ssn: "444-44-4444", name: "Bill", age: 35, email: "[email protected]" },
{ ssn: "555-55-5555", name: "Donna", age: 32, email: "[email protected]" }];
var request = indexedDB.open(dbName, 2);
request.onsuccess = function(event) {
var db = event.target.result;
// todo
};
request.onupgradeneeded = function(event) {
var db = event.target.result;
var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
objectStore.createIndex("name", "name", { unique: false });
objectStore.createIndex("email", "email", { unique: true });
objectStore.transaction.oncomplete = function(event) {
var customerObjectStore = db.transaction("customers", "readwrite").objectStore("customers");
customerData.forEach(function(customer) {
customerObjectStore.add(customer);
});
};
};
开库:
indexedDB.open(库名,数据库版本)
注意事项:
- 库名必填,版本非必填
- 版本号如果不填则,默认打开当前版本的数据库
- 这个函数的回调即上文中提到的重要概念之一 IDBRequest
IDBRequest:
通常来说我们经常会用到的函数
onsuccess : 成功回调,通俗的讲就是:你可以开始页面的其他操作了。
onupgradeneeded :升级数据库回调,通俗的讲就是:稳一手,再操作。
注意事项
- onupgradeneeded 优先于 onsuccess 触发
-
当仅当数据库版本号 发生变化的时候触发 onupgradeneeded 。换句话说,如果当前版本号为2。
- indexedDB.open('myDB') 只会触发 onsuccess 。
- indexedDB.open('myDB', 3) 同时触发 onsuccess 与 onupgradeneeded 。优先级参看第1条。
- indexedDB.open('myDB', 1) 什么都不会发生 :)
- 当仅当触发 onupgradeneeded 时 可以对 IDBObjectStore 也就是表进行增、删、改。
建表:
event.target.result.createObjectStore('myList',{ keyPath: 'id', autoIncrement: true })
注意事项:
- 第一个参数表名,第二个参数 keyPath 主键名,autoIncrement 主键是否自增。
- 这里有个很隐晦的坑,如果设置主键自增,那么在创建索引的时候可以无需传入主键名,反之则需要传入主键名,后续的例子中会呈现。
- event.target.result 是函数 onupgradeneeded 的返回值,同时也是上文提到的重要概念之一 IDBDatabase 以及它的方法 IDBTransaction
IDBDatabase
这个对象就是通常意义上的数据库本身,我们可以通过这个对象进行表的增、删,以及事物 IDBTransaction 。
IDBTransaction
在IndexedDB中所做的所有事情总是发生在事务的上下文中,表示与数据库中的数据的交互。
IndexedDB中的所有对象——包括对象存储、索引和游标等都与特定事务绑定。
因此,在事务之外不能执行命令、访问数据或打开任何东西。
(PS: 通俗的意义上讲就是...此路是我开,此树是我栽,要想读写数据,请过我这关  ̄□ ̄ )
创建索引
objectStore.createIndex("name", "name", { unique: false });
注意事项
- 第一个和第二个参数均是索引名,unique 如果为true,则索引将不允许单个键有重复的值。
- objectStore 即 IDBObjectStore 也就是表。
- 表数据的增、删、改可以放在 onupgradeneeded 或 onsuccess 中进行(推荐在 onsuccess 中),但是对于表本身和索引的修改仅能在 onupgradeneeded 中。
IDBObjectStore
这个就是表了,它所包含的方法很多都是实际项目中经常用到的比如:
add() 写入数据
createIndex() 创建索引
delete() 删除键
index() 获取索引
get() 检索值
getAll() 检索所有的值
不做过多叙述,详见文档。
注意事项
- 再次重复一遍,这个对象包含的方法涵盖了对表本身以及数据的操作。对本身的操作请在 onupgradeneeded 中,对数据的操作请在 onsuccess 中。
存入/删除数据
还记得 IDBTransaction 和 IDBObjectStore 吗?此时绕不开这俩货
虽说真正执行数据操作的函数是 objectStore.add() 等等,但请在事物IDBTransaction中获取IDBObjectStore对象。
获取数据
同上,原谅我,懒得写 :) 了
正片开始
如果光看上面的例子,其实 IndexedDB 并不复杂。然而在实际项目中却会遇到大量的问题,主要集中在1个问题所引发更多的小问题。
这个问题就是:多库或多表的同时操作。 这也是本文真正的想要表达的东西
在实际项目中,不太可能一张表就写完所有数据,有过数据库操作经验的老哥应该明白。通常我们需要关联两张甚至多张表,即一张表的键值,是另一张表的键或主键,所以我们可以关联这两张表,而不必要也不需要在一张表里写完所有数据。
由于 IndexedDB 是异步实现,所以首先要明确我们究竟在操作哪张表,建立了哪个事物,这个链接完成了吗?等等。
明确上述问题才能解决:为何索引变动会蛋疼到难以言喻?为什么首次进入浏览器创建两张表再写入数据会失效?等一系列问题。
话不多说,先上代码,下面是我对 IndexedDB 的简单封装用作讲解。
class localDB {
constructor(openRequest = {}, db = {}, objectStore = {}) {
this.openRequest = openRequest;
this.db = db;
this.objectStore = objectStore;
Object.getOwnPropertyNames(this.__proto__).map(fn => {
if (this.__proto__[fn] === 'function') {
this[fn] = this[fn].bind(this);
}
})
}
openDB(ops, version) {
let db = Object.assign(new defaultVaule('db'), ops);
this.openRequest = !!version ? window.indexedDB.open(db.name, version) : window.indexedDB.open(db.name);
}
onupgradeneeded() {
const upgradeneed = new Promise((resolve, reject) => {
this.openRequest.onupgradeneeded = (event) => {
this.db = event.target.result;
resolve(this);
}
})
return upgradeneed;
}
onsuccess() {
const success = new Promise((resolve, reject) => {
this.openRequest.onsuccess = (event) => {
this.db = event.target.result;
resolve(this);
}
})
return success;
}
createObjectStore(ops) {
let list = Object.assign(new defaultVaule('list'), ops);
const store = new Promise((resolve, reject) => {
this.objectStore = this.db.createObjectStore(list.name, {
keyPath: list.keyPath,
autoIncrement: list.auto
});
resolve(this);
})
return store;
}
createIndex(ops, save) {
const store = new Promise((resolve, reject) => {
ops.map(data => {
let o = Object.assign(new defaultVaule('idx'), data);
this.objectStore.createIndex(o.name, o.name, {
unique: o.unique
})
})
resolve(this);
})
return store;
}
saveData(type = {}, savedata) {
let save = Object.assign(new defaultVaule('save'), type);
const transAction = new Promise((resolve, reject) => {
let preStore = this.objectStore = this.getObjectStore(save);
preStore.transaction.oncomplete = (event) => {
let f = 0;
let store = this.objectStore = this.getObjectStore(save);
savedata.map(data => {
let request = store.add(data);
request.onsuccess = (event) => {
// todo 这里相当于每个存储完成后的回调,可以做点其他事,也可以啥都不干,反正留出来吧 :)
}
f++;
})
if (f == savedata.length) {
resolve(this);
}
}
})
return transAction;
}
getData(ops, name, value) {
let store = this.getObjectStore(ops);
let data = new Promise((resolve, reject) => {
store.index(name).get(value).onsuccess = (event) => {
event.target.result ? resolve(event.target.result) : resolve('暂无相关数据')
}
})
return data;
}
getAllData(ops) {
let store = this.getObjectStore(ops);
let data = new Promise((resolve, reject) => {
store.getAll().onsuccess = (event) => {
event.target.result ? resolve(event.target.result) : resolve('暂无相关数据')
};
})
return data;
}
deleteData(ops,name) { // 主键名
let store = this.getObjectStore(ops);
store.delete(name).onsuccess = (event) => {
console.log(event);
console.log(this);
}
}
updateData(ops, index, lastValue, newValue) { // index 索引名 lastValue 需要修改的值 newValue 修改后的值
let store = this.getObjectStore(ops);
let data = new Promise((resolve, reject) => {
store.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (cursor.value[index] == lastValue) {
let updateData = cursor.value;
updateData[index] = newValue;
let updateDataRequest = cursor.update(updateData)
updateDataRequest.onsuccess = () => {
resolve('更新完成');
};
}
cursor.continue();
} else {
resolve('找不到指定的值');
}
}
})
return data;
}
getObjectStore(ops) {
return this.db.transaction(ops.name, ops.type).objectStore(ops.name);
}
clear(ops) {
let clear = new Promise((resolve, reject) => {
this.getObjectStore(ops).clear();
resolve(this);
})
return clear
}
deleteStore(name) {
let store = new Promise((resolve, reject) => {
this.db.deleteObjectStore(name);
resolve(this);
})
return store;
}
updateDB() {
let version = this.db.version;
let name = this.db.name;
let update = new Promise((resolve, reject) => {
this.closeDB();
this.openDB({
name: name
}, ++version);
resolve(this);
})
return update;
}
closeDB() {
this.db.close();
this.objectStore = this.db = this.request = {};
}
}
class defaultVaule {
constructor(fn) {
if (typeof this.__proto__[fn] === 'function') {
return this.__proto__[fn]();
}
}
db() {
return {
name: 'myDB',
}
}
list() {
return {
name: 'myList',
keyPath: 'id',
auto: false,
}
}
idx() {
return {
name: 'myIndex',
unique: false,
}
}
save() {
return {
name: 'myList',
type: 'readwrite'
}
}
}
模拟一下用户在使用的时候遇到的场景:
1、打开浏览器 → 因为是首次进入浏览器,这时必然触发 onsuccess 与 onupgradeneeded 。此时我们在 onupgradeneeded 中建表建立索引,存入或者不存入初始数据之类的操作,当然还是根据具体的业务逻辑来。
let db = new localDB();
db.openDB(DB);
db.onsuccess().then(data => {
console.log('onsuccess');
// todo
})
db.onupgradeneeded().then(data => {
console.log('onupgradeneeded');
// todo
})
此处,如果只建立一张表,再存入数据,那写法可能是多样的,例如
db.onupgradeneeded().then(data => {
data.createObjectStore(MAINKEY).then(data => {
data.createIndex(ITEMKEY).then(data => {
console.log('表和索引创建完毕')
})
})
})
db.onsuccess().then(data=>{
data.saveData(SAVETYPE, person).then(data => {
console.log('数据写入完毕');
})
})
这样做,不是不可以,但是没有必要,既然用了promise就不要搞成无限嵌套 推荐使用 async/await 看上去更美滋滋。
async function showDB(db) {
try {
await db.createObjectStore(MAINKEY);
await db.createIndex(ITEMKEY);
return db;
} catch (err) {
console.log(err);
}
}
db.onupgradeneeded().then(data => {
console.log('onupgradeneeded');
showDB(data).then(data=>{
console.log('表以及索引创建完毕')
})
})
用同步写异步,逻辑层面更清晰一点。上述代码其实回归本质依然是
var localIDB = function() {
this.request = this.db = this.objectStore = {};
}
localIDB.prototype = {
openDB: function(ops, callback) {
var ops = this.extend(ops, this.defaultDB());
this.request = window.indexedDB.open(ops.name, ops.version);
return this;
},
onupgradeneeded: function(callback) {
var _this = this;
this.request.onupgradeneeded = function(event) {
_this.db = event.target.result;
callback && callback(event, _this);
}
return this;
},
onsuccess: function(callback) {
var _this = this;
this.request.onsuccess = function(event) {
_this.db = event.target.result;
callback && callback(event, _this);
}
return this;
}
}
var db = new localDB();
db.open().onupgradeneeded(function(event,data){
// todo event是这个事件,data指向对象本身
}).onsuccess(function(event,data){
// todo 同上
})
其实看上去差不多对不对,但如果建立两张表,并分别写入数据呢? async/await 就显得更清晰了
async function showDB(db) {
try {
await db.createObjectStore(MAINKEY);
await db.createIndex(ITEMKEY);
let success = await db.onsuccess(); // 第一次 触发 onsuccess
await success.saveData(SAVETYPE, person); // 第一次 写入数据
await success.updateDB(); // 升级数据库
await success.onupgradeneeded(); // 第二次 触发 onupgradeneeded
await success.createObjectStore(MAINKEY1);
await success.createIndex(ITEMKEY1);
let success1 = await success.onsuccess(); // 第二次 触发 onsuccess
await success1.saveData(SAVETYPE1, personDetail); // 第二次 写入数据
return success1;
} catch (err) {
console.log(err);
}
}
db.onupgradeneeded().then(data => {
console.log('onupgradeneeded');
showDB(data).then(data => {
console.log('两张表,分别写入数据完成');
})
})
db.onsuccess().then(data=>{
console.log('数据库加载完毕');
})
这里有个值得注意的地方:
-
当用户第一次进入时开库建表触发的是 onupgradeneeded 以及完成开库建表操作的 onsuccess 。实际情况也确实如此,但我们在 onupgradeneeded 里面执行了函数 showDB(),于是问题来了:
- 那么,showDB()的返回是什么呢?
- 答:执行了saveData的对象db本身。
-
为什么最外层的
db.onsuccess().then(data=>{ console.log('数据库加载完毕'); })
没有被触发呢?
- 答:async/await 中第一个 onsuccess 的 callback 用来执行写入操作以及之后的升级,第二次建表等等。通俗的来讲大概就是:这是一个异步的且连贯的操作,外层的 onsuccess 根本没机会插手的机会。
- 当用户第二次进入时(刷新页面之列的操作),因为版本号没有变化所以只会触发 onsuccess 。 这个时候就会触发最外层的 onsuccess 了。
让我们举一个简单的查询例子:
假设:我们需要从表1中拿到秀儿的uid,然后用uid去表2中获取秀儿的具体信息。
// html部分代码
// js 部分
// 可以如下嵌套的方式
function getXiuer() {
let uid;
let obj;
db.getData({
name: 'person',
type: 'readonly',
}, 'name', '秀儿').then(data => {
console.log(data)
uid = data.uid;
db.getData({
name: 'detail',
type: 'readonly',
}, 'uid', uid).then(data => {
console.log(data);
});
});
}
// 也可以如下async/await的方式
funtion getXiuer() {
getXiuerWait(db).then(data => {
console.log(data);
})
}
async function getXiuerWait(db) {
try {
let uid;
let data = await db.getData({
name: 'person',
type: 'readonly',
}, 'name', '秀儿');
let result = await db.getData({
name: 'detail',
type: 'readonly',
}, 'uid', data.uid);
return result;
} catch (err) {
console.log(err);
}
}
获取所有数据的返回值是一个数组
db.getAllData({
name: 'detail',
type: 'readonly'
}).then(data => {
console.log(data)
})
想必聪明的你已经发现,其实存入数据库的值可以是多种多样的,字符串、数字、布尔值、数组都是可以的。长度其实也没有特别的限制(反正我连base64的本地图片都存了 o(╥﹏╥)o )
假设:我们需要修改一个已经存在的值(把索引为 age 的值由 60 改为 17)
db.updateData(SAVETYPE1, 'age', 60, 17).then(data => {
console.log(data)
})
总结
IndexedDB只要理清楚开篇的几个概念即:
- 数据库: IDBDatabase
- 对象仓库:IDBObjectStore
- 索引: IDBIndex
- 事务: IDBTransaction
- 操作请求:IDBRequest
- 指针: IDBCursor
以及异步返回的时机,此时此刻在操作哪张表,可以触发哪几个函数,其实是一个蛮好用的工具。
现在再来回答 索引的修改应该如何进行?
答:
- 要么在一开始就设计好索引,避免修改(这是句废话 (ಥ﹏ಥ))
-
如果无可避免,那么可以备份当前索引(getAllData里应有尽有)。再通过升级数据库版本触发 onupgradeneeded 删除以前的表,创建新的表。然而这里又有一个隐晦的坑 o(╥﹏╥)o
- 如果用户刷新页面,也就是说仅触发 onsuccess 。那么,自然要升级一次版本号,在这次升级中触发的 onupgradeneeded 中,让我们来看看索引的建立
var objectStore = db.createObjectStore("customers", { keyPath: "ssn" }); objectStore.createIndex("email", "email", { unique: true });
- objectStore 也就是 IDBObjectStore 对象的获取是通过创立主键来达成的。
- 或者objectStore 也可以通过事物 IDBTransaction 来获取。
- 但这里有个问题 IDBTransaction 尽量在 onsuccess 中,而主键创建在 onupgradeneeded 中,僵住了...
所以我们的代码可能看上去可能应该是这样
// 懒得写 async/await 版本的了 !!(╯' - ')╯︵ ┻━┻ 好累!反正就这意思
db.updateDB().then(data => {
data.onupgradeneeded().then(data => {
data.deleteStore('detail').then(data => {
console.log(data);
// 建表 建包含新索引的索引 再存入数据
})
})
})
看到了吗?这是人干的事儿吗?第一次开库建表的时候就可以弄好的事情,不要搞成这样...
差不多就是这样了,当只有1张表的时候,事情很轻松,但是多张表的时候笑容渐渐变态...
好了,有啥不清楚的,可以留言,如果看到了,而且我会的话,肯定会回答。
最后附上用作存储的测试数据 (可以忽略)
const DB = {
name: 'student',
version: 1
}
const MAINKEY = {
name: 'person',
keyPath: 'id',
auto: true,
}
const ITEMKEY = [{
name: 'name',
unique: false,
}, {
name: 'uid',
unique: true,
}]
const person = [{
name: '秀儿',
uid: '100',
}, {
name: '张三',
uid: '101',
}, {
name: '李敏',
uid: '102',
}, {
name: '日天',
uid: '103',
}]
const SAVETYPE = {
name: 'person',
type: 'readwrite',
}
const MAINKEY1 = {
name: 'detail',
keyPath: 'uid',
auto: false,
}
const ITEMKEY1 = [{
name: 'uid',
unique: false,
}, {
name: 'age',
unique: false,
}, {
name: 'sex',
unique: false,
}, {
name: 'desc',
unique: false,
}, {
name: 'address',
unique: false,
}]
const personDetail = [{
uid: '102',
age: '18',
sex: '♀',
desc: '女装大佬',
address: ["遥远的地方"],
}, {
uid: '103',
age: '18',
sex: 'man',
desc: 'rua!',
address: '{"test":"123","more":"asd"}',
}, {
uid: '100',
age: 'unknown',
sex: 'unknown',
desc: '666',
address: true,
}, {
uid: '101',
age: 60,
sex: 'man',
desc: '路人甲',
address: true,
}]
const SAVETYPE1 = {
name: 'detail',
type: 'readwrite',
}