揭秘 IndexedDB:如何构建一个高效且易用的数据存储库

27 阅读15分钟

在当今的网页应用开发中,高效、安全地管理和存储大量数据变得尤为重要。随着用户对响应速度、数据持久化和隐私保护的要求提升,开发者需要找到一个既能支持高效存储大规模数据,又具备易操作性的解决方案。IndexedDB 作为浏览器内置的低级API,正好满足了这些需求。IndexedDB 能够高效地管理和存取大批量数据,避免了性能瓶颈。

然而,由于 IndexedDB 的 API 比较复杂,许多开发者在初次使用时可能会感到困惑。如何掌握基本操作,避免回调地狱,并封装出易用的存储库?这些问题可能让开发者望而却步。

本文将带你一步步理解 IndexedDB,从基础到进阶,帮助你快速构建高效、易用的数据存储类库,使得前端数据管理,特别是大数据量存储和处理,变得更加轻松。

我们将从以下几个方面进行探讨:

  1. IndexedDB 介绍:了解 IndexedDB 的工作原理及其优势。
  2. 构建数据存储类库:逐步引导您创建一个简单的 IndexedDB 封装库。
  3. Vue中使用示例:展示如何在实际 Vue 项目中利用这个类库进行数据操作。

1 IndexedDB 介绍

1.1 概述

随着浏览器的功能不断增强,越来越多的网站开始考虑,将大量数据储存在客户端,这样可以减少从服务器获取数据,直接从本地获取数据。

现有的浏览器数据储存方案,都不适合储存大量数据:Cookie 的大小不超过 4KB,且每次请求都会发送回服务器;LocalStorage 在 2.5MB 到 10MB 之间(各家浏览器不同),而且不提供搜索功能,不能建立自定义的索引。所以,需要一种新的解决方案,这就是 IndexedDB 诞生的背景。

通俗地说,IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。

IndexedDB 具有以下特点。

(1)键值对储存。 IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以“键值对”的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。

(2)异步。 IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。

(3)支持事务。 IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。

(4)同源限制。 IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。

(5)储存空间大。 IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。

(6)支持二进制储存。 IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。

1.2 基本概念

IndexedDB 是一个比较复杂的 API,涉及不少概念。它把不同的实体,抽象成一个个对象接口。学习这个 API,就是学习它的各种对象接口。下面是对相关概念的简单介绍:

  1. 数据库:IDBDatabase 对象

    数据库是一系列相关数据的容器。每个域名(严格的说,是协议 + 域名 + 端口)都可以新建任意多个数据库。

    IndexedDB 数据库有版本的概念。同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除表、索引或者主键),只能通过升级数据 库版本完成。

  2. 对象仓库:IDBObjectStore 对象

    每个数据库包含若干个对象仓库(object store)。它类似于关系型数据库的表格。

    IDBDatabase 对象的 transaction() 返回一个事务对象,事务对象的 objectStore() 方法返回 IDBObjectStore 对象,可通过对象仓库对数据记录进行增删改查,如 IDBObjectStore.add() 可以给指定对象仓库新增一条数据记录。这里先有个印象即可。

  3. 数据记录

    对象仓库保存的是数据记录。每条记录类似于关系型数据库的行,但是只有主键和数据体两部分。主键用来建立默认的索引,必须是不同的,否则会报错。主键可以是数据记录里面的一个属性,也可以指定为一个递增的整数编号。数据体可以是任意数据类型,不限于对象。

  4. 索引: IDBIndex 对象

    IDBIndex 对象是 IndexedDB 中非常强大的工具,使得对数据的检索和操作更加高效和灵活。通过创建索引,开发者可以根据特定的字段快速找到所需的对象,而无需遍历整个对象仓库(IDBObjectStore),从而优化应用的性能。

    对象仓库 IDBObjectStore.index(name) 返回指定名称的索引对象 IDBIndex

  5. 事务: IDBTransaction 对象

    数据记录的读写和删改,都要通过事务完成。事务对象提供errorabortcomplete三个事件,用来监听操作结果。

    IDBDatabase.transaction()方法返回的就是一个 IDBTransaction 对象。

  6. 操作请求:IDBRequest 对象

    IDBRequest 对象表示打开的数据库连接,indexedDB.open()方法和indexedDB.deleteDatabase()方法会返回这个对象。数据库的操作都是通过这个对象完成的。

  7. 游标: IDBCursor 对象

    IDBCursor 对象代表游标对象,用来遍历数据仓库(IDBObjectStore)或索引(IDBIndex)的记录。

    IDBCursor 对象一般通过IDBObjectStore.openCursor()IDBIndex.openCursor() 方法获得。

  8. 主键集合:IDBKeyRange 对象

    简单来说,IDBKeyRange 对象的作用就是在 IDBObjectStore 对象仓库IDBIndex 索引对象 在查询数据时,限制其查询范围,以达到更加精确的查询效果,并提高查询效率。具体查询范围的方法有如下四个:

    • IDBKeyRange.lowerBound():指定下限。

      IDBKeyRange.lowerBound(y); // 查询大于等于 y 的记录
       IDBKeyRange.lowerBound(y, true); // 查询大于 y 的记录
      
    • IDBKeyRange.upperBound():指定上限。

      IDBKeyRange.upperBound(y); // 查询小于等于 y 的记录
      IDBKeyRange.upperBound(y, true); // 查询小于 y 的记录
      
    • IDBKeyRange.bound():同时指定上下限。

      IDBKeyRange.bound(x, y); // 查询大于等于 x 且小于等于 y 的记录
      IDBKeyRange.bound(x, y, true, true); // 查询大于 x 且小于 y 的记录
      
    • IDBKeyRange.only():指定只包含一个值。

      IDBKeyRange.only(y); // 查询等于 y 的记录
      IDBKeyRange.only(null); // 查询所有记录
      

    IDBObjectStore 对象仓库 结合IDBKeyRange查询示例:

    const request = window.indexedDB.open(databaseName, version);
    let db;
    request.onsuccess = function (event) {
      db = request.result;
      // 打开名为 test 的仓库对象
      const objectStore = db.transaction('test', 'readwrite').objectStore('test')
       // 指定查询主键值为1-10的数据记录
      const keyRange = IDBKeyRange.bound(1, 10)
      const request = objectStore.getAll(keyRange)
      // 通过 onsuccess 事件返回结果
      request.onsuccess = function (event) {
          console.log(event.target.result);
      }
    };
    

    IDBIndex 索引对象 结合IDBKeyRange查询示例:

    const request = window.indexedDB.open(databaseName, version);
    let db;
    request.onsuccess = function (event) {
        db = request.result;
        let list = []
        // 打开名为 test 的仓库对象
        const objectStore = db.transaction('test', 'readwrite').objectStore('test')
        // 假定索引为年龄age, 指定查询年龄为10-20岁的数据记录
        const keyRange = IDBKeyRange.bound(10, 20)
        const request = objectStore
        .index('age') // 通过 age 获取索引对象
        .openCursor(keyRange) // 指针对象
    ​
        request.onsuccess = function (event) {
            const cursor = event.target.result
            if (cursor) {
                // 这里可对遍历的数据进行相关操作
                list.push(cursor.value)
                // continue() 方法将游标移到下一个数据对象,如果当前数据对象已经是最后一个数据了,则光标指向null
                cursor.continue()
            } else {
                console.log(list);
            }
        }
    };
    

1.3 操作流程

下面介绍下 IndexedDB 数据库的操作流程,熟悉后可进一步封装成类库来使用,类库的封装在下一节介绍。

1.3.1 打开或创建数据库

使用 indexedDB.open() 方法打开一个数据库。如果指定的数据库不存在,它将创建一个新的数据库。

const request = indexedDB.open('myDatabase', 1); // 'myDatabase' 是数据库名,1 是版本号
​
request.onsuccess = (event) => {
    const db = event.target.result;
    console.log('数据库打开成功', db);
};
​
request.onerror = (event) => {
    console.error('数据库打开失败', event);
};

indexedDB.open() 这个方法接受两个参数,第一个参数是字符串,表示数据库的名字。如果指定的数据库不存在,就会新建数据库。第二个参数是整数,表示数据库的版本。如果省略,打开已有数据库时,默认为当前版本;新建数据库时,默认为1

indexedDB.open()方法返回一个 IDBRequest 对象。这个对象通过三种事件errorsuccessupgradeneeded,处理打开数据库的操作结果。

和 localStorage 类似,IndexedDB 新建完成后可在浏览器开发者工具中的 Application 左侧的 IndexedDB 中查看详细内容。

1.3.2 创建对象仓库(IDBObjectStore)

在首次打开数据库或版本升级时,会触发 onupgradeneeded 事件,此时可以创建对象仓库。对象仓库类似于数据库中的表。

request.onupgradeneeded = (event) => {
    const db = event.target.result;
    // 先判断一下,对象仓库是否存在,如果不存在再新建。
    if (!db.objectStoreNames.contains(this.storeName)) {
        // 'myObjectStore' 是对象仓库名,keyPath 指定主键名,主键自增设置 autoIncrement: true
        const objectStore = db.createObjectStore('myObjectStore', { keyPath: 'id'autoIncrement: true});    
        
        objectStore.createIndex('name', 'name', { unique: false }); // 创建索引
    }
};

1.3.3 新增数据

使用 add() 方法将数据添加到对象仓库中。

 // 'myObjectStore' 是对象仓库名,readwrite 表示可读写,默认readonly只读
 // transaction: 在对对象仓库进行读取或写入操作时,通常会使用事务。事务确保操作的原子性。
const transaction = db.transaction('myObjectStore', 'readwrite');
const objectStore = transaction.objectStore('myObjectStore');
​
const requestAdd = objectStore.add({ id: 1, name: 'Alice' });
​
requestAdd.onsuccess = () => {
    console.log('数据添加成功');
};
​
requestAdd.onerror = (event) => {
    console.error('数据添加失败', event);
};

1.3.4 读取数据

可以通过 get() 方法读取数据。

...
const requestGet = objectStore.get(1); // 通过主键获取数据
​
requestGet.onsuccess = (event) => {
    const data = event.target.result;
    console.log('读取到的数据:', data);
};
​
requestGet.onerror = (event) => {
    console.error('读取数据失败', event);
};

1.3.5 更新数据

更新数据可以使用 put() 方法。

...
const requestUpdate = objectStore.put({ id: 1, name: 'Bob' }); // 更新 id 为 1 的数据
​
requestUpdate.onsuccess = () => {
    console.log('数据更新成功');
};
​
requestUpdate.onerror = (event) => {
    console.error('数据更新失败', event);
};

1.3.6 删除数据

使用 delete() 方法删除数据。

...
const requestDelete = objectStore.delete(1); // 删除 id 为 1 的数据
​
requestDelete.onsuccess = () => {
    console.log('数据删除成功');
};
​
requestDelete.onerror = (event) => {
    console.error('数据删除失败', event);
};

1.3.7 遍历数据

遍历对象仓库的所有记录,要使用游标对象 IDBCursor。

...
  const request = objectStore.openCursor()
  request.onsuccess = function (event) {
     var cursor = event.target.result;
​
     if (cursor) {
       console.log('id: ' + cursor.key); // 返回当前记录的主键
       console.log('value: ' + cursor.value); // 返回当前记录的数据值
       cursor.continue(); // continue()方法将游标移到下一个数据对象,如果当前数据对象已经是最后一个数据了,则游标指向null
    } else {
      console.log('没有更多数据了!');
    }
  };

1.3.8 使用索引

索引的意义在于,可以让你搜索任意字段,也就是说从任意字段拿到数据记录。如果不建立索引,默认只能搜索主键(即从主键取值)。

假定新建对象仓库的时候,对name字段建立了索引。现在,就可以从name找到对应的数据记录了。

...
const indexObj = objectStore.index('name'); // 通过 name 获取索引对象
const request = indexObj.get('李四'); // 查询name为李四的数据记录
​
request.onsuccess = function (e) {
  var result = e.target.result;
  if (result) {
    // ...
  } else {
    // ...
  }
}

1.3.9 关闭数据库

操作完成后,可以选择关闭数据库连接,会等所有事务完成后再关闭。

db.close();

2 构建数据存储类库

2.1 初始化

初始化数据库、对象仓库(表)、版本号,方便后续的操作。在首次打开数据库或版本升级(版本升级将 version 加1即可)时,会触发 onupgradeneeded 事件,此时可以创建对象仓库及索引。对象仓库类似于数据库中的表。

// 记录每个对象仓库的索引
const indexList = {
    test: [
        {
            field: 'datetime', // 查询时间:number
            unique: false,
        },
        {
            field: 'name', // 姓名:string
            unique: false,
        },
        {
            field: 'institution', // 机构:array
            unique: false,
        },
    ],
    test2: [
        {
            field: 'userId', // 用户id:array
            unique: false,
        },
    ]
}
​
export default class IndexedDB {
    /**
     * 构造函数
     * @param {string} dbName 数据库名
     * @param {string} storeName 对象仓库名
     * @param {number} version 版本号
     */
    constructor(dbName, storeName, version = 1) {
        this.dbName = dbName
        this.storeName = storeName
        this.version = version
        this.db = null
    }
    
    /**
     * 打开或创建数据仓库
     * @returns 
     */
    openDB() {
        return new Promise((resolve, reject) => {
            // request 对象表示打开的数据库连接,indexedDB.open()方法和indexedDB.deleteDatabase()方法会返回这个对象。数据库的操作都是通过这个对象完成的。
            const request = indexedDB.open(this.dbName, this.version)
​
            request.onsuccess = (event) => {
                this.db = event.target.result
                console.log('Database opened successfully:')
                resolve(this.db)
            }
​
            request.onerror = (event) => {
                console.error('Error opening database:', event.target.error)
                reject(event.target.error)
            }
            // 在首次打开数据库或版本升级时,会触发onupgradeneeded事件
            request.onupgradeneeded = (event) => {
                const db = event.target.result
                console.log('Database upgrade needed')
​
                // 这里可以创建对象存储和索引
                if (!db.objectStoreNames.contains(this.storeName)) {
                    const objectStore = db.createObjectStore(this.storeName, {
                        keyPath: 'id', // 这是主键
                        autoIncrement: true, // 实现自增
                    })
                    indexList[this.storeName].forEach((item) => {
                        objectStore.createIndex(item.field, item.field, { unique: item.unique })
                    })
                } else {
                    // 如果对象存储已存在,首先获取现有的对象存储
                    const objectStore = event.target.transaction.objectStore(this.storeName)
​
                    // 删除现有索引(如果需要)
                    for (const indexName of objectStore.indexNames) {
                        objectStore.deleteIndex(indexName)
                    }
​
                    // 重新创建索引
                    indexList[this.storeName].forEach((item) => {
                        objectStore.createIndex(item.field, item.field, { unique: item.unique })
                    })
                }
            }
        })
    }
​
}

2.2 新增数据

    /**
     * 新增数据
     * @param {string} storeName 仓库名称
     * @param {string} data 数据
     * @returns promise对象,fulfilled状态时的结果为主键值,rejected状态时结果为错误信息
     */
    addData(storeName, data) {
        return new Promise((resolve, reject) => {
            // 获取事务对象,所有读写操作都要通过此对象进行
            const transaction = this.db.transaction([storeName], 'readwrite')
            // 获取指定对象仓库
            const objectStore = transaction.objectStore(storeName)
            // IDBRequest 对象可以监听该操作是否成功
            const request = objectStore.add(data)
​
            request.onsuccess = () => {
                console.log('Data added successfully')
                resolve(request.result)
            }
​
            request.onerror = (event) => {
                console.error('Error adding data:', event.target.error)
                reject(event.target.error)
            }
        })
    }

2.3 读取数据

    /**
     * 通过主键读取数据
     * @param {string} storeName 仓库名称
     * @param {string} key 主键值
     * @returns promise对象,fulfilled状态时的结果为数据记录,rejected状态时结果为错误信息
     */
    getData(storeName, key) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction([storeName])
            const objectStore = transaction.objectStore(storeName)
            const request = objectStore.get(key)
​
            request.onsuccess = () => {
                console.log('Data retrieved successfully')
                resolve(request.result)
            }
​
            request.onerror = (event) => {
                console.error('Error retrieving data:', event.target.error)
                reject(event.target.error)
            }
        })
    }

2.4 更新数据

    /**
     * 通过主键更新数据
     * @param {string} storeName 仓库名称
     * @param {object} data 要更新的数据
     */
    updateData(storeName, data) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction([storeName], 'readwrite')
            const objectStore = transaction.objectStore(storeName)
            const request = objectStore.put(data) // 使用put方法更新数据
​
            request.onsuccess = () => {
                console.log('Data updated successfully')
                resolve(request.result)
            }
​
            request.onerror = (event) => {
                console.error('Error updating data:', event.target.error)
                reject(event.target.error)
            }
        })
    }

2.5 删除数据

    /**
     * 通过主键删除数据
     * @param {string} storeName 仓库名称
     * @param {number} key 主键值
     */
    deleteData(storeName, key) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction([storeName], 'readwrite')
            const objectStore = transaction.objectStore(storeName)
            const request = objectStore.delete(key)
​
            request.onsuccess = () => {
                console.log('Data deleted successfully')
                resolve()
            }
​
            request.onerror = (event) => {
                console.error('Error deleting data:', event.target.error)
                reject(event.target.error)
            }
        })
    }

2.6 通过索引读取数据

    /**
     * 通过索引读取数据,会一次性读取出该索引值的所有记录。如果只查单条,将getAll替换成get即可
     * @param {string} storeName 仓库名称
     * @param {string} indexName 索引名称
     * @param {string} indexValue 索引值
     */
    getDataByIndex(storeName, indexName, indexValue) {
        return new Promise((resolve, reject) => {
            var store = this.db.transaction(storeName, 'readwrite').objectStore(storeName)
            var request = store.index(indexName).getAll(indexValue)
​
            request.onsuccess = function () {
                console.log('Data retrieved successfully', request.result)
                resolve(request.result)
            }
            request.onerror = function (event) {
                console.error('Error retrieving data:', event.target.error)
                reject(event.target.error)
            }
        })
    }

2.7 多条件分页查询(重点)

实际项目中,往往不是单一的查询条件,而是多条件查询,且还要进行分页排序等操作,这里结合我实际项目中遇到的问题进行探讨,前提条件是首先要建立索引(参考2.1节中创建索引的方法)。思路是先通过时间索引查询(因为一般起始时间查询后的条数不会特别多,这样后续的遍历次数会大大减少),代码及注释如下:

    /**
     * 多条件检索
     * @param {string} storeName 仓库名称
     * @param {object} queryObj 查询条件, 格式: {  name:"", // 姓名
                                                institution:[], // 机构
                                                "startDatetime":"2024-09-23", // 开始时间
                                                "endDatetime":"2024-09-23", // 结束时间
                                                "page":1,  // 查询页码
                                                "rows":20, // 查询条数
                                                "field":"datetime", // 排序字段
                                                "sort":"desc" // 升降序
                                              } 
     * @returns {object} 查询结果, 格式:{
                    data: [],// 查询数据
                    total: 0,// 总条数
                    totalPages: 0,// 总页数
                    currentPage: 1,// 当前页码
                }
     */
    async findByFields(storeName, queryObj) {
        return new Promise((resolve, reject) => {
            const transaction = this.db.transaction([storeName])
            const store = transaction.objectStore(storeName)
            
            let keyRange = null
            if (queryObj.startDatetime && queryObj.endDatetime) {
                const startDate = new Date(startDatetime).getTime()
                const endDate = new Date(endDatetime).getTime()
                keyRange = IDBKeyRange.bound(startDate, endDate)
            }
            
            // 获取仓库中所有记录
            // 根据时间参数查询记录,为空时查所有
            const request = store.index('datetime').getAll(keyRange)
            
            // 查询成功的处理
            request.onsuccess = function (event) {
                let results = event.target.result
​
                // 查询姓名(有更多类似条件继续筛就行)
                if (queryObj.name) {
                    results = results.filter(item => item.name === queryObj.name)
                }
                // 查询机构(有更多类似条件继续筛就行)
                if (queryObj.institution.length) {
                    results = results.filter(item => {
                        const res = queryObj.institution.includes(item.institution)
                        if (res) {
                            return true
                        }
                        return false
                    })
                }
                
                // 升降序排序
                results = this._sortByField({ data: results, field: queryObj.field, sort: queryObj.sort })
                
                // 分页查询
                const page = queryObj.page
                const rows = queryObj.rows
                const totalResults = results.length
                const totalPages = Math.ceil(totalResults / rows)
                const offset = (page - 1) * rows
                const paginatedResults = results.slice(offset, offset + rows)
​
                const finalRes = {
                    data: paginatedResults, // 查询数据
                    total: totalResults, // 总条数
                    totalPages, // 总页数
                    currentPage: page,  // 当前页码
                }
                
                resolve(finalRes)
            }
            
            // 查询失败的处理
            request.onerror = function (event) {
                reject(event.target.error)
            }
        })
    }
​
    /**
     * 根据传入的字段排序
     * @param {object} {data:查询的结果,field:排序字段,sort:asc升序,desc降序} 
     * @returns 排序后的结果
     */
    _sortByField({ data, field, sort }) {
        // 根据 field 排序
        if (field === 'datetime') {
            data.sort((a, b) => {
                if (a[field]) {
                    a = new Date(a[field]).getTime()
                } else {
                    a = 0
                }
                if (b[field]) {
                    b = new Date(b[field]).getTime()
                } else {
                    b = 0
                }
                if (sort === 'asc') {
                    return a - b
                } else {
                    return b - a
                }
            })
        }
        return data
    }

经项目中测试,采用上面的写法,查询2万条数据的时间为 0.2 秒左右

3 Vue中使用示例

将第2节中的类库在 vue 项目中实际应用

初始化

在合适的条件下进行初始化,如进入某个tab页时:

新建或打开 test_db数据库的 test 对象仓库:

async initDB() {
    // 新建或打开的数据库名为 test_db ,仓库(表)名为 test,版本号为1。此时会新建一个 test 仓库
    this.dbManager = new IndexedDB('test_db', 'test', 1)
    await this.dbManager.openDB()
    // openDB()成功后再进行增删改查
}

新建或打开 test_db数据库的 test2 对象仓库:

async initDB() {
    // 新建或打开的数据库名为 test_db ,仓库(表)名为 test2,版本号为2。此时会新建一个 test2 仓库
    this.dbManager = new IndexedDB('test_db', 'test2', 2)
    await this.dbManager.openDB()
    // openDB()成功后再进行增删改查
}

新增数据

const id = await this.dbManager.addData('test', data) // 返回主键id

读取数据

const res = await this.dbManager.getData('test', id) // 返回数据记录

更新数据

const id = await this.dbManager.updateData('test', data) // 返回主键id

删除数据

const id = await this.dbManager.deleteData('test', id) // 返回主键id

多条件分页检索

const res = await this.dbManager.findByFields('test', {  name:"", // 姓名
                                                institution:[], // 机构
                                                "startDatetime":"2024-09-23", // 开始时间
                                                "endDatetime":"2024-09-23", // 结束时间
                                                "page":1,  // 查询页码
                                                "rows":20, // 查询条数
                                                "field":"datetime", // 排序字段
                                                "sort":"desc" // 升降序
                                              })
//返回结果res: {
//                data: paginatedResults, // 查询数据
//                total: totalResults, // 总条数
//                totalPages, // 总页数
//                currentPage: page, // 当前页码
//            }

关闭数据库

使用完应释放资源占用

beforeDestroy(){
    this.dbManager.close()
}

参考链接:wangdoc.com/javascript/…