#1 概述
IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象,如 blobs )。该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。本页面 MDN IndexedDB 的主要引导页 - 这里,我们提供了完整的 API 参考和使用指南,浏览器支持细节,以及关键概念的一些解释的链接。
截至2023年9月,主流浏览器对 IndexedDB 的支持情况如下(数据来源:caniuse.com):
如果您的应用需要支持 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.js 和JsStore都是仅针对 IndexedDB 的封装,并提供类 SQL 的接口。
#2.2 简单封装
在不希望引入新库的情况下,可以试试自己封装
javascript
/**
* 封装 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 使用说明
javascript
// 引入(上述类保存到 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()