Indexed DB

0x01 概述

(1)简介

  • IndexedDB 是一个事务型数据库系统,类似于基于 SQL 的 RDBMS(关系型数据库系统)
  • IndexedDB 是一个基于 JavaScript 的面向对象数据库
  • IndexedDB 允许存储和检索用索引的对象,可以存储结构化克隆算法支持的任何对象
  • 使用场景主要用于存储大量数据(大于 250MB) ,如:
    • 数据可视化界面
    • 即时聊天
    • 其他缓存方式容量不足
  • IndexedDB 执行的操作是异步执行的,以免阻塞应用程序

(2)名词

  1. 仓库(objectStore):相当于 MySQL 的表
  2. 索引(index):用于加快查找速率
  3. 游标(cursor):相当于指针,遍历每行的数据并返回
    • IndexedDB 仅通过主键、索引、游标的方式查询数据
  4. 事务(transaction):用于确保数据一致性,当操作失败时可以回滚

(3)环境准备

  • 采用原生 HTML+JavaScript 开发,理论上用记事本就可以完成,推荐采用 VSCode+Live Preview 插件
  • 下述代码在 Google Chrome 136.0.7103.93 测试通过
  1. 新建 script.js,用于封装 IndexedDB 相关方法

  2. 同目录下新建 index.html,用于可视化操作

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8" />
        <title>IndexedDB</title>
        <script src="./script.js"></script>
      </head>
      <body></body>
      <script></script>
    </html>
    
    

0x02 创建与连接

(1)数据库

// 浏览器兼容性判断
const currentIndexedDB =
  window.indexedDB || // IE9+
  window.mozIndexedDB || // Firefox
  window.webkitIndexedDB || // Chrome
  window.msIndexedDB; // Safari

/**
 * 创建或连接数据库
 * @param {object} dbName 数据库名称
 * @param {number} version 数据库版本
 * @param {function} upgradeCallback 数据库更新回调
 * @returns {Promise} 数据库实例
 */
function connect(dbName, version = 1, upgradeCallback = () => {}) {
  return new Promise((resolve, reject) => {
    if (!dbName) reject("请传入数据库名称");

    // 打开数据库,没有则创建
    const request = currentIndexedDB.open(dbName, version);
    let db;

    // 数据库连接成功回调
    request.onsuccess = (event) => {
      db = event.target.result;
      console.log("数据库连接成功");
      resolve(db);
    };

    // 数据库连接失败回调
    request.onerror = (event) => {
      console.error("数据库连接失败");
      reject(event.target.error);
    };

    // 数据库更新回调
    request.onupgradeneeded = (event) => {
      db = event.target.result;
      console.log("数据库更新");
      resolve(upgradeCallback(db));
    };
  });
}

当版本(version)变化时触发 onupgradeneeded

(2)仓库与索引

  • 仓库与索引可以在 request.onupgradeneeded 回调函数中创建

  • 举例:创建用户仓库及其索引

    1. script.js

      /**
       * 创建仓库
       * @param {IDBDatabase} db 数据库实例
       * @param {string} tableName 仓库名
       * @param {object} options 仓库结构
       * @param {array} index 索引字符串数组
       * @returns {Promise} 仓库实例
       */
      function createObjectStore(db, storeName, options, index = []) {
        return new Promise((resolve, reject) => {
          if (!db) reject("请先连接数据库");
          if (!storeName) reject("请传入仓库名称");
          if (!options) reject("请传入仓库结构");
      
          // 创建仓库
          const objectStore = db.createObjectStore(storeName, options);
      
          // 创建索引
          index.forEach((item) =>
            objectStore.createIndex(item, item, { unique: false })
          );
      
          // 仓库创建成功回调
          objectStore.oncomplete = (event) => {
            console.log("仓库创建成功");
            resolve(objectStore);
          };
      
          // 仓库创建失败回调
          objectStore.onerror = (event) => {
            console.error("仓库创建失败");
            reject(event.target.error);
          };
        });
      }
      
    2. index.html

      <!DOCTYPE html>
      <html>
        <head>
          <meta charset="UTF-8" />
          <title>IndexedDB</title>
          <script src="./script.js"></script>
        </head>
        <body>
          <button onclick="init()">初始化数据库</button>
      
          <script>
            const DATABASE_NAME = "myDB",
              USERS_STORE = "users";
            let db = null;
      
            // 初始化数据库
            async function init() {
              db = await connect(DATABASE_NAME, 1, async (database) => {
                await createObjectStore(database, USERS_STORE, { keyPath: "id" }, [
                  "name",
                  "email",
                ]);
              });
            }
          </script>
        </body>
      </html>
      

0x03 增删改查

(1)新增数据

  1. script.js

    /**
     * 新增数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {object} data 待新增数据
     * @returns {Promise} 新增数据结果
     */
    function insert(db, storeName, data) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!data) reject("请传入数据");
    
        const request = db
          .transaction(storeName, "readwrite") // 创建事务,读写
          .objectStore(storeName) // 获取仓库
          .add(data); // 添加数据
    
        request.onsuccess = () => {
          console.log("数据添加成功");
          resolve();
        };
    
        request.onerror = (event) => {
          console.error("数据添加失败");
          reject(event.target.error);
        };
      });
    }
    
  2. index.html

    <!DOCTYPE html>
    <html>
      <!-- ... -->
      <body>
        <button onclick="init()">初始化数据库</button>
        <fieldset>
          <legend>新增数据</legend>
          <form>
            <label>姓名:<input type="text" name="name" /></label>
            <label>邮箱:<input type="text" name="email" /></label>
            <input type="button" value="保存" onclick="insertSubmit(event)" />
          </form>
        </fieldset>
    
        <script>
          const DATABASE_NAME = "myDB",
            USERS_STORE = "users";
          let db = null;
    
          // 生成 uuid
          function uuidv4() {
            return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
              var r = (Math.random() * 16) | 0,
                v = c === "x" ? r : (r & 0x3) | 0x8;
              return v.toString(16);
            });
          }
    
          // 初始化数据库
          async function init() {
            // ...
          }
    
          // 保存表单数据到数据库
          async function insertSubmit(event) {
            var form = event.target.form;
            await insert(db, USERS_STORE, {
              id: uuidv4(),
              name: form.name.value,
              email: form.email.value,
            });
          }
        </script>
      </body>
    </html>
    

(2)查询数据

  1. 根据主键查询

    /**
     * 根据主键获取数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {string} key 主键
     * @returns {Promise} 获取数据结果
     */
    function queryByPrimaryKey(db, storeName, key) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!key) reject("请传入主键");
    
        const request = db
          .transaction(storeName, "readonly") // 创建事务,只读
          .objectStore(storeName) // 获取仓库
          .get(key);
    
        request.onsuccess = (event) => {
          console.log("数据获取成功");
          resolve(event.target.result);
        };
    
        request.onerror = (event) => {
          console.error("数据获取失败");
          reject(event.target.error);
        };
      });
    }
    
  2. 根据游标查询

    /**
     * 根据游标获取数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @returns {Promise} 获取数据结果
     */
    function queryByCursor(db, storeName) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
    
        // 存储结果
        const result = [];
    
        const request = db
          .transaction(storeName, "readonly") // 创建事务,只读
          .objectStore(storeName) // 获取仓库
          .openCursor(); // 获取游标
    
        request.onsuccess = (event) => {
          console.log("数据获取成功");
          let cursor = event.target.result;
          if (cursor) {
            result.push(cursor.value);
            cursor.continue();
          } else resolve(result);
        };
    
        request.onerror = (event) => {
          console.error("数据获取失败");
          reject(event.target.error);
        };
      });
    }
    
  3. 根据索引查询

    /**
     * 根据索引获取数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {{name, value}} indexObject 索引对象
     * @returns {Promise} 获取数据结果
     */
    function queryByIndex(db, storeName, indexObject) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!indexObject) reject("请传入索引对象");
        else if (!indexObject.name) reject("请传入索引名称");
        else if (!indexObject.value) reject("请传入索引值");
    
        const request = db
          .transaction(storeName, "readonly") // 创建事务,只读
          .objectStore(storeName) // 获取仓库
          .index(indexObject.name) // 获取索引
          .get(indexObject.value); // 获取数据
    
        request.onsuccess = (event) => {
          console.log("数据获取成功");
          resolve(event.target.result);
        };
    
        request.onerror = (event) => {
          console.error("数据获取失败");
          reject(event.target.error);
        };
      });
    }
    
  4. 根据索引和游标查询

    /**
     * 根据索引和游标获取数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {{name, value}} indexObject 索引对象
     * @returns {Promise} 获取数据结果
     */
    function queryByIndexAndCursor(db, storeName, indexObject) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!indexObject) reject("请传入索引对象");
        else if (!indexObject.name) reject("请传入索引名称");
        else if (!indexObject.value) reject("请传入索引值");
    
        // 存储结果
        const result = [];
    
        const request = db
          .transaction(storeName, "readonly") // 创建事务,只读
          .objectStore(storeName) // 获取仓库
          .index(indexObject.name) // 获取索引
          .openCursor(IDBKeyRange.only(indexObject.value)); // 获取游标
    
        request.onsuccess = (event) => {
          console.log("数据获取成功");
          let cursor = event.target.result;
          if (cursor) {
            result.push(cursor.value);
            cursor.continue();
          } else resolve(result);
        };
    
        request.onerror = (event) => {
          console.error("数据获取失败");
          reject(event.target.error);
        };
      });
    }
    
  5. 根据索引和游标分页查询

    /**
     * 根据索引和游标并分页获取数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {{name, value}} indexObject 索引对象
     * @param {{number, size}} pageObject 分页对象
     * @returns {Promise} 获取数据结果
     */
    function queryByIndexAndCursorAndPagination(
      db,
      storeName,
      indexObject,
      pageObject
    ) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!indexObject) reject("请传入索引对象");
        else if (!indexObject.name) reject("请传入索引名称");
        else if (!indexObject.value) reject("请传入索引值");
        if (!pageObject) reject("请传入分页对象");
        else if (!pageObject.number) reject("请传入页码");
        else if (pageObject.number < 1) reject("页码不能小于 1");
        else if (!pageObject.size) reject("请传入页大小");
        else if (pageObject.size < 1) reject("页大小不能小于 1");
    
        // 存储结果
        const result = [];
        let counter = 0, // 计数器
          advanced = true; // 是否已跳过指定页码
    
        const request = db
          .transaction(storeName, "readonly") // 创建事务,只读
          .objectStore(storeName) // 获取仓库
          .index(indexObject.name) // 获取索引
          .openCursor(IDBKeyRange.only(indexObject.value)); // 获取游标
    
        request.onsuccess = (event) => {
          console.log("数据获取成功");
          let cursor = event.target.result;
    
          // 跳过指定页码
          if (pageObject.number > 1 && advanced) {
            advanced = false;
            cursor.advance((pageObject.number - 1) * pageObject.size);
            return;
          }
    
          if (cursor) {
            result.push(cursor.value);
            counter++;
            if (counter < pageObject.size) cursor.continue(); // 继续获取下一条数据
            else resolve(result);
          } else resolve(result);
        };
    
        request.onerror = (event) => {
          console.error("数据获取失败");
          reject(event.target.error);
        };
      });
    }
    
  6. index.html

    <!DOCTYPE html>
    <html>
      <!-- ... -->
      <body>
        <!-- ... -->
        <fieldset>
          <legend>查询数据</legend>
          <fieldset>
            <legend>根据主键查询</legend>
            <form>
              <label>主键:<input type="text" name="pk" /></label>
              <input
                type="button"
                value="查询"
                onclick="querySubmit(event, 'pk')"
              />
            </form>
          </fieldset>
          <fieldset>
            <legend>根据游标查询</legend>
            <form>
              <input
                type="button"
                value="查询所有"
                onclick="querySubmit(event, 'cursor')"
              />
            </form>
          </fieldset>
          <fieldset>
            <legend>根据索引查询</legend>
            <form>
              <label>姓名:<input type="text" name="name" /></label>
              <input
                type="button"
                value="查询"
                onclick="querySubmit(event, 'index')"
              />
            </form>
          </fieldset>
          <fieldset>
            <legend>根据索引和游标查询</legend>
            <form>
              <label>姓名:<input type="text" name="name" /></label>
              <input
                type="button"
                value="查询"
                onclick="querySubmit(event, 'index-cursor')"
              />
            </form>
          </fieldset>
          <fieldset>
            <legend>根据索引和游标并分页查询</legend>
            <form>
              <label>
                邮箱:
                <input type="text" name="email" />
              </label>
              <br />
              <label>
                页码:
                <input type="number" min="1" name="page-number" />
              </label>
              <label>
                每页数量:<input type="number" min="1" name="page-size" />
              </label>
              <input
                type="button"
                value="查询"
                onclick="querySubmit(event, 'index-cursor-pagination')"
              />
            </form>
          </fieldset>
    
          <fieldset>
            <legend>查询结果</legend>
            <ul id="result"></ul>
          </fieldset>
        </fieldset>
    
        <script>
          // ...
    
          // 查询表单数据
          async function querySubmit(event, type) {
            var form = event.target.form;
            var result = document.getElementById("result");
            result.innerHTML = "";
            var data = [];
            switch (type) {
              case "pk":
                data = [await queryByPrimaryKey(db, USERS_STORE, form["pk"].value)];
                break;
              case "cursor":
                data = await queryByCursor(db, USERS_STORE);
                break;
              case "index":
                data = [
                  await queryByIndex(db, USERS_STORE, {
                    name: "name",
                    value: form["name"].value,
                  }),
                ];
                break;
              case "index-cursor":
                data = await queryByIndexAndCursor(db, USERS_STORE, {
                  name: "name",
                  value: form["name"].value,
                });
                break;
              case "index-cursor-pagination":
                data = await queryByIndexAndCursorAndPagination(
                  db,
                  USERS_STORE,
                  {
                    name: "email",
                    value: form["email"].value,
                  },
                  {
                    number: parseInt(form["page-number"].value),
                    size: parseInt(form["page-size"].value),
                  }
                );
                break;
              default:
                alert("未知查询类型");
                break;
            }
    
            // 创建结果列表
            const fragment = document.createDocumentFragment();
            if (data.length > 0)
              data.forEach((item) => {
                const li = document.createElement("li");
                li.textContent = item.name + " " + item.email;
                fragment.appendChild(li);
              });
            result.appendChild(fragment);
          }
        </script>
      </body>
    </html>
    
    

(3)更新数据

  1. script.js

    /**
     * 修改数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {object} data 待修改数据
     * @returns {Promise} 修改数据结果
     */
    function update(db, storeName, data) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!data) reject("请传入数据");
        else if (!data.id) reject("请传入数据 ID");
    
        const request = db
          .transaction(storeName, "readwrite") // 创建事务,读写
          .objectStore(storeName) // 获取仓库
          .put(data); // 修改数据
    
        request.onsuccess = () => {
          console.log("数据修改成功");
          resolve();
        };
    
        request.onerror = (event) => {
          console.error("数据修改失败");
          reject(event.target.error);
        };
      });
    }
    
  2. index.html

    <!DOCTYPE html>
    <html>
      <!-- ... -->
      <body>
        <!-- ... -->
        <fieldset>
          <legend>修改数据</legend>
          <form>
            <label>主键:<input type="text" name="pk" /></label>
            <label>姓名:<input type="text" name="name" /></label>
            <label>邮箱:<input type="text" name="email" /></label>
            <input type="button" value="保存" onclick="updateSubmit(event)" />
          </form>
        </fieldset>
        </fieldset>
    
        <script>
          // ...
    
          // 修改表单数据
          async function updateSubmit(event) {
            var form = event.target.form;
            await update(db, USERS_STORE, {
              id: form.pk.value,
              name: form.name.value,
              email: form.email.value,
            });
          }
        </script>
      </body>
    </html>
    
    

(4)删除数据

  1. 根据主键删除

    /**
     * 根据主键删除数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {string} key 主键
     * @returns {Promise} 获取数据结果
     */
    function deleteByPrimaryKey(db, storeName, key) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!key) reject("请传入主键");
    
        const request = db
          .transaction(storeName, "readwrite") // 创建事务,读写
          .objectStore(storeName) // 获取仓库
          .delete(key); // 删除数据
    
        request.onsuccess = (event) => {
          console.log("数据删除成功");
          resolve(event.target.result);
        };
    
        request.onerror = (event) => {
          console.error("数据删除失败");
          reject(event.target.error);
        };
      });
    }
    
  2. 根据索引游标删除

    /**
     * 根据索引和游标删除数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {{name, value}} indexObject 索引对象
     * @returns {Promise} 删除数据结果
     */
    function deleteByIndexAndCursor(db, storeName, indexObject) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!indexObject) reject("请传入索引对象");
        else if (!indexObject.name) reject("请传入索引名称");
        else if (!indexObject.value) reject("请传入索引值");
    
        const request = db
          .transaction(storeName, "readwrite") // 创建事务,读写
          .objectStore(storeName) // 获取仓库
          .index(indexObject.name) // 获取索引
          .openCursor(IDBKeyRange.only(indexObject.value)); // 获取游标
    
        request.onsuccess = (event) => {
          let cursor = event.target.result;
          if (cursor) {
            const deleteRequest = cursor.delete();
    
            deleteRequest.onsuccess = () => {
              console.log("数据删除成功");
            };
    
            deleteRequest.onerror = () => {
              console.error("数据删除失败");
              reject(deleteRequest.error);
            };
    
            cursor.continue();
          } else resolve();
        };
    
        request.onerror = (event) => {
          console.error("数据删除失败");
          reject(event.target.error);
        };
      });
    }
    
  3. index.html

    <!DOCTYPE html>
    <html>
      <!-- ... -->
      <body>
        <!-- ... -->
        <fieldset>
          <legend>删除数据</legend>
          <fieldset>
            <legend>根据主键删除</legend>
            <form>
              <label>主键:<input type="text" name="pk" /></label>
              <input
                type="button"
                value="删除"
                onclick="deleteSubmit(event, 'pk')"
              />
            </form>
          </fieldset>
          <fieldset>
            <legend>根据索引和游标删除</legend>
            <form>
              <label>姓名:<input type="text" name="name" /></label>
              <input
                type="button"
                value="删除"
                onclick="deleteSubmit(event, 'index-cursor')"
              />
            </form>
          </fieldset>
        </fieldset>
    
        <script>
          // ...
    
          // 删除表单数据
          async function deleteSubmit(event, type) {
            var form = event.target.form;
            switch (type) {
              case "pk":
                await deleteByPrimaryKey(db, USERS_STORE, form["pk"].value);
                break;
              case "index-cursor":
                await deleteByIndexAndCursor(db, USERS_STORE, {
                  name: "name",
                  value: form["name"].value,
                });
                break;
              default:
                alert("未知删除类型");
                break;
            }
          }
        </script>
      </body>
    </html>
    
    

0x04 删库与断开

(1)删除仓库

  1. script.js

    /**
     * 删除仓库
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @returns {Promise} 删除仓库结果
     */
    function deleteObjectStore(db, storeName) {
      return new Promise((resolve, reject) => {
        if (!storeName) reject("仓库名称不能为空");
    
        if (db.objectStoreNames.contains(storeName)) {
          const deleteRequest = db.deleteObjectStore(storeName);
    
          deleteRequest.onsuccess = () => {
            console.log("仓库删除成功");
            resolve();
          };
    
          deleteRequest.onerror = (event) => {
            console.error("仓库删除失败");
            reject(event.target.error);
          };
        } else {
          reject("仓库不存在");
        }
      });
    }
    
  2. index.html

    <!DOCTYPE html>
    <html>
      <!-- ... -->
      <body>
        <!-- ... -->
        <button onclick="deleteDatabaseSubmit()">删除仓库</button>
    
        <script>
          const DATABASE_NAME = "myDB",
            USERS_STORE = "users";
          let db = null;
    
          // ...
    
          // 删除仓库
          async function deleteObjectStoreSubmit() {
            db = await connect(DATABASE_NAME, 2, async (database) => {
              await deleteObjectStore(database, USERS_STORE);
            });
          }
        </script>
      </body>
    </html>
    

(2)删除数据库

  1. script.js

    /**
     * 删除数据库
     * @param {string} dbName 数据库名称
     * @returns {Promise} 删除数据库结果
     */
    function deleteDatabase(dbName) {
      return new Promise((resolve, reject) => {
        if (!dbName) reject("请输入数据库名称");
        if (!currentIndexedDB) reject("请先初始化数据库");
    
        const request = currentIndexedDB.deleteDatabase(dbName);
    
        request.onsuccess = () => {
          console.log("数据库删除成功");
          resolve();
        };
    
        request.onerror = (event) => {
          console.error("数据库删除失败");
          reject(event.target.error);
        };
      });
    }
    
  2. index.html

    <!DOCTYPE html>
    <html>
      <!-- ... -->
      <body>
        <!-- ... -->
        <button onclick="deleteDatabaseSubmit()">删除数据库</button>
    
        <script>
          const DATABASE_NAME = "myDB",
            USERS_STORE = "users";
          let db = null;
    
          // ...
    
          // 删除数据库
          async function deleteDatabaseSubmit() {
            try {
              await deleteDatabase(DATABASE_NAME);
              db = null;
            } catch (error) {}
          }
        </script>
      </body>
    </html>
    

(3)断开连接

  1. script.js

    /**
     * 关闭数据库连接
     * @param {IDBDatabase} db 数据库对象
     */
    function disconnect(db) {
      if (!db) return;
      db.close();
      console.log("数据库已断开连接");
    }
    
  2. index.html

    <!DOCTYPE html>
    <html>
      <!-- ... -->
      <body>
        <!-- ... -->
        <button onclick="disconnectSubmit()">断开连接</button>
    
        <script>
          const USERS_STORE = "users";
          let db = null;
    
          // ...
    
          // 断开连接
          function disconnectSubmit() {
            disconnect();
            db = null;
          }
        </script>
      </body>
    </html>
    

完整代码

  1. script.js

    // 浏览器兼容性判断
    const currentIndexedDB =
      window.indexedDB || // IE9+
      window.mozIndexedDB || // Firefox
      window.webkitIndexedDB || // Chrome
      window.msIndexedDB; // Safari
    
    /**
     * 创建或连接数据库
     * @param {string} dbName 数据库名称
     * @param {number} version 数据库版本
     * @param {function} upgradeCallback 数据库更新回调
     * @returns {Promise} 数据库实例
     */
    function connect(dbName, version = 1, upgradeCallback = () => {}) {
      return new Promise((resolve, reject) => {
        if (!dbName) reject("请传入数据库名称");
    
        // 打开数据库,没有则创建
        const request = currentIndexedDB.open(dbName, version);
        let db;
    
        // 数据库连接成功回调
        request.onsuccess = (event) => {
          db = event.target.result;
          console.log("数据库连接成功");
          resolve(db);
        };
    
        // 数据库连接失败回调
        request.onerror = (event) => {
          console.error("数据库连接失败");
          reject(event.target.error);
        };
    
        // 数据库更新回调
        request.onupgradeneeded = (event) => {
          db = event.target.result;
          console.log("数据库更新");
          resolve(upgradeCallback(db));
        };
      });
    }
    
    /**
     * 创建仓库
     * @param {IDBDatabase} db 数据库实例
     * @param {string} tableName 仓库名
     * @param {object} options 仓库结构
     * @param {array} index 索引字符串数组
     * @returns {Promise} 仓库实例
     */
    function createObjectStore(db, storeName, options, index = []) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!options) reject("请传入仓库结构");
    
        // 创建仓库
        const objectStore = db.createObjectStore(storeName, options);
    
        // 创建索引
        index.forEach((item) =>
          objectStore.createIndex(item, item, { unique: false })
        );
    
        // 仓库创建成功回调
        objectStore.oncomplete = (event) => {
          console.log("仓库创建成功");
          resolve(objectStore);
        };
    
        // 仓库创建失败回调
        objectStore.onerror = (event) => {
          console.error("仓库创建失败");
          reject(event.target.error);
        };
      });
    }
    
    /**
     * 新增数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {object} data 待新增数据
     * @returns {Promise} 新增数据结果
     */
    function insert(db, storeName, data) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!data) reject("请传入数据");
    
        const request = db
          .transaction(storeName, "readwrite") // 创建事务,读写
          .objectStore(storeName) // 获取仓库
          .add(data); // 添加数据
    
        request.onsuccess = () => {
          console.log("数据添加成功");
          resolve();
        };
    
        request.onerror = (event) => {
          console.error("数据添加失败");
          reject(event.target.error);
        };
      });
    }
    
    /**
     * 根据主键获取数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {string} key 主键
     * @returns {Promise} 获取数据结果
     */
    function queryByPrimaryKey(db, storeName, key) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!key) reject("请传入主键");
    
        const request = db
          .transaction(storeName, "readonly") // 创建事务,只读
          .objectStore(storeName) // 获取仓库
          .get(key);
    
        request.onsuccess = (event) => {
          console.log("数据获取成功");
          resolve(event.target.result);
        };
    
        request.onerror = (event) => {
          console.error("数据获取失败");
          reject(event.target.error);
        };
      });
    }
    
    /**
     * 根据游标获取数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @returns {Promise} 获取数据结果
     */
    function queryByCursor(db, storeName) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
    
        // 存储结果
        const result = [];
    
        const request = db
          .transaction(storeName, "readonly") // 创建事务,只读
          .objectStore(storeName) // 获取仓库
          .openCursor(); // 获取游标
    
        request.onsuccess = (event) => {
          console.log("数据获取成功");
          let cursor = event.target.result;
          if (cursor) {
            result.push(cursor.value);
            cursor.continue();
          } else resolve(result);
        };
    
        request.onerror = (event) => {
          console.error("数据获取失败");
          reject(event.target.error);
        };
      });
    }
    
    /**
     * 根据索引获取数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {{name, value}} indexObject 索引对象
     * @returns {Promise} 获取数据结果
     */
    function queryByIndex(db, storeName, indexObject) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!indexObject) reject("请传入索引对象");
        else if (!indexObject.name) reject("请传入索引名称");
        else if (!indexObject.value) reject("请传入索引值");
    
        const request = db
          .transaction(storeName, "readonly") // 创建事务,只读
          .objectStore(storeName) // 获取仓库
          .index(indexObject.name) // 获取索引
          .get(indexObject.value); // 获取数据
    
        request.onsuccess = (event) => {
          console.log("数据获取成功");
          resolve(event.target.result);
        };
    
        request.onerror = (event) => {
          console.error("数据获取失败");
          reject(event.target.error);
        };
      });
    }
    
    /**
     * 根据索引和游标获取数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {{name, value}} indexObject 索引对象
     * @returns {Promise} 获取数据结果
     */
    function queryByIndexAndCursor(db, storeName, indexObject) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!indexObject) reject("请传入索引对象");
        else if (!indexObject.name) reject("请传入索引名称");
        else if (!indexObject.value) reject("请传入索引值");
    
        // 存储结果
        const result = [];
    
        const request = db
          .transaction(storeName, "readonly") // 创建事务,只读
          .objectStore(storeName) // 获取仓库
          .index(indexObject.name) // 获取索引
          .openCursor(IDBKeyRange.only(indexObject.value)); // 获取游标
    
        request.onsuccess = (event) => {
          console.log("数据获取成功");
          let cursor = event.target.result;
          if (cursor) {
            result.push(cursor.value);
            cursor.continue();
          } else resolve(result);
        };
    
        request.onerror = (event) => {
          console.error("数据获取失败");
          reject(event.target.error);
        };
      });
    }
    
    /**
     * 根据索引和游标并分页获取数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {{name, value}} indexObject 索引对象
     * @param {{number, size}} pageObject 分页对象
     * @returns {Promise} 获取数据结果
     */
    function queryByIndexAndCursorAndPagination(
      db,
      storeName,
      indexObject,
      pageObject
    ) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!indexObject) reject("请传入索引对象");
        else if (!indexObject.name) reject("请传入索引名称");
        else if (!indexObject.value) reject("请传入索引值");
        if (!pageObject) reject("请传入分页对象");
        else if (!pageObject.number) reject("请传入页码");
        else if (pageObject.number < 1) reject("页码不能小于 1");
        else if (!pageObject.size) reject("请传入页大小");
        else if (pageObject.size < 1) reject("页大小不能小于 1");
    
        // 存储结果
        const result = [];
        let counter = 0, // 计数器
          advanced = true; // 是否已跳过指定页码
    
        const request = db
          .transaction(storeName, "readonly") // 创建事务,只读
          .objectStore(storeName) // 获取仓库
          .index(indexObject.name) // 获取索引
          .openCursor(IDBKeyRange.only(indexObject.value)); // 获取游标
    
        request.onsuccess = (event) => {
          console.log("数据获取成功");
          let cursor = event.target.result;
    
          // 跳过指定页码
          if (pageObject.number > 1 && advanced) {
            advanced = false;
            cursor.advance((pageObject.number - 1) * pageObject.size);
            return;
          }
    
          if (cursor) {
            result.push(cursor.value);
            counter++;
            if (counter < pageObject.size) cursor.continue(); // 继续获取下一条数据
            else resolve(result);
          } else resolve(result);
        };
    
        request.onerror = (event) => {
          console.error("数据获取失败");
          reject(event.target.error);
        };
      });
    }
    
    /**
     * 修改数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {object} data 待修改数据
     * @returns {Promise} 修改数据结果
     */
    function update(db, storeName, data) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!data) reject("请传入数据");
        else if (!data.id) reject("请传入数据 ID");
    
        const request = db
          .transaction(storeName, "readwrite") // 创建事务,读写
          .objectStore(storeName) // 获取仓库
          .put(data); // 修改数据
    
        request.onsuccess = () => {
          console.log("数据修改成功");
          resolve();
        };
    
        request.onerror = (event) => {
          console.error("数据修改失败");
          reject(event.target.error);
        };
      });
    }
    
    /**
     * 根据主键删除数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {string} key 主键
     * @returns {Promise} 获取数据结果
     */
    function deleteByPrimaryKey(db, storeName, key) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!key) reject("请传入主键");
    
        const request = db
          .transaction(storeName, "readwrite") // 创建事务,读写
          .objectStore(storeName) // 获取仓库
          .delete(key); // 删除数据
    
        request.onsuccess = (event) => {
          console.log("数据删除成功");
          resolve(event.target.result);
        };
    
        request.onerror = (event) => {
          console.error("数据删除失败");
          reject(event.target.error);
        };
      });
    }
    
    /**
     * 根据索引和游标删除数据
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @param {{name, value}} indexObject 索引对象
     * @returns {Promise} 删除数据结果
     */
    function deleteByIndexAndCursor(db, storeName, indexObject) {
      return new Promise((resolve, reject) => {
        if (!db) reject("请先连接数据库");
        if (!storeName) reject("请传入仓库名称");
        if (!indexObject) reject("请传入索引对象");
        else if (!indexObject.name) reject("请传入索引名称");
        else if (!indexObject.value) reject("请传入索引值");
    
        const request = db
          .transaction(storeName, "readwrite") // 创建事务,读写
          .objectStore(storeName) // 获取仓库
          .index(indexObject.name) // 获取索引
          .openCursor(IDBKeyRange.only(indexObject.value)); // 获取游标
    
        request.onsuccess = (event) => {
          let cursor = event.target.result;
          if (cursor) {
            const deleteRequest = cursor.delete();
    
            deleteRequest.onsuccess = () => {
              console.log("数据删除成功");
            };
    
            deleteRequest.onerror = () => {
              console.error("数据删除失败");
              reject(deleteRequest.error);
            };
    
            cursor.continue();
          } else resolve();
        };
    
        request.onerror = (event) => {
          console.error("数据删除失败");
          reject(event.target.error);
        };
      });
    }
    
    /**
     * 删除仓库
     * @param {IDBDatabase} db 数据库对象
     * @param {string} storeName 仓库名称
     * @returns {Promise} 删除仓库结果
     */
    function deleteObjectStore(db, storeName) {
      return new Promise((resolve, reject) => {
        if (!storeName) reject("仓库名称不能为空");
    
        if (db.objectStoreNames.contains(storeName)) {
          const deleteRequest = db.deleteObjectStore(storeName);
    
          deleteRequest.onsuccess = () => {
            console.log("仓库删除成功");
            resolve();
          };
    
          deleteRequest.onerror = (event) => {
            console.error("仓库删除失败");
            reject(event.target.error);
          };
        } else {
          reject("仓库不存在");
        }
      });
    }
    
    /**
     * 删除数据库
     * @param {string} dbName 数据库名称
     * @returns {Promise} 删除数据库结果
     */
    function deleteDatabase(dbName) {
      return new Promise((resolve, reject) => {
        if (!dbName) reject("请输入数据库名称");
        if (!currentIndexedDB) reject("请先初始化数据库");
    
        const request = currentIndexedDB.deleteDatabase(dbName);
    
        request.onsuccess = () => {
          console.log("数据库删除成功");
          resolve();
        };
    
        request.onerror = (event) => {
          console.error("数据库删除失败");
          reject(event.target.error);
        };
      });
    }
    
    /**
     * 关闭数据库连接
     * @param {IDBDatabase} db 数据库对象
     */
    function disconnect(db) {
      if (!db) return;
      db.close();
      console.log("数据库已断开连接");
    }
    
    
  2. index.html

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="UTF-8" />
        <title>IndexedDB</title>
        <script src="./script.js"></script>
      </head>
      <body>
        <button onclick="init()">初始化数据库</button>
        <fieldset>
          <legend>新增数据</legend>
          <form>
            <label>姓名:<input type="text" name="name" /></label>
            <label>邮箱:<input type="text" name="email" /></label>
            <input type="button" value="保存" onclick="insertSubmit(event)" />
          </form>
        </fieldset>
        <fieldset>
          <legend>查询数据</legend>
          <fieldset>
            <legend>根据主键查询</legend>
            <form>
              <label>主键:<input type="text" name="pk" /></label>
              <input
                type="button"
                value="查询"
                onclick="querySubmit(event, 'pk')"
              />
            </form>
          </fieldset>
          <fieldset>
            <legend>根据游标查询</legend>
            <form>
              <input
                type="button"
                value="查询所有"
                onclick="querySubmit(event, 'cursor')"
              />
            </form>
          </fieldset>
          <fieldset>
            <legend>根据索引查询</legend>
            <form>
              <label>姓名:<input type="text" name="name" /></label>
              <input
                type="button"
                value="查询"
                onclick="querySubmit(event, 'index')"
              />
            </form>
          </fieldset>
          <fieldset>
            <legend>根据索引和游标查询</legend>
            <form>
              <label>姓名:<input type="text" name="name" /></label>
              <input
                type="button"
                value="查询"
                onclick="querySubmit(event, 'index-cursor')"
              />
            </form>
          </fieldset>
          <fieldset>
            <legend>根据索引和游标并分页查询</legend>
            <form>
              <label>
                邮箱:
                <input type="text" name="email" />
              </label>
              <br />
              <label>
                页码:
                <input type="number" min="1" name="page-number" />
              </label>
              <label>
                每页数量:<input type="number" min="1" name="page-size" />
              </label>
              <input
                type="button"
                value="查询"
                onclick="querySubmit(event, 'index-cursor-pagination')"
              />
            </form>
          </fieldset>
    
          <fieldset>
            <legend>查询结果</legend>
            <ul id="result"></ul>
          </fieldset>
        </fieldset>
        <fieldset>
          <legend>修改数据</legend>
          <form>
            <label>主键:<input type="text" name="pk" /></label>
            <label>姓名:<input type="text" name="name" /></label>
            <label>邮箱:<input type="text" name="email" /></label>
            <input type="button" value="保存" onclick="updateSubmit(event)" />
          </form>
        </fieldset>
        <fieldset>
          <legend>删除数据</legend>
          <fieldset>
            <legend>根据主键删除</legend>
            <form>
              <label>主键:<input type="text" name="pk" /></label>
              <input
                type="button"
                value="删除"
                onclick="deleteSubmit(event, 'pk')"
              />
            </form>
          </fieldset>
          <fieldset>
            <legend>根据索引和游标删除</legend>
            <form>
              <label>姓名:<input type="text" name="name" /></label>
              <input
                type="button"
                value="删除"
                onclick="deleteSubmit(event, 'index-cursor')"
              />
            </form>
          </fieldset>
        </fieldset>
        <button onclick="deleteObjectStoreSubmit()">删除仓库</button>
        <button onclick="deleteDatabaseSubmit()">删除数据库</button>
        <button onclick="disconnectSubmit()">断开连接</button>
    
        <script>
          const DATABASE_NAME = "myDB",
            USERS_STORE = "users";
          let db = null;
    
          // 生成 uuid
          function uuidv4() {
            return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
              var r = (Math.random() * 16) | 0,
                v = c === "x" ? r : (r & 0x3) | 0x8;
              return v.toString(16);
            });
          }
    
          // 初始化数据库
          async function init() {
            db = await connect(DATABASE_NAME, 1, async (database) => {
              await createObjectStore(database, USERS_STORE, { keyPath: "id" }, [
                "name",
                "email",
              ]);
            });
          }
    
          // 保存表单数据到数据库
          async function insertSubmit(event) {
            var form = event.target.form;
            var id = await insert(db, USERS_STORE, {
              id: uuidv4(),
              name: form.name.value,
              email: form.email.value,
            });
          }
    
          // 查询表单数据
          async function querySubmit(event, type) {
            var form = event.target.form;
            var result = document.getElementById("result");
            result.innerHTML = "";
            var data = [];
            switch (type) {
              case "pk":
                data = [await queryByPrimaryKey(db, USERS_STORE, form["pk"].value)];
                break;
              case "cursor":
                data = await queryByCursor(db, USERS_STORE);
                break;
              case "index":
                data = [
                  await queryByIndex(db, USERS_STORE, {
                    name: "name",
                    value: form["name"].value,
                  }),
                ];
                break;
              case "index-cursor":
                data = await queryByIndexAndCursor(db, USERS_STORE, {
                  name: "name",
                  value: form["name"].value,
                });
                break;
              case "index-cursor-pagination":
                data = await queryByIndexAndCursorAndPagination(
                  db,
                  USERS_STORE,
                  {
                    name: "email",
                    value: form["email"].value,
                  },
                  {
                    number: parseInt(form["page-number"].value),
                    size: parseInt(form["page-size"].value),
                  }
                );
                break;
              default:
                alert("未知查询类型");
                break;
            }
    
            // 创建结果列表
            const fragment = document.createDocumentFragment();
            if (data.length > 0)
              data.forEach((item) => {
                const li = document.createElement("li");
                li.textContent = item.name + " " + item.email;
                fragment.appendChild(li);
              });
            result.appendChild(fragment);
          }
    
          // 修改表单数据
          async function updateSubmit(event) {
            var form = event.target.form;
            await update(db, USERS_STORE, {
              id: form.pk.value,
              name: form.name.value,
              email: form.email.value,
            });
          }
    
          // 删除表单数据
          async function deleteSubmit(event, type) {
            var form = event.target.form;
            switch (type) {
              case "pk":
                await deleteByPrimaryKey(db, USERS_STORE, form["pk"].value);
                break;
              case "index-cursor":
                await deleteByIndexAndCursor(db, USERS_STORE, {
                  name: "name",
                  value: form["name"].value,
                });
                break;
              default:
                alert("未知删除类型");
                break;
            }
          }
    
          // 删除仓库
          async function deleteObjectStoreSubmit() {
            db = await connect(DATABASE_NAME, 2, async (database) => {
              await deleteObjectStore(database, USERS_STORE);
            });
          }
    
          // 删除数据库
          async function deleteDatabaseSubmit() {
            try {
              await deleteDatabase(DATABASE_NAME);
              db = null;
            } catch (error) {}
          }
    
          // 断开连接
          function disconnectSubmit() {
            disconnect();
            db = null;
          }
        </script>
      </body>
    </html>
    
    

-End-

posted @ 2025-06-01 00:50  SRIGT  阅读(34)  评论(0)    收藏  举报