前端不再怕离线!IndexedDB 教你搞定本地大数据存储

0 阅读9分钟

前端不再怕离线!IndexedDB 教你搞定本地大数据存储

前端不再怕离线!IndexedDB 教你搞定本地大数据存储

IndexedDB前言

IndexedDB是浏览器提供的一种本地数据库,用于在用户浏览器中存储大量的结构化数据,包括文件二进制数据。它是一个异步的,基于事务的noSQL数据库,适用于需要脱机访问,数据缓存,大型数据集存储等场景。 非关系型数据库(NoSql)indexedDB是非关系型数据库,主要以键值对的形式存储数据。 持久化存储,localStorage,cookie,sessionStorage等方式i存储的数据,清除浏览器缓存之后,这些数据都会被清除掉,而实用indexedDB存储的数据不会,除非手动删除。 indexedDB操作时不会锁死浏览器,用户依然可以进行其他的操作,与localStorage对比很鲜明,localStorage是同步的。 indexedDB支持事务,这意味着一些列的操作和步骤之中,只要有一步失败了,整个事务就会取消,数据库回滚到事务发生之前的状态,这个MySQL等数据库的事务类似。

IndexedDB 主要特点

特性说明
持久化存储数据保存在用户浏览器中,不会因为刷新或关闭浏览器而丢失。
结构化数据支持存储对象嵌套对象,BlobArrayBuffer等。
异步操作使用事件或者promise避免阻塞主线程。
基于对象存储类似数据库中的表,成为object store,每个对象存储类似于一张表。
索引支持可以为字段创建索引,加快数据查询。
事务机制操作数据的时候需要通过事务进行,保证数据的一致性和完整性。

IndexedDB与其他浏览器存储区别

特性cookiesessionStoragelocalStorageindexedDB
容量大小每个域名4KB5MB5~10MB根据本机自身存储容量,通常数百MB-GB
生命周期可设置事件页面绘画关闭即清除永久除非主动清除永久除非主动清除
作用范围同源所有路径共享当前标签页/窗口同源共享同源共享
是否随请求发送是(每次HTTP请求都会附带)
数据类型字符串(需要手动编码)字符串(需要手动编码)字符串(需要手动编码)可以存储对象,二进制,结构化数据
同步/异步同步(阻塞)同步(阻塞)同步(阻塞)异步(阻塞)
可编程接口简单API简单API,键值对简单API,键值对强大API,支持事务/索引等
适合用途跨请求数据,如登录状态临时数据,如表单临时UI状态持久数据,如用户设置缓存大量结构化数据,如离线应用,本地数据库

IndexedDB实用场景

  1. 离线应用
  2. 大量数据本地缓存,比如地图和表格数据
  3. 存储用户设置,表单草稿
  4. 替代localStorage的方案

IndexedDB基本概念

仓库objectStore

indexedDB没有表的概念,它只有仓库store的概念,可以把仓库理解为表即可,即一个store是一张表。

索引index

在关系型数据库当中也有索引的概念,我们可以给对应的表字段添加索引,以便加快查找速率,在indexedDB中同样有索引,我们可以在创建store的时候创建索引,在后续对store进行查询的时候通过索引来筛选,给某个字段添加索引后,在后续插入数据的过程中,索引字段不能为空。

游标cursor

游标是indexedDB数据库新的概念,大家可以把游标想象为一个指针,比如我们要查询满足某一条件所有数据时,就需要用到游标,让游标一行一行的往下走,游标走到的地方便会返回一行数据,此时我们便可对此行数据进行判断是否满足条件。

事务

indexedDB支持事务,即对数据库进行操作的时候,只要失败了,都会回滚到最初始的状态,确保数据的一致性。

indexedDB操作

indexedDB所有针对仓库的操作都是基于事务的。

插入操作

indexedDB插入数据需要通过事务来进行操作,插入的方法也很简单,利用indexedDB提供的add方法即可,但是要注意的是,插入的数据是一个对象,而且还包含了我们声明的索引键值对。

插入函数封装
/**
 * @param {*} db 在创建或链接数据库时候,返回的db实例
 * @param {*} storeName 仓库名称,在创建或者连接数据库时候就已经创建好了
 * @param {*} data 需要插入的数据,通常是一个对象 
 */
function addData(db, storeName, data) {
  let request = db.transaction([storeName], "readwrite") // 创建一个事务
    .objectStore(storeName) // 获取仓库
    .add(data); // 添加数据
  request.onsuccess = function (e) {
    console.log("数据添加成功");
  };
  request.onerror = function (e) {
    console.log("数据添加失败");
  };
}
插入函数调用
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>IndexedDB数据库</title>
    <script src="./indexedDB.js"></script>
  </head>
  <body></body>
  <script>
    let db;
    // 创建数据库
    openDB("studentDB", 1).then((db) => {
      db = db;
      console.log(db);
      // 插入数据
      addData(db, "user", {
        uuid: new Date(),
        name: "张三",
        age: parseInt(Math.random() * 100),
      });
    });
  </script>
</html>
插入效果

前端性能优化:IndexedDB 数据插入与查询流程图

通过主键读取数据

前端性能优化:IndexedDB 数据插入与查询流程图

封装通过主键查询方法
// 通过主键查询数据 
function getDataByKey(db, storeName, key) {
  var transaction = db.transaction([storeName]);  // 声明事务
  var objectStore = transaction.objectStore(storeName); // 仓库对象
  var request = objectStore.get(key); // 通过主键获取数据
  request.onsuccess = function (e) {
    console.log("数据获取成功", request.result);
    return request.result;
  };
  request.onerror = function (e) {
    console.log("数据查询失败");
  };
}

也可以通过下面getAll() 获取所有数据

  var request = objectStore.getAll(); // 通过主键获取数据
调用
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>IndexedDB数据库</title>
    <script src="./indexedDB.js"></script>
  </head>
  <body></body>
  <script>
    let db;
    // 创建数据库
    openDB("studentDB", 1).then((db) => {
      db = db;
      console.log(db);
      // 插入数据
      addData(db, "user", {
        uuid: parseInt(Math.random() * 100),
        name: "张三",
        age: parseInt(Math.random() * 100),
      });

      // 通过主键查询数据
      getDataByKey(db, "user", 61);
    });
  </script>
</html>

查询效果

前端性能优化:IndexedDB 数据插入与查询流程图

通过游标读取数据

前端性能优化:IndexedDB 数据插入与查询流程图

封装游标查询方法
// 通过游标查询数据
function cursorGetData(db, storeName) {
  let list = [];
  var store = db
    .transaction(storeName, "readwrite")  // 事务
    .objectStore(storeName) // 仓库对象
  var request = store.openCursor(); // 打开游标
  request.onsuccess = function (e) {
    var cursor = e.target.result;
    if (cursor) {
      list.push(cursor.value);
      cursor.continue(); // 遍历了对象中的所有内容
    } else {
      console.log("游标读取的数据 ", list);
      return list;
    }
  };
}
调用
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>IndexedDB数据库</title>
    <script src="./indexedDB.js"></script>
  </head>
  <body></body>
  <script>
    let db;
    // 创建数据库
    openDB("studentDB", 1).then((db) => {
      db = db;
      console.log(db);
      // 插入数据
      addData(db, "user", {
        uuid: parseInt(Math.random() * 100),
        name: "张三",
        age: parseInt(Math.random() * 100),
      });

      // 通过主键查询数据
      getDataByKey(db, "user", 61);

      // 通过游标查询数据
      cursorGetData(db, "user");
    });
  </script>
</html>

查询效果

前端性能优化:IndexedDB 数据插入与查询流程图

通过索引来查询数据

封装索引查询数据方法
// 通过索引查询数据,只查询第一条满足的数据
function getDataByIndex(db, storeName, indexName, indexValue) {
  var store = db
    .transaction(storeName, "readwrite")
    .objectStore(storeName);
  var request = store.index(indexName).get(indexValue);
  request.onsuccess = function (e) {
    var result = e.target.result;
    console.log("通过索引查询数据 ", result);
  };
  request.onerror = function (e) {
    console.log("通过索引查询数据失败 ", e.target.result);
  };
}
调用方法及查询结果

前端性能优化:IndexedDB 数据插入与查询流程图

通过索引和游标查询数据

通过索引查询数据可以看出,得到的数据只有一个,因为通过索引查到第一个数据就会停止,不会继续往下查,如果想查询所有索引值满足条件的就需要用到游标进行查询。

方法说明参数说明
IDBKeyRange.only(value)精确匹配value需要精确匹配的值
IDBKeyRange.lowerBound(lower, open)指定下限范围openfalse包含下限值>=opentrue不包含下限值>
IDBKeyRange.upperBound(upper, open)指定上限范围openfalse包含上限值<=opentrue不包含上限值<
IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen)指定上下限范围lowerOpenupperOpen分别表示是否排除下限和上限的值。

openCursor() 方法用于遍历对象存储或索引中的记录。结合上述键范围方法,可以实现灵活的数据查询。以下是一个使用 openCursor()

封装索引和游标查询方法
// 通过索引和游标查询数据
function cursorGetDataByIndex(db, storeName, indexName, indexValue) {
  let list = []
  var store = db.transaction(storeName, "readwrite").objectStore(storeName); // 获取对象仓库
  var request = store.index(indexName).openCursor(IDBKeyRange.only(indexValue)); // 指针对象
  request.onsuccess = function (e) {
    var cursor = e.target.result;
    if (cursor) {
      // 必须检查
      list.push(cursor.value)
      cursor.continue(); // 遍历了存储对象中的所有内容
    } else {
      console.log("通过索引和游标查询数据成功 ", list);
    }
  };
  request.onerror = function (e) {
    console.log("通过索引和游标查询数据失败 ", e.target.result);
  };
}
游标索引查询效果

前端性能优化:IndexedDB 数据插入与查询流程图

通过索引和游标分页查询

indexedDB分页查询不像MySQL分页查询那么简单,没有提供现成的APIlimit等,所以需要自己实现分页,实现过程中用到了indexedDB的一个advanceAPI,该API可以让游标跳过多少条开始查询,加入分也是每页10条数据,现在需要查询第二页,那我们就需要跳过前面十条数据,从11条数据开始查询,知道计数器等于10,就关闭游标结束查询。

封装索引游标分页查询方法
/**
 * 通过索引和游标分页查询数据
 * @param {*} db  // 数据库
 * @param {*} storeName // 存储对象名称
 * @param {*} query  // 查询条件
 * @param {*} callback // 回调函数
 */
function queryByConditionsAndPage(db, storeName, query, callback) {
  const { pageSize = 10, currentPage = 1, ...filters } = query;
  const transaction = db.transaction([storeName], "readonly"); // 创建事务
  const store = transaction.objectStore(storeName); // 获取存储对象
  const request = store.openCursor(); // 打开游标
  const results = []; // 结果集
  let total = 0; // 总数
  let advanced = false; // 是否已跳过指定条数
  const startPosition = pageSize * (currentPage - 1); // 开始位置

  request.onsuccess = function (event) { // 获取成功
    const cursor = event.target.result; 
    if (!cursor) {
      // 查询结束,返回结果
      callback({ total, data: results });
      return;
    }

    const record = cursor.value; // 当前记录
    let match = true; // 是否匹配

    // 检查每个过滤条件
    for (const key in filters) {
      if (filters.hasOwnProperty(key)) {
        if (record[key] !== filters[key]) {
          match = false;
          break;
        }
      }
    }

    if (match) { // 匹配
      total++;
      if (total > startPosition && results.length < pageSize) {
        results.push(record);
      }
    }

    cursor.continue(); // 继续遍历下一条记录
  };

  request.onerror = function (event) {
    console.error("查询失败:", event.target.error);
    callback({ total: 0, data: [] });
  };
}
调用索引分页查询方法
// 通过索引和游标分页查询
const query = {
  pageSize: 10,
  currentPage: 1,
  name: "张三",
};

queryByConditionsAndPage(db, "user", query, function (result) {
  console.log("查询数据", result);
});
分页查询效果

前端性能优化:IndexedDB 数据插入与查询流程图

更新数据

封装更新数据方法
// 更新数据
function updateDB(db, storeName, data) {
  var request = db.transaction([storeName], "readwrite")
    .objectStore(storeName)
    .put(data);
  request.onsuccess = function (event) {
    console.log("数据更新成功");
  };
  request.onerror = function (event) {
    console.error("数据更新失败:");
  };
}
调用更新数据方法
 let update = {
  uuid: 7,
   name: "吴又可",
   age: 24,
 };
 updateDB(db, "user", update);

注意:其中如果uuid未匹配到,则是添加新的数据。

更新数据效果

前端性能优化:IndexedDB 数据插入与查询流程图

通过主键删除数据

封装调用删除方法
// 通过主键删除数据
function deleteDB(db, storeName, id) {
  var request = db.transaction([storeName], "readwrite")
    .objectStore(storeName)
    .delete(id);
  request.onsuccess = function (event) {
    console.log("数据删除成功");
  };
  request.onerror = function (event) {
    console.error("数据删除失败:");
  };
}

调用:

 deleteDB(db, "user", 7);
删除前后的对比效果

前端性能优化:IndexedDB 数据插入与查询流程图

通过索引和游标删除指定数据

封装调用


// 通过游标和索引删除数据
function deleteRecordsByConditions(db, storeName, conditions, callback) {
  const transaction = db.transaction([storeName], 'readwrite');
  const store = transaction.objectStore(storeName);
  const request = store.openCursor();
  let deleteCount = 0;

  request.onsuccess = function (event) {
    const cursor = event.target.result;
    if (cursor) {
      const record = cursor.value;
      let match = true;

      // 检查每个条件
      for (const key in conditions) {
        if (conditions.hasOwnProperty(key)) {
          if (record[key] !== conditions[key]) {
            match = false;
            break;
          }
        }
      }

      if (match) {
        const deleteRequest = cursor.delete();
        deleteRequest.onsuccess = function () {
          deleteCount++;
        };
        deleteRequest.onerror = function (event) {
          console.error('删除记录失败:', event.target.error);
        };
      }

      cursor.continue();
    } else {
      // 所有记录已遍历完成
      callback(deleteCount);
    }
  };

  request.onerror = function (event) {
    console.error('游标打开失败:', event.target.error);
    callback(0);
  };
}

调用:

const conditions = {
  name: "张三",
};
deleteRecordsByConditions(
  db,
  "user",
  conditions,
  function (deletedCount) {
    console.log(`共删除了 ${deletedCount} 条记录`);
  }
);
根据条件删除所有满足条件数据

前端性能优化:IndexedDB 数据插入与查询流程图 前端性能优化:IndexedDB 数据插入与查询流程图

关闭数据库

封装方法
// 关闭数据库
function closeDB(db) {
  return new Promise((resolve) => {
    db.close(); // 实际是同步的
    console.log("关闭数据库成功");
    resolve(); // 手动返回 Promise 让调用端等待
  });
}

调用方法

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>IndexedDB数据库</title>
    <script src="./indexedDB.js"></script>
  </head>
  <body></body>
  <script>
    // 封装为异步函数,统一 await 调用链
    async function run() {
      const db = await openDB("studentDB", 1);

      await addData(db, "user", {
        uuid: parseInt(Math.random() * 100),
        name: "张三",
        age: parseInt(Math.random() * 10),
      });

      await new Promise((resolve) => {
        getDataByKey(db, "user", 61);
        resolve(); // 非异步返回的兼容处理
      });

      await new Promise((resolve) => {
        cursorGetData(db, "user");
        resolve();
      });

      await new Promise((resolve) => {
        getDataByIndex(db, "user", "age", 9);
        resolve();
      });

      await new Promise((resolve) => {
        cursorGetDataByIndex(db, "user", "age", 9);
        resolve();
      });

      const query = {
        pageSize: 10,
        currentPage: 1,
        name: "张三",
      };

      await new Promise((resolve) => {
        queryByConditionsAndPage(db, "user", query, function (result) {
          console.log("查询数据", result);
          resolve();
        });
      });

      let update = {
        uuid: 7,
        name: "吴又可",
        age: 24,
      };
      await new Promise((resolve) => {
        updateDB(db, "user", update);
        resolve();
      });

      const conditions = {
        name: "张三",
      };
      await new Promise((resolve) => {
        deleteRecordsByConditions(
          db,
          "user",
          conditions,
          function (deletedCount) {
            console.log(`共删除了 ${deletedCount} 条记录`);
            resolve();
          }
        );
      });

      await closeDB(db); // 最后执行
    }

    run();
  </script>
</html>

关闭效果

前端性能优化:IndexedDB 数据插入与查询流程图

完结~