前言
我们大部分时候想享受到indexDB的优点,比如能保存二进制文件,大量数据等,但又不想要了解indexedDB的功能,比如表、游标、事务。那其实可以使用localforage这个库,它把indexedDB封装成了类似"localStorage"的样子,让indexedDB的使用变得极易上手。
但受到好奇心和求知欲望的驱使,不少人还是想简单地了解下indexedDB相关机制。因此本篇文章通过把indexedDB封装成"localStorage",边实践,边学习indexedDB。
目的是学习indexedDB,所以代码实现不会考虑得很周全,想进一步深入学习可以翻翻localforage的源码。
步骤
设计思路
- 首先设计getIndexedDBManager函数,返回值为具备setItem、getItem等接口的对象。
- 获取数据库的函数叫getDataBase,它会尝试打开或者创建indexDB数据库。并且缓存此数据库引用。
- 每次使用setItem、getItem都会先获取数据库
function getIndexedDBManager() {
let dataBase = null // 此处缓存
function getDataBase() {
if (dataBase) {
return dataBase
}
// todo: 打开或者创建indexDB数据库
}
return {
setItem(key, value) {
const dataBase = getDataBase()
},
getItem(key) {
const dataBase = getDataBase()
return
},
}
}
获取数据库
实现getDataBase:
-
indexedDB需要取一下数据库、表的名字,还需要从存储数据中指定一个索引。封装localStorage大概率不会改变这些名称,所以都设置为固定不变的常量。
-
indexedDB的接口大部分都是异步的,采用了回调函数的方式实现,为了方便我们将其封装成promise。
// 固定不变的常量
const DATA_BASE_NAME = 'KeyValuePairDB'
const TABLE_NAME = 'KeyValuePair'
const UNIQ_KEY = 'key'
let dataBase = null // 缓存
function getDataBase() {
if (dataBase) {
return dataBase
}
return new Promise(resolve =>
// 开始打开数据库
// open第一个参数是数据库名字
// 第二个参数是版本号,在更改数据库格式时候才传。
// 版本号改变后会触发onupgradeneeded。
// 如果不存在数据库且没传参,则版本号为默认为1。并触发upgradeneeded事件
const request = indexedDB.open(DATA_BASE_NAME)
// 处理upgradeneeded事件
request.onupgradeneeded = e => {
const db = e.target.result
// 如果不存在store,则创建它,并通过keyPath指定索引。
if (!db.objectStoreNames.contains(TABLE_NAME)) {
db.createObjectStore(TABLE_NAME, { keyPath: UNIQ_KEY })
}
}
// open与upgradeneeded事件处理成功,缓存并且resolve 数据库。
request.onsuccess = e => {
const db = e.target.result
dataBase = db
resolve(db)
}
})
}
实现setItem、getItem
-
因为getDataBase返回promise,所以setItem、getItem也不妨用async-await语法。
-
简单介绍下事务transaction,它是用来保证数据库操作要么全部成功,要么全部失败的一个限制。
- 比如,在修改多条数据时,前面几条已经成功了,在中间的某一条是失败了。那么数据库就重置前面数据的修改,放弃后面的数据修改。一条数据也不修改,直接返回错误。
- 这个机制主要用于处理数据库并发的问题,所以下面实现没有体现这个特点。
async setItem(key, value) {
const dataBase = await getDataBase()
return new Promise(resolve => {
// transaction: 开启一个可读写的事务
// objectStore:再读取数据表
// put: 设置数据
const request = dataBase.transaction(TABLE_NAME, 'readwrite')
.objectStore(TABLE_NAME)
.put({ data: value, [UNIQ_KEY]: key })
// 成功,返回success信号
request.onsuccess = resolve('success')
})
},
async getItem(key) {
const dataBase = await getDataBase()
return new Promise(resolve => {
// transaction: 开启事务,默认为可读
// objectStore:再读取数据表
// get: 获取数据
const request = dataBase.transaction(TABLE_NAME)
.objectStore(TABLE_NAME)
.get(key)
// 成功,返回数据
request.onsuccess = () => {
resolve(request.result?.data)
}
})
},
扩展:实现keys()
- localStorage没有keys方法。但我们可以扩展下keys方法,以此来学习indexedDB的游标cursor。
- 游标cursor,可用于遍历或迭代数据库中的多条记录。
async keys() {
const keys = []
return new Promise(resolve => {
const request = dataBase.transaction(TABLE_NAME)
.objectStore(TABLE_NAME)
.openCursor()
// openCursor可传一参指定范围,默认为全部记录的范围
// 可传二参指定方向。
request.onsuccess = () => {
const cursor = request.result;
if (cursor) {
// continue为向下一个移动,另外也有方法advance向前移动
cursor.continue()
keys.push(cursor.value[UNIQ_KEY])
} else {
resolve(keys)
}
}
})
}
最后
全部代码
function getIndexedDBManager() {
const DATA_BASE_NAME = 'KeyValuePairDB'
const TABLE_NAME = 'KeyValuePair'
const UNIQ_KEY = 'key'
let dataBase = null
function getDataBase() {
if (dataBase) {
return dataBase
}
return new Promise(resolve => {
const request = indexedDB.open(DATA_BASE_NAME)
request.onupgradeneeded = e => {
const db = e.target.result
if (!db.objectStoreNames.contains(TABLE_NAME)) {
db.createObjectStore(TABLE_NAME, { keyPath: UNIQ_KEY })
}
}
request.onsuccess = e => {
const db = e.target.result
dataBase = db
resolve(db)
}
})
}
return {
async setItem(key, value) {
const dataBase = await getDataBase()
return new Promise(resolve => {
const request = dataBase.transaction(TABLE_NAME, 'readwrite')
.objectStore(TABLE_NAME)
.put({ data: value, [UNIQ_KEY]: key })
request.onsuccess = resolve('success')
})
},
async getItem(key) {
const dataBase = await getDataBase()
return new Promise(resolve => {
const request = dataBase.transaction(TABLE_NAME)
.objectStore(TABLE_NAME)
.get(key)
request.onsuccess = () => {
resolve(request.result?.data)
}
})
},
async keys() {
const keys = []
return new Promise(resolve => {
const request = dataBase.transaction(TABLE_NAME)
.objectStore(TABLE_NAME)
.openCursor()
request.onsuccess = () => {
const cursor = request.result;
if (cursor) {
cursor.continue()
keys.push(cursor.value[UNIQ_KEY])
} else {
resolve(keys)
}
}
})
}
}
}
测试
测试代码:
async function run() {
const dbManager = await getIndexedDBManager()
const result1 = await dbManager.getItem('key1')
console.log('getItem key1: ', result1)
const info1 = await dbManager.setItem('key2', 'value2')
console.log('setItem key2: ', info1)
const result2 = await dbManager.getItem('key2')
console.log('getItem key2: ', result2)
await dbManager.setItem('key3', 'value3')
await dbManager.setItem('key4', 'value4')
const keys = await dbManager.keys()
console.log('keys: ', keys)
}
run()
测试结果:
另外提醒下,可以在此开发者工具里手动删除数据库