浏览器缓存: IndexDB

文章目录

    • [主要 API 方法和步骤](#主要 API 方法和步骤)
      • [1. 打开/创建数据库](#1. 打开/创建数据库)
      • [2. 处理版本升级 (`onupgradeneeded`)](#2. 处理版本升级 (onupgradeneeded))
      • [3. 增删改查操作](#3. 增删改查操作)
      • [现代开发:使用 `Promise` 包装](#现代开发:使用 Promise 包装)
    • 总结
    • 实战演示

IndexedDB 是一个运行在浏览器上的非关系型数据库,用于在客户端存储大量结构化数据。

特点:

特点 简要说明
存储空间大 每个源通常 ≥ 250 MB,浏览器/系统配额决定,可视为"无上限"。
支持二进制数据 原生支持 ArrayBufferBlobFile 等二进制类型。
键值对存储 以对象仓库为单位,按主键(key)存储 JavaScript 对象(value)。
同源限制 只能被同一协议+域名+端口的页面访问,无法跨域共享。
异步执行 所有操作基于事件/Promise,非阻塞主线程。
事务型数据库系统 支持 read-only、readwrite、versionchange 事务,具备 ACID 语义(失败回滚)。

主要 API 方法和步骤

1. 打开/创建数据库

使用 indexedDB.open(name, version) 方法。如果数据库不存在,则会创建它。

javascript 复制代码
// 打开版本为 1 的 'my-database' 数据库
const request = indexedDB.open("my-database", 1);

let db; // 用于保存数据库连接

// 成功打开数据库
request.onsuccess = function (event) {
  db = event.target.result; // 数据库对象
  console.log("数据库打开成功");
};

// 打开数据库失败
request.onerror = function (event) {
  console.error("数据库打开错误:", event.target.error);
};

2. 处理版本升级 (onupgradeneeded)

这是最关键的一步。当第一次创建数据库,或者传入的版本号比当前版本高时,会触发 onupgradeneeded 事件。所有对象存储和索引的创建、修改、删除都必须在这里进行。

javascript 复制代码
request.onupgradeneeded = function (event) {
  // 保存数据库实例
  const db = event.target.result;

  // 检查对象存储是否已存在,如果不存在则创建
  if (!db.objectStoreNames.contains("books")) {
    // 创建一个名为 'books' 的对象存储,并定义主键为 'id'(它将来自对象本身的 'id' 属性)
    const objectStore = db.createObjectStore("books", { keyPath: "id", autoIncrement: true });

    // 创建一个索引以便通过 'title' 搜索
    objectStore.createIndex("title", "title", { unique: false });
    objectStore.createIndex("author", "author", { unique: false });

    console.log("对象存储和索引创建完毕");
  }
};

3. 增删改查操作

所有操作都需要在事务中进行。

a. 添加数据

javascript 复制代码
function addBook(book) {
  // 1. 开启一个读写事务,指定涉及的对象存储 'books'
  const transaction = db.transaction(["books"], "readwrite");
  // 2. 获取对象存储
  const objectStore = transaction.objectStore("books");
  // 3. 执行添加操作
  const request = objectStore.add(book);

  request.onsuccess = function () {
    console.log("书籍已添加");
  };

  request.onerror = function () {
    console.error("添加书籍失败:", request.error);
  };
}

// 使用
addBook({ title: "JavaScript权威指南", author: "David Flanagan", year: 2020 });

b. 读取数据(通过主键)

javascript 复制代码
function getBook(id) {
  const transaction = db.transaction(["books"], "readonly");
  const objectStore = transaction.objectStore("books");
  const request = objectStore.get(id); // 根据主键获取

  request.onsuccess = function () {
    if (request.result) {
      console.log("找到书籍:", request.result);
    } else {
      console.log("未找到该书籍");
    }
  };

  request.onerror = function () {
    console.error("获取书籍失败:", request.error);
  };
}

c. 通过索引查询数据

javascript 复制代码
function getBooksByAuthor(authorName) {
  const transaction = db.transaction(["books"], "readonly");
  const objectStore = transaction.objectStore("books");
  // 1. 获取索引
  const index = objectStore.index("author");
  // 2. 在索引上查询
  const request = index.getAll(authorName); // 获取所有匹配的作者

  request.onsuccess = function () {
    console.log(`找到作者为 ${authorName} 的书籍:`, request.result);
  };
}

d. 使用游标遍历所有数据

javascript 复制代码
function getAllBooks() {
  const transaction = db.transaction(["books"], "readonly");
  const objectStore = transaction.objectStore("books");
  // 1. 在对象存储上打开游标请求
  const request = objectStore.openCursor();
  const allBooks = [];

  request.onsuccess = function (event) {
    // 2. 获取游标对象
    const cursor = event.target.result;
    if (cursor) {
      // 3. cursor.value 是当前指向的数据记录
      allBooks.push(cursor.value);
      // 4. 移动到下一条记录
      cursor.continue();
    } else {
      // 5. 遍历完毕,没有更多数据了
      console.log("所有书籍:", allBooks);
    }
  };
}

e. 更新数据

更新操作类似于添加,但使用 put() 方法。如果主键已存在,则替换该记录;如果不存在,则添加新记录。

javascript 复制代码
function updateBook(updatedBook) {
  const transaction = db.transaction(["books"], "readwrite");
  const objectStore = transaction.objectStore("books");
  const request = objectStore.put(updatedBook); // 使用 put 方法

  request.onsuccess = function () {
    console.log("书籍已更新");
  };
}

f. 删除数据

javascript 复制代码
function deleteBook(id) {
  const transaction = db.transaction(["books"], "readwrite");
  const objectStore = transaction.objectStore("books");
  const request = objectStore.delete(id);

  request.onsuccess = function () {
    console.log("书籍已删除");
  };
}

现代开发:使用 Promise 包装

原生的 API 是基于事件的,代码容易形成"回调地狱"。一个常见的做法是用 Promise 将其包装起来,使其更易于使用。

javascript 复制代码
// 一个简单的 Promise 包装函数,用于执行事务操作
function promisifyRequest(request) {
  return new Promise((resolve, reject) => {
    request.onsuccess = () => resolve(request.result);
    request.onerror = () => reject(request.error);
  });
}

// 使用 async/await 的读取示例
async function getBookAsync(id) {
  const transaction = db.transaction(["books"], "readonly");
  const objectStore = transaction.objectStore("books");
  try {
    const book = await promisifyRequest(objectStore.get(id));
    console.log("找到书籍:", book);
    return book;
  } catch (error) {
    console.error("获取失败:", error);
  }
}

总结

概念/方法 用途
indexedDB.open() 打开/创建数据库
onupgradeneeded 创建/修改数据库结构(对象存储、索引)
transaction() 创建事务,指定模式和对象存储
objectStore() 在事务中获取对象存储
add() 添加新数据
get() 通过主键获取数据
put() 更新数据(或添加)
delete() 删除数据
createIndex() 创建索引
index() 获取索引
openCursor() 打开游标以遍历数据
getAll(), getAllKeys() 获取所有数据或键(小心大量数据)

优点:

  • 存储量大(通常远大于 localStorage 的 5MB)。
  • 支持异步操作,不阻塞 UI。
  • 支持事务,保证数据一致性。
  • 支持复杂的查询(通过索引和游标)。
  • 支持二进制数据存储(如 Blob, File)。

缺点:

  • API 相对底层和繁琐。
  • 语法较为复杂,需要理解事务、游标等概念。

对于现代项目,可以考虑使用封装了 IndexedDB 的库(如 Dexie.jsIDB),它们提供了更简洁、更符合现代开发习惯的 API。

实战演示

主要操作如下:

实现代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>IndexedDB 数据管理</title>
    <style>
      body {
        font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
        max-width: 800px;
        margin: 0 auto;
        padding: 20px;
        background-color: #f8f9fa;
        color: #333;
      }
      h1 {
        color: #2c3e50;
        text-align: center;
        margin-bottom: 20px;
      }
      .container {
        background-color: white;
        border-radius: 10px;
        padding: 25px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      }
      .button-group {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
        margin-bottom: 20px;
      }
      button {
        padding: 12px 18px;
        border: none;
        border-radius: 5px;
        background-color: #3498db;
        color: white;
        font-weight: bold;
        cursor: pointer;
        transition: background-color 0.3s;
        flex: 1;
        min-width: 150px;
      }
      button:hover {
        background-color: #2980b9;
      }
      button.success {
        background-color: #2ecc71;
      }
      button.success:hover {
        background-color: #27ae60;
      }
      button.danger {
        background-color: #e74c3c;
      }
      button.danger:hover {
        background-color: #c0392b;
      }
      button.warning {
        background-color: #f39c12;
      }
      button.warning:hover {
        background-color: #d35400;
      }
      .status {
        margin: 15px 0;
        padding: 12px;
        border-radius: 5px;
        background-color: #f8f9fa;
        border-left: 4px solid #3498db;
      }
      .status.success {
        border-left-color: #2ecc71;
        background-color: #eafaf1;
      }
      .status.error {
        border-left-color: #e74c3c;
        background-color: #fdedec;
      }
      .status.warning {
        border-left-color: #f39c12;
        background-color: #fef9e7;
      }
      .data-display {
        margin-top: 20px;
        padding: 15px;
        background-color: #f8f9fa;
        border-radius: 5px;
        max-height: 300px;
        overflow-y: auto;
      }
      pre {
        white-space: pre-wrap;
        word-break: break-all;
        margin: 0;
      }
      .explanation {
        margin-top: 20px;
        padding: 15px;
        background-color: #e8f4fc;
        border-radius: 5px;
        border-left: 4px solid #3498db;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <h1>IndexedDB 数据管理</h1>

      <div class="button-group">
        <button id="open-btn">打开/创建数据库</button>
        <button id="add-btn" disabled>添加示例数据</button>
        <button id="read-btn" disabled>读取数据</button>
        <button id="clear-btn" disabled class="danger">清空数据</button>
        <button id="delete-db-btn" class="warning">删除整个数据库</button>
      </div>

      <div id="status" class="status">准备就绪,请点击"打开/创建数据库"开始</div>

      <div class="data-display">
        <h3>当前存储的数据:</h3>
        <pre id="data-output">暂无数据</pre>
      </div>

      <div class="explanation">
        <h3>为什么清空数据后刷新页面数据还在?</h3>
        <p>IndexedDB 是浏览器端的持久化存储,数据会一直保存直到:</p>
        <ol>
          <li>用户手动清除浏览器数据</li>
          <li>程序主动删除数据库或数据</li>
          <li>达到浏览器存储配额限制</li>
        </ol>
        <p>"清空数据"只是删除当前对象仓库中的所有记录,但数据库结构仍然存在。</p>
        <p>如果需要完全移除数据,请使用"删除整个数据库"按钮。</p>
      </div>
    </div>

    <script>
      let db = null;
      const dbName = "myDB";
      const storeName = "userInfo";
      const dbVersion = 1;

      const openBtn = document.getElementById("open-btn");
      const addBtn = document.getElementById("add-btn");
      const readBtn = document.getElementById("read-btn");
      const clearBtn = document.getElementById("clear-btn");
      const deleteDbBtn = document.getElementById("delete-db-btn");
      const statusEl = document.getElementById("status");
      const dataOutput = document.getElementById("data-output");

      // 更新状态信息
      function updateStatus(message, isError = false, isWarning = false) {
        statusEl.textContent = message;
        if (isError) {
          statusEl.className = "status error";
        } else if (isWarning) {
          statusEl.className = "status warning";
        } else {
          statusEl.className = "status success";
        }
        console.log(message);
      }

      // 打开/创建一个 IndexedDB 数据库
      openBtn.addEventListener("click", () => {
        updateStatus("正在打开数据库...");

        const request = indexedDB.open(dbName, dbVersion);

        request.onsuccess = function (event) {
          db = event.target.result;
          updateStatus("数据库打开成功!");

          // 启用其他按钮
          addBtn.disabled = false;
          readBtn.disabled = false;
          clearBtn.disabled = false;

          // 自动读取现有数据
          readData();
        };

        request.onerror = function (event) {
          updateStatus("打开数据库时出错: " + event.target.error, true);
        };

        request.onupgradeneeded = function (event) {
          db = event.target.result;
          updateStatus("数据库升级中,正在创建对象仓库...", false, true);

          // 如果对象仓库不存在,则创建它
          if (!db.objectStoreNames.contains(storeName)) {
            const objectStore = db.createObjectStore(storeName, {
              keyPath: "id",
              autoIncrement: false
            });

            // 创建索引
            objectStore.createIndex("id", "id", { unique: true });
            objectStore.createIndex("name", "name");
            objectStore.createIndex("email", "email");

            updateStatus("对象仓库创建成功");
          }
        };
      });

      // 添加示例数据
      addBtn.addEventListener("click", () => {
        if (!db) {
          updateStatus("请先打开数据库", true);
          return;
        }

        try {
          const transaction = db.transaction([storeName], "readwrite");
          const objectStore = transaction.objectStore(storeName);

          // 添加示例数据
          const users = [
            { id: 1, name: "张三", email: "zhangsan@example.com", age: 25 },
            { id: 2, name: "李四", email: "lisi@example.com", age: 30 },
            { id: 3, name: "王五", email: "wangwu@example.com", age: 28 }
          ];

          users.forEach((user) => {
            const request = objectStore.add(user);
            request.onerror = () => {
              updateStatus(`添加用户 ${user.name} 时出错: 可能ID已存在`, true);
            };
          });

          transaction.oncomplete = () => {
            updateStatus("成功添加示例数据");
            readData(); // 读取并显示数据
          };

          transaction.onerror = (event) => {
            updateStatus("添加数据时出错: " + event.target.error, true);
          };
        } catch (error) {
          updateStatus("添加数据时发生异常: " + error.message, true);
        }
      });

      // 读取数据
      readBtn.addEventListener("click", readData);

      function readData() {
        if (!db) {
          updateStatus("请先打开数据库", true);
          return;
        }

        try {
          const transaction = db.transaction([storeName], "readonly");
          const objectStore = transaction.objectStore(storeName);
          const request = objectStore.getAll();

          request.onsuccess = () => {
            const data = request.result;
            if (data.length > 0) {
              dataOutput.textContent = JSON.stringify(data, null, 2);
              updateStatus(`成功读取 ${data.length} 条数据`);
            } else {
              dataOutput.textContent = "暂无数据";
              updateStatus("数据库中没有数据", false, true);
            }
          };

          request.onerror = () => {
            updateStatus("读取数据时出错", true);
          };
        } catch (error) {
          updateStatus("读取数据时发生异常: " + error.message, true);
        }
      }

      // 清空数据(只删除记录,不删除数据库结构)
      clearBtn.addEventListener("click", () => {
        if (!db) {
          updateStatus("请先打开数据库", true);
          return;
        }

        if (confirm("确定要清空所有数据吗?此操作不可撤销。刷新页面后数据将不再显示,但数据库结构仍会保留。")) {
          try {
            const transaction = db.transaction([storeName], "readwrite");
            const objectStore = transaction.objectStore(storeName);
            const request = objectStore.clear();

            request.onsuccess = () => {
              dataOutput.textContent = "暂无数据";
              updateStatus("数据已清空。注意:刷新页面后数据库结构仍然存在。", false, true);
            };

            request.onerror = () => {
              updateStatus("清空数据时出错", true);
            };
          } catch (error) {
            updateStatus("清空数据时发生异常: " + error.message, true);
          }
        }
      });

      // 删除整个数据库(完全移除)
      deleteDbBtn.addEventListener("click", () => {
        if (confirm("确定要删除整个数据库吗?此操作将完全移除数据库及其所有数据,不可撤销!")) {
          // 先关闭当前数据库连接
          if (db) {
            db.close();
          }

          const deleteRequest = indexedDB.deleteDatabase(dbName);

          deleteRequest.onsuccess = () => {
            db = null;
            dataOutput.textContent = "暂无数据";
            updateStatus("数据库已成功删除!刷新页面后将不再存在。");

            // 禁用操作按钮
            addBtn.disabled = true;
            readBtn.disabled = true;
            clearBtn.disabled = true;
          };

          deleteRequest.onerror = () => {
            updateStatus("删除数据库时出错", true);
          };

          deleteRequest.onblocked = () => {
            updateStatus("删除操作被阻止,请关闭其他可能使用此数据库的页面", true);
          };
        }
      });

      // 页面加载时尝试自动打开数据库
      window.addEventListener("load", () => {
        openBtn.click();
      });
    </script>
  </body>
</html>

👉点击进入 我的网站

相关推荐
@淡 定5 小时前
Redis热点Key独立集群实现方案
数据库·redis·缓存
小二·5 小时前
前端监控体系完全指南:从错误捕获到用户行为分析(Vue 3 + Sentry + Web Vitals)
前端·vue.js·sentry
laocooon5238578866 小时前
mysql,100个题目。
数据库·sql·mysql
Web极客码6 小时前
如何在Ubuntu服务器上安装和配置BIND9
服务器·数据库·ubuntu
W001hhh6 小时前
数据库实训Day004上午
数据库
阿珊和她的猫7 小时前
`require` 与 `import` 的区别剖析
前端·webpack
funfan05177 小时前
【运维】MySQL数据库全量备份与恢复实战指南:从入门到精通
运维·数据库·mysql
+VX:Fegn08957 小时前
计算机毕业设计|基于springboot + vue在线音乐播放系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
谎言西西里7 小时前
零基础 Coze + 前端 Vue3 边玩边开发:宠物冰球运动员生成器
前端·coze
tq10867 小时前
通用数据引用表示法:基于协议-URI-JSONPath的简洁数据定位规范
数据库