Web 客户端数据库 IndexedDB 速览及应用

#1 概述

IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象,如 blobs)。该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。本页面 MDN IndexedDB 的主要引导页 - 这里,我们提供了完整的 API 参考和使用指南,浏览器支持细节,以及关键概念的一些解释的链接。

截至2023年9月,主流浏览器对 IndexedDB 的支持情况如下(数据来源:caniuse.com):
Web 客户端数据库 IndexedDB 速览及应用_第1张图片
如果您的应用需要支持 IE 浏览器,抱歉,用不了 IndexedDB 。

#1.1 特性

  • 在 Web Worker 中可用
  • 容量足够大
  • 支持事物及索引

#1.2 前端存储方案对比

方式 数据生命周期 容量大小 与后端通信
cookie 通常由服务端创建(配置 Response 的 Header),前端也支持手动录入,可设置存活期限 4K 参与
localStorage 创建后一直存在(除非被清理) 5M 不参与
sessionStorage 仅限当前标签页,关闭后丢失(页面刷新可存活) 5M 不参与
IndexedDB 同 localStorage 理论上不受限 不参与

IndexedDB 容量上限其实受浏览器、操作系统、磁盘空间等因素限制,通常认为 250 M,毕竟一个网页需要处理到过大的数据,怎么看都有点流氓

#2 如何使用

#2.1 开源库

目前市面上已经有不少优秀针对 IndexedDB 的库,这里重点介绍localForage、Dexie.js、JsStore

localForage:一个快速且简单的前端存储库,提供极简API(类 localStorage),在不支持 IndexedDB 或 WebSQL 的环境下自动使用 localStorage ,业务场景简单下的首选。

Dexie.jsJsStore都是仅针对 IndexedDB 的封装,并提供类 SQL 的接口。

#2.2 简单封装

在不希望引入新库的情况下,可以试试自己封装

/**
 * 封装 indexedDB,默认的数据库为 db
 */
export default class IDB {
    /**
     * @type {IDBDatabase}
     */
    db          = undefined
    /**
     * @type {Object}
     */
    table       = undefined
    /**
     * @type {String}
     */
    name        = window.DBName || "db"

    /**
     * @class IDB
     * @param {String|Object} nameOrObj - 数据表名或者配置对象
     * @param {String} dbName - 数据库名,默认使用全局属性 DBName
     */
    constructor(nameOrObj, dbName){
        this.table = typeof(nameOrObj) === 'string'?{name: nameOrObj, options:{keyPath:"id"}} : nameOrObj
        if(dbName)  this.name = dbName
    }

    /**
     * 初始化数据库连接
     * @returns {Promise}
     */
    #init = ()=> new Promise((ok, fail)=>{
        if(!!this.db) return ok(this.db)

        const req = indexedDB.open(this.name)
        req.onsuccess = e=> {
            this.db = e.target.result
            ok(this.db)
        }
        req.onerror = e=>fail(e)
        req.onupgradeneeded = e=>{
            this.db = e.target.result

            let { name, options } = this.table
            if(!this.db.objectStoreNames.contains(name)){
                this.db.createObjectStore(name, options)
            }
        }
    })

    /**
     * 单条或批量插入数据行,返回 {count: 处理数据量, used:耗时(单位ms)}
     * @param {Object|Array} rows - 待插入的数据对象或者数组
     * @returns {Promise}
     */
    insert = rows => new Promise((ok, fail)=>this.#init().then(async db=>{
        let { name } = this.table
        let store = db.transaction(name, 'readwrite').objectStore(name)
        rows = Array.isArray(rows)? rows : [rows]

        let started = Date.now()
        let count = 0
        for (const row of rows) {
            try{
                await store.put(row)
                count ++
            }catch(e){
                return fail(e)
            }
        }
        ok({ count, used: Date.now()-started })
    }))

    /**
     * 按主键读取数据行
     * @param {String} key - 数据行主键
     * @returns {Promise}
     */
    get = key => new Promise(async(ok, fail)=> this.#init().then(db=>{
        let { name } = this.table
        const req = db.transaction(name, 'readonly').objectStore(name).get(key)
        req.onsuccess = e=> ok(req.result)
        req.onerror = ({target})=>{
            fail(target.error)
        }
    }))

    /**
     *
     * @param {String} key - 数据行主键
     * @returns {Promise}
     */
    remove = key => new Promise((ok, fail)=> this.#init().then(db=>{
        let { name } = this.table

        const req = db.transaction(name, 'readwrite').objectStore(name).delete(key)
        req.onsuccess = e=> ok()
        req.onerror = ({target})=>{
            fail(target.error)
        }
    }))

    /**
     * 按主键更新数据行(可新增字段)
     * @param {String} key - 数据行主键
     * @param {Object} data - 待更新的字段
     * @returns {Promise}
     */
    update = (key, data)=> new Promise((ok, fail)=> this.#init().then(db=>{
        this.get(key).then(row=>{
            if(row==undefined)  fail(`KEY=${key}的数据对象不存在`)

            let { name } = this.table

            const req = db.transaction(name, 'readwrite').objectStore(name).put(Object.assign(row, data))
            req.onsuccess = e=> ok(req.result)
            req.onerror = ({target})=>{
                fail(target.error)
            }
        })
    }))

    /**
     * 游标方式遍历数据表,返回 {count:处理数据量, used:耗时(单位ms)}
     * @param {Function} worker - 处理函数
     * @param {IDBKeyRange} range - 查询条件,详见 https://developer.mozilla.org/en-US/docs/Web/API/IDBKeyRange
     * @returns {Promise}
     */
    stream = (worker, range)=> new Promise((ok, fail)=> this.#init().then(db=>{
        let { name } = this.table
        let count = 0
        let started = Date.now()
        const req = db.transaction(name, 'readonly').objectStore(name).openCursor(range)
        req.onsuccess = e=> {
            let cursor = e.target.result
            if(cursor){
                worker(cursor.value)
                cursor.continue()
                count ++
            }
            else
                ok({count, used: Date.now()-started})
        }
        req.onerror = ({target})=>{
            fail(target.error)
        }
    }))

    close = ()=> {
        if(!!this.db) {
            this.db.close()
            this.db = null
        }
    }
}

#2.3 使用说明

// 引入(上述类保存到 idb.js )
import IDB from "./idb.js"

// 创建名为 test 的表
const db - new IDB("user")
// 插入两条数据,注意:主键属性(默认 id)为必填项,重复主键时会覆盖旧数据
db.insert([
	{ id:1, name:"集成显卡", vip:1 },
	{ id:2, name:"张三", vip:0 }
])
// 查询ID=1的数据
db.get(1).then(row=>console.debug(row))
// 遍历数据
db.stream(row=>console.debug(row)).then(result=>console.debug(`遍历完成`, result))
// 关闭连接
db.close()

你可能感兴趣的:(前端,前端,数据库)