如何把indexedDB简单封装成"localStorage"

2,266 阅读4分钟

前言

我们大部分时候想享受到indexDB的优点,比如能保存二进制文件,大量数据等,但又不想要了解indexedDB的功能,比如表、游标、事务。那其实可以使用localforage这个库,它把indexedDB封装成了类似"localStorage"的样子,让indexedDB的使用变得极易上手。

但受到好奇心和求知欲望的驱使,不少人还是想简单地了解下indexedDB相关机制。因此本篇文章通过把indexedDB封装成"localStorage",边实践,边学习indexedDB。

目的是学习indexedDB,所以代码实现不会考虑得很周全,想进一步深入学习可以翻翻localforage的源码。

步骤

设计思路

  1. 首先设计getIndexedDBManager函数,返回值为具备setItem、getItem等接口的对象。
  2. 获取数据库的函数叫getDataBase,它会尝试打开或者创建indexDB数据库。并且缓存此数据库引用。
  3. 每次使用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()

测试结果:

另外提醒下,可以在此开发者工具里手动删除数据库

参考资料

  1. IndexedDB API - Web APIs | MDN
  2. 新一代的前端存储方案--indexedDB - 掘金
  3. localForage官网