这里不会介绍基本概念,请前往MDN或阮一峰老师的IndexedDB自行学习
version的正确用法
关于open
var request = window.indexedDB.open(name, version);
第一个参数很简单,就是name。
第二个参数version是有说法的。version决定了数据库的结构,比如有哪些store,每个store是什么样的。
The version of the database determines the database schema — the object stores in the database and their structure.
- 如果数据库不存在,立即就创建数据库,同时触发onupgradeneeded事件
- 如果数据库存在
- version不变,不会触发onupgradeneeded事件
- version升级了,会触发onupgradeneeded事件
错误的结论
根据这些信息,很容易让我得出了❎错误的结论:
- version:1,触发onupgradeneeded事件,在事件中创建名为member的store;
- version:2,触发onupgradeneeded事件,在事件中创建名为spend的store;
- 依次类推,等等。
根据实践发现,这个结论在特定条件下才能成功执行,所以是个错误的结论,也就是说是个错误的做法。
// 错误案例 执行下来会发现createSpendStore()永远得不到执行
var req1 = window.indexedDB.open(name, 1)
req1.onupgradeneeded = function(){createMemberStore()}
var req2 = window.indexedDB.open(name, 2)
req2.onupgradeneeded = function(){createSpendStroe()}
原因就是open方法不会立即打开数据库或开始执行事务,而是返回一个event,如果我们顺序执行两次open,其实最终得到的是version:2的event,所以只会触发一次onupgradeneeded事件,也就是req1.onupgradeneeded得到了执行。
The open request doesn't open the database or start the transaction right away. The call to the open() function returns an IDBOpenDBRequest object with a result (success) or error value that you handle as an event.
至于特定条件下才能成功指的是,如果我们等req1全部执行完毕后,再执行req2的话,是可行的。如何得知req1完全执行完毕呢?这个就不得而知了。还是强行来一个案例吧
// 第一次刷新页面只执行req1
var req1 = window.indexedDB.open(name, 1)
req1.onupgradeneeded = function(){createMemberStore()}
// var req2 = window.indexedDB.open(name, 2)
// req2.onupgradeneeded = function(){createSpendStroe()}
// 第二次刷新页面只执行req2
// var req1 = window.indexedDB.open(name, 1)
// req1.onupgradeneeded = function(){createMemberStore()}
var req2 = window.indexedDB.open(name, 2)
req2.onupgradeneeded = function(){createSpendStroe()}
// 按照如上步骤可以保证正确执行,但这太苛刻了,不可取。
正确的结论
✅正确的做法,我们应该在onupgradeneeded事件中一次性创建所有的store,version的upgrade是为了改变更新这些store。(其实想想mysql也是先建表格,再做操作)
// 正确案例
var req1 = window.indexedDB.open(name, 1)
req1.onupgradeneeded = function(){
createMemberStore()
createSpendStore()
}
安全的升级version
升级version几条限制
- 如果存在了store,再次创建,抛错
- 如果不存在store,删除store,抛错
- 如果删除store,数据全部丢失,如果想保存数据,需要在version update之前__读取__需要的数据,自行存储,待createStore之后再次写入数据。
- 这里隐藏了一个暗坑,读取必须在version update之前或者在version update之中读取oldVersion的db的数据(不能在onupgradeneeded中读取当前db的数据,这将存在version update的transition和读取的transition的冲突,并且当前db也没有数据啊😹😹😹😹😹😹)
- 升级version后,清除站点数据,以免新的version版本不生效
总结
在踩了上面那些坑之后,实现一个方便好用的IDB class(使用了idb package,解决回调地狱),如下:
// db.js
import { openDB } from 'idb';
export default class IDB {
constructor({ name, version } = {}) {
this.db = null;
this.cacheData = null;
this.name = name;
this.version = version;
this.stores = [];
}
// 先存储,待创建完成后,setItems
transfer = async ({ db, storeName } = {}) => {
// 如果存在,先读取数据,内存缓存,待创建完成后,恢复数据
this.cacheData = await this.getItems({ storeName, db });
db.deleteObjectStore(storeName);
}
// 从transfer中恢复数据
recovery = async ({ db, storeName } = {}) => {
await this.setItems({ storeName, values: this.cacheData, db });
}
addStore = ({ storeName, keepData, updated, createStore } = {}) => {
this.stores.push({ storeName, keepData, updated, createStore });
}
// 判断store是否存在
existStore = ({ db, storeName } = {}) => {
db = db || this.db
if (db) {
return db.objectStoreNames.contains(storeName)
}
return false;
}
// 创建表格
createDatabase = async () => {
this.db = await openDB(this.name, this.version, {
upgrade: async (db, oldVersion, newVersion, transaction) => {
await Promise.all(this.stores.map(async obj => {
const { storeName, keepData, updated, createStore } = obj;
if (this.existStore({ db, storeName })) {
// 如果更新版本需要updated,则更新之,否则do nothing
if (updated) {
if (keepData) { // 如果保留数据,转移数据,创建store,回复数据
// 这里一定要用oldVersion打开oldDB,来读取之前的数据
const oldDb = await openDB(this.name, oldVersion)
await this.transfer({ db: oldDb, storeName });
createStore({ db });
await this.recovery({ db, storeName });
} else { // 如果不保存数据,直接删除
db.deleteObjectStore(storeName);
createStore({ db });
}
}
} else {
createStore({ db })
}
}));
}
})
}
setItems = async ({ storeName, values, db } = {}) => {
db = db || this.db;
if (db && this.existStore({ db, storeName })) {
if (Object.prototype.toString.call(values) === '[object Object]') {
// 单个增加
db.add(storeName, values)
} else {
// 多个增加
const tx = db.transaction(storeName, 'readwrite');
const arr = values.map(item => {
return tx.store.add(item);
})
arr.push(tx.done)
await Promise.all(arr)
}
}
}
editItems = async ({key, values, db, storeName}={})=>{
db = db || this.db;
if (db && this.existStore({ db, storeName })) {//判断是否存在store
const store = db.transaction(storeName, 'readwrite').objectStore(storeName);
const _data = await store.get(key)
Object.keys(values).map(k=>{
if(_data[k] !== values[k]){
_data[k] = values[k]
}
})
store.put(_data)
}
}
getFromIndex = async({store, key, index, fuzzy}={})=>{
const _index = store.index(index);
// _index.get(key) 默认只能获取第一个元素,如果有多个元素需要使用cursor
// return await _index.get(key);
let cursor = await _index.openCursor();
const _values = [];
while (cursor) {
if (fuzzy) {
if (cursor.value[index].indexOf(key) > -1) {
_values.push(cursor.value);
}
} else {
if (cursor.value[index] === key) {
_values.push(cursor.value);
}
}
cursor = await cursor.continue()
}
return _values
}
// 返回数组的结构
getItems = async ({ storeName, key, db, index, indexName, fuzzy } = {}) => {
db = db || this.db;
if (db && this.existStore({ db, storeName })) {//判断是否存在store
if (key) {
const store = db.transaction(storeName).objectStore(storeName);
if(index){
let _values = []
if(Object.prototype.toString.call(index) === '[object Array]'){
const __values = await Promise.all(index.map(async item=>{
return await this.getFromIndex({ store,key, index: item, fuzzy })
}))
_values = __values.flat()
}else{
_values = await this.getFromIndex({store,key, index, fuzzy})
}
return _values;
}else{
return [await store.get(key)]
}
} else {
return await db.getAllFromIndex(storeName, indexName)
}
}
}
delItems = async ({storeName, key, db}={})=>{
db = db || this.db;
if (db && this.existStore({ db, storeName })) {//判断是否存在store
const store = db.transaction(storeName, 'readwrite').objectStore(storeName);
await store.delete(key);
}
}
getStoreInstance = ({ storeName, indexName } = {}) => {
return {
setItems: async (values) => {
return await this.setItems({ storeName, values })
},
getItems: async (key, index, fuzzy) => {
return await this.getItems({ storeName, key, index, indexName, fuzzy })
},
editItems: async(key, values)=>{
return await this.editItems({key, storeName, values})
},
delItems: async(key)=>{
return await this.delItems({key, storeName})
}
}
}
}
用法
// biz.js
import IDB from 'db'
const idb = new IDB({ version: 23, name: 'ziyi' })
idb.addStore({
storeName: 'member',
keepData: true,
updated: false,
createStore: ({ db }) => {
const store = db.createObjectStore('member', {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('phone', 'phone', { unique: true });
store.createIndex('name', 'name');
store.createIndex('score', 'score');
store.createIndex('hisotry', 'history');
}
});
idb.addStore({
storeName: 'spend',
updated: false,
createStore: ({ db }) => {
const store = db.createObjectStore('spend', {
keyPath: 'id',
autoIncrement: true
})
store.createIndex('phone', 'phone');
store.createIndex('desc', 'desc');
}
});
const member = idb.getStoreInstance({ storeName: 'member', indexName: 'phone' })
const spend = idb.getStoreInstance({ storeName: 'spend', indexName: 'id' })
await idb.createDababase();
await member.getItems();
await member.setItems({phone: xxxx})
awiat member.setItems([{phone: xxxx},{}])