前端大容量存储方案IndexedDB封装使用(包含版本升级)

255 阅读6分钟

developer.mozilla.org/zh-CN/docs/…

随着浏览器版本的不断迭代,加上日益增长的需求,前端对于浏览器大容量存储的方案需求也越来越迫切。而IndexedDB就是用来解决这一方面使用场景的,我们来看前端现在各个存储方案的容量:

  • Cookie:不超过4KB
  • LocalStorage:根据浏览器内核的不同,存储容量在2.5-10MB
  • Web SQL:已被废弃,不推荐使用 因为直接使用sql语句进行开发
  • IndexedDB:没有上限,一般不少于250M

IndexedDB是新一代的Web存储方案,更接近NoSQL数据库,是一个基于事务操作的key-value型前端数据库。支持异步API。

IndexedDB特性

  • 使用key-value进行存储:它的每一条数据记录都有对应的主键,并且主键是独一无二的
  • 异步API:使用异步操作,在操作大数据时不会锁死浏览器,用户依然可以做其他操作
  • 支持事务:异步失败会导致整个事务会滚到事务发生前的状态
  • 同源限制:不能访问跨源数据库
  • 存储空间大:没有上限,一般不少于250M

IndexedDB的基本概念

版本:IndexedDB有版本的概念,所以对于库表结构的修改,必须得通过升级数据库版本的方式完成

事务:上方有提到过,这样保护了数据,当错误事件发生时数据库数据会回滚

操作IndexedDB

下面我们对indexedDB来进行操作

1. 创建/打开数据库

创建和打开数据库都是使用open方法,如果没有数据库则为创建

onupgradeneeded事件:

触发条件:

  1. 指定的版本号大于当前版本号触发
  2. 新建数据库触发

获取数据库实例的方式:

  • 本地有数据库则在success事件中获取
  • 触发onupgradeneeded则在该事件内获取
const APP_DATABASE_NAME = 'demo'
const APP_DATABASE_VERSION = 1
export class IndexedDB {
    private db!:IDBDatabase
    open() {
        const idbOpenDBRequest = indexedDB.open(APP_DATABASE_NAME,APP_DATABASE_VERSION)
        idbOpenDBRequest.onerror = () => {
            console.log('db open error')
        }
        idbOpenDBRequest.onsuccess = () => {
            console.log('db open success')
            this.db = idbOpenDBRequest.result
        }
        // 1. 指定的版本号大于当前版本号触发
        // 2. 新建数据库也是使用open方法,这时候也会触发该事件
        idbOpenDBRequest.onupgradeneeded = (event: IDBVersionChangeEvent) => {
            console.log('db upgradeneeded')
            var db = event.target?.result // 获取数据库实例
            this.db = db
        }
    }
}

2. 创建表格

对于表格的结构可以想做是对象,有key和value的存在,通过db对象的objectStoreNames可以判断是否存在表格并进行处理。

createObjectStore方法的第二个参数可以传入keyPath,那么将会将keyPath传入的值作为表格对象的key,也可以设置自动设置主键,设置请看下方代码

private createObjectStore() {
        // 创建表格
        if(!this.db) return
        const db = this.db
        let objectStore;
        if(!this.db.objectStoreNames.contains('user')) {
            // 不存在则创建
            objectStore = db.createObjectStore('user', {keyPath: 'id'})
            // 自动设置主键
            // objectStore = db.createObjectStore('user', {autoIncrement:true})
            // 创建索引 搜索索引会更快,建议对多数量数据添加索引
            objectStore.createIndex('name', 'name', {unique: false})
        }
    }
    
  // 调用时机
idbOpenDBRequest.onupgradeneeded = (event: IDBVersionChangeEvent) => {
      console.log('db upgradeneeded')
      var db = event.target?.result // 获取数据库实例
      this.db = db
      this.createObjectStore()
  }

3. 新增数据

对于表格的操作需要新建事务,所以调用transaction方法,第一个参数为操作的表格数组,第二个参数为操作模式,关于操作模式请看下方:

  • readwrite: 读写模式
  • readonly:只读
  • versionchange: 操作结构

通过objectStore方法可以获取具体表格对象,并进行操作

APP_OBJECT_STORE_NAME为上方的表格名,抽成一个常量

add() {
    const request = this.db.transaction([APP_OBJECT_STORE_NAME], 'readwrite')
    .objectStore(APP_OBJECT_STORE_NAME)
    .add({id: 1, name: 'user1', age: 1, sex: 1})

    request.onsuccess = (e) => {
        console.log('add data success', e)
    }
    request.onerror = (e) => {
        console.log('add data error', e)
    }
}

4. 读取数据

通过getAll可以获取数据,也可以通过get,getAllKeys等方法进行获取

read() {
    const request = this.db.transaction([APP_OBJECT_STORE_NAME], 'readwrite')
    .objectStore(APP_OBJECT_STORE_NAME).getAll()

    request.onerror = (e) => {
        console.log('get data error',e)
    }
    request.onsuccess = (e) => {
        if(!request.result) {
            console.log('no data')
            return
        }
        const result = request.result
        console.log('data array ----->', result, e)
    }
    
}

5. 遍历数据

也可以通过forEach的方式进行数据遍历

eachAll() {
    const request = this.db.transaction([APP_OBJECT_STORE_NAME], 'readwrite')
    .objectStore(APP_OBJECT_STORE_NAME).openCursor()

    request.onsuccess = (e) => {
        const cursor = e.target.result
        if(!cursor) return
        console.log(cursor, '<------cursor')
    }
}

6. 更新数据

put() {
    const request = this.db.transaction([APP_OBJECT_STORE_NAME], 'readwrite')
    .objectStore(APP_OBJECT_STORE_NAME)
    .put({id: 1, name: 'user-put1', age: 2, sex: 2})

    request.onsuccess = (e) => {
        console.log('put data success', e)
    }
    request.onerror = (e) => {
        console.log('put data error', e)
    }
}

7. 删除数据

通过创建库表时设置的主键进行删除

delete() {
    const request = this.db.transaction([APP_OBJECT_STORE_NAME], 'readwrite')
    .objectStore(APP_OBJECT_STORE_NAME)
    .delete(1)

    request.onsuccess = (e) => {
        console.log('put data success', e)
    }
    request.onerror = (e) => {
        console.log('put data error', e)
    }
 }

8. 通过索引快捷读取数据

刚才我们所演示的读取数据只能通过主键或者遍历获取,这样子太麻烦了,我想直接通过里面的某个字段进行搜索的话。索引的重要性就凸显出来了。创建索引可以让我们根据索引字段来搜索数据

// 参数依次为索引名称,对应字段名,配置
objectStore.createIndex('name', 'name', {unique: false})
readIndexData() {
    const index = this.db.transaction([APP_OBJECT_STORE_NAME], 'readwrite')
    .objectStore(APP_OBJECT_STORE_NAME).index('name')
    const request = index.get('user1')

    request.onerror = (e) => {
        console.log('get data error',e)
    }
    request.onsuccess = (e) => {
        if(!request.result) {
            console.log('no data')
            return
        }
        const result = request.result
        console.log('data array ----->', result, e)
    }   
}

整个文件

const APP_DATABASE_NAME = 'demo'
const APP_DATABASE_VERSION = 1
const APP_OBJECT_STORE_NAME = 'user'
export class IndexedDB {
    private db!:IDBDatabase
    private objectStore!: IDBObjectStore

    constructor() {
        this.open()
    }
    private open() {
        const idbOpenDBRequest = indexedDB.open(APP_DATABASE_NAME,APP_DATABASE_VERSION)
        idbOpenDBRequest.onerror = () => {
            console.log('db open error')
        }
        idbOpenDBRequest.onsuccess = () => {
            console.log('db open success', idbOpenDBRequest.result)
            this.db = idbOpenDBRequest.result
        }
        // 1. 指定的版本号大于当前版本号触发
        // 2. 新建数据库也是使用open方法,这时候也会触发该事件
        idbOpenDBRequest.onupgradeneeded = (event: IDBVersionChangeEvent) => {
            console.log('db upgradeneeded')
            var db = event.target?.result // 获取数据库实例
            this.db = db
            this.createObjectStore()
        }
    }
    private createObjectStore() {
        // 创建表格
        if(!this.db) return
        const db = this.db
        let objectStore;
        if(!this.db.objectStoreNames.contains(APP_OBJECT_STORE_NAME)) {
            // 不存在则创建
            objectStore = db.createObjectStore(APP_OBJECT_STORE_NAME, {keyPath: 'id'})
            // 自动设置主键
            // objectStore = db.createObjectStore('user', {autoIncrement:true})
            // 创建索引 搜索索引会更快,建议对多数量数据添加索引
            objectStore.createIndex('name', 'name', {unique: false})
        }
    }

    add() {
        const request = this.db.transaction([APP_OBJECT_STORE_NAME], 'readwrite')
        .objectStore(APP_OBJECT_STORE_NAME)
        .add({id: 1, name: 'user1', age: 1, sex: 1})

        request.onsuccess = (e) => {
            console.log('add data success', e)
        }
        request.onerror = (e) => {
            console.log('add data error', e)
        }
    }
    read() {
        const request = this.db.transaction([APP_OBJECT_STORE_NAME], 'readwrite')
        .objectStore(APP_OBJECT_STORE_NAME).getAll()

        request.onerror = (e) => {
            console.log('get data error',e)
        }
        request.onsuccess = (e) => {
            if(!request.result) {
                console.log('no data')
                return
            }
            const result = request.result
            console.log('data array ----->', result, e)
        }   
    }
    eachAll() {
        const request = this.db.transaction([APP_OBJECT_STORE_NAME], 'readwrite')
        .objectStore(APP_OBJECT_STORE_NAME).openCursor()

        request.onsuccess = (e) => {
            const cursor = e.target.result
            if(!cursor) return
            console.log(cursor, '<------cursor')
        }
    }
    put() {
        const request = this.db.transaction([APP_OBJECT_STORE_NAME], 'readwrite')
        .objectStore(APP_OBJECT_STORE_NAME)
        .put({id: 1, name: 'user-put1', age: 2, sex: 2})

        request.onsuccess = (e) => {
            console.log('put data success', e)
        }
        request.onerror = (e) => {
            console.log('put data error', e)
        }
    }
    delete() {
        const request = this.db.transaction([APP_OBJECT_STORE_NAME], 'readwrite')
        .objectStore(APP_OBJECT_STORE_NAME)
        .delete(1)

        request.onsuccess = (e) => {
            console.log('put data success', e)
        }
        request.onerror = (e) => {
            console.log('put data error', e)
        }
    }
    readIndexData() {
        const index = this.db.transaction([APP_OBJECT_STORE_NAME], 'readwrite')
        .objectStore(APP_OBJECT_STORE_NAME).index('name')
        const request = index.get('user1')

        request.onerror = (e) => {
            console.log('get data error',e)
        }
        request.onsuccess = (e) => {
            if(!request.result) {
                console.log('no data')
                return
            }
            const result = request.result
            console.log('data array ----->', result, e)
        }   
    }
}

总结

indexedDB是前端大数据存储的一个解决方案,在有多数据量或者大文件(web3d模型等)的使用场景下效果显著

推荐Dexie食用

我们在上文中也看到了,indexedDB的大部分API都是使用回调的方式来进行编写。这并不符合我们Promise回调的开发方式。有一款Dexie的插件就封装了indexedDB,使其更加便携好用。教程文章也在笔者栏目下,推荐观看