数据存储(三)WebStorage 之 IndexedDB

参考:《JavaScript 高级程序设计》(第3版)

扩展:

数据存储(一)Cookie_不头秃的码农的博客-CSDN博客_cookie存数据

数据存储(二)WebStorage 之 sessionStorage、globalStorage、localStorage_不头秃的码农的博客-CSDN博客

Indexed Database API,简称为 IndexedDB,是在浏览器中保存结构化数据的一种数据库,支持保存、读取、查询、搜索等。

替代了已被废弃的 Web SQL Database API。

================ 使用步骤 ================

1. 建立与数据库的连接

indexedDB 是一个数据库,使用对象保存数据(不是用表来保存)。一个 indexedDB 数据库,就是一组位于相同命名空间下的对象的集合。

  • 使用 window.indexedDB.open(dbName, version) 打开数据库。
  • 使用 db.close() 关闭数据库、断开数据库连接。db代表数据库实例对象。
  • 使用 window.indexedDB.deleteDatabase(dbName) 删除数据库。(只有在数据库断开连接的情况下才能删除)
// 打开名为 JackDB 的数据库
let request = indexedDB.open("JackDB", 1);

/**
 * onupgradeneeded 当创建一个新的数据库或者增加已存在的数据库的版本号,onupgradeneeded 事件会被触发
 * onupgradeneeded 是唯一可以修改数据库结构的地方
 */
request.onupgradeneeded = function (event) {
  // todo something
};
request.onsuccess = function (event) {
  // event.target 是 IDBRequest 实例,指向request对象
  // event.target.result 指向数据库实例对象

  // todo something
};
request.onerror = function (event) {
  console.log("数据库打开失败:", event.target.errorCode);
};

2. 创建对象存储空间

因仅能在 onupgradeneeded 事件修改数据库结构,所以仅能在此事件里创建存储空间。

创建对象存储空间时,需要指定一个唯一的键。

  • 可以通过 keyPath 指定主键,
  • 可以通过 autoIncrement: true 设置自增键
request.onupgradeneeded = function (event) {
  // event.target 是 IDBRequest 实例,指向 request 对象
  db = event.target.result; // event.target.result 是数据库实例对象
  let store;
  if (!db.objectStoreNames.contains("users")) {
    // 创建名为 users 的存储空间
    // store = db.createObjectStore("users", { autoIncrement: true }); // autoIncrement 自增主键
    store = db.createObjectStore("users", { keyPath: "name" }); // keyPath 主键字段名
  }
};

3. 数据的基本CRUD

3.1 新增数据

/**
 * 写入数据
 * @param {IDBDatabase} db 数据库实例
 * @param {String | Array<string>} dbName 数据库名
 * @param {String} storeName objectStore名
 * @param {*} data 待新增的数据
 */
function add(db, dbName, storeName, data) {
  let request = db
    .transaction(dbName, "readwrite") // 新建事务
    .objectStore(storeName) // 拿到 IDBObjectStore 对象
    .add(data);
  request.onsuccess = (event) => {
    console.log('写入数据成功',event.target.result)
  };
  request.onerror = (event) => {
    console.log('写入数据失败', event.target.error);
  };
  request.onabort = (event) => {
   console.log('事务回滚', event.target);
  };
}

3.2 修改数据

/**
 * 根据主键修改数据(若无此主键,则新增数据)
 * @param {IDBDatabase} db 数据库实例
 * @param {String | Array<string>} dbName 数据库名
 * @param {String} storeName objectStore名
 * @param {*} key 主键
 */
function update(db, dbName, storeName, data) {
  let request = db
    .transaction(dbName, "readwrite") // 新建事务
    .objectStore(storeName) // 拿到 IDBObjectStore 对象
    .put(data);
  request.onsuccess = (event) => {
    console.log('修改数据成功',event.target.result)
  };
  request.onerror = (event) => {
    console.log('修改数据失败', event.target.error);
  };
  request.onabort = (event) => {
   console.log('事务回滚', event.target);
  };
}

3.3 查询数据

/**
 * 根据主键读取数据
 * @param {IDBDatabase} db 数据库实例
 * @param {String | Array<string>} dbName 数据库名
 * @param {String} storeName objectStore名
 * @param {*} key 主键
 */
function read(db, dbName, storeName, key) {
  let request = db
    .transaction(dbName, "readwrite") // 新建事务
    .objectStore(storeName) // 拿到 IDBObjectStore 对象
    .get(key);
  request.onsuccess = (event) => {
    console.log('读取数据成功',event.target.result)
  };
  request.onerror = (event) => {
    console.log('读取数据失败', event.target.error);
  };
  request.onabort = (event) => {
   console.log('事务回滚', event.target);
  };
}

3.4 删除数据

/**
 * 根据主键删除数据
 * @param {IDBDatabase} db 数据库实例
 * @param {String | Array<string>} dbName 数据库名
 * @param {String} storeName objectStore名
 * @param {*} key 主键
 */
 function remove(db, dbName, storeName, key) {
  let request = db
    .transaction(dbName, "readwrite") // 新建事务
    .objectStore(storeName) // 拿到 IDBObjectStore 对象
    .delete(key);
  request.onsuccess = (event) => {
    console.log("删除数据成功", event.target.result);
  };
  request.onerror = (event) => {
    console.log("删除数据失败", event.target.error);
  };
  request.onabort = (event) => {
    console.log("事务回滚", event.target);
  };
}

3.5 清空数据

/**
 * 清空所有数据
 * @param {IDBDatabase} db 数据库实例
 * @param {String | Array<string>} dbName 数据库名
 * @param {String} storeName objectStore名
 */
 function read(db, dbName, storeName) {
  let request = db
    .transaction(dbName, "readwrite") // 新建事务
    .objectStore(storeName) // 拿到 IDBObjectStore 对象
    .clear();
  request.onsuccess = (event) => {
    console.log("清空数据成功", event.target.result);
  };
  request.onerror = (event) => {
    console.log("清空数据失败", event.target.error);
  };
  request.onabort = (event) => {
    console.log("事务回滚", event.target);
  };
}

4. 游标

/**
 * cursor 游标
 * @desc 指向结果集的指针
 * 游标工作原理:游标指针先指向结果中的第一项,在接到查找下一项的命令时,才会指向下一项。(不提前收集结果)
 * 创建游标:在存储空间上调用openCursor(range, direction)
 * @param {IDBKeyRound} range 键范围,默认范围为全部
 * @param {String} direction 方向
 *   next 下一个(默认)
 *   nextunique 下一个不重复项
 *   prev 前一项
 *   prevunique 前一个不重复项
 * @return 会返回一个请求对象,可以指定onsuccess()/onerror()事件处理程序。
 *   onsuccess()可以通过event.target.result取得结果中的下一项。
 *     有下一项时:event.target.result 是 IDBCursorWithValue的实例
 *     无下一项时:event.target.result 是 null
 * 游标查找下一项指令:continue(key), advance(count)
 *   continue(key) 移动到结果集中的下一项。参数key(主键)可选。
 *     不指定key,游标移动到下一项;
 *     指定key,游标移动到指定键。
 *   advance(count) 向前移动count指定项数。
 */

 游标的使用示例,放在最后辣 ~

5. 键范围 

/**
 * keyRange 键范围
 * @desc 利用游标查找数据太局限,键范围可灵活的查找数据(实例:IDBKeyRange)
 * @兼容 var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
 * @func 四种定义键的方式,最后一种功能包含前三种
 *   1. only(key) // 只取得键为key的对象,类似于objectStore().get(key)
 *   2. lowerBound(startKey, isStartNext) // 指定游标开始的位置,默认isStartNext为false
 *        isStartNext = false  =>  从startKey开始一直查找到最后
 *        isStartNext = true  =>  从startKey的下一个对象开始一直查找到最后
 *   3. upperBound(endKey, isEndNext) // 指定游标结束的位置,默认isEndNext=false
 *        isEndNext = false  => 从头查找到endKey的位置
 *        isEndNext = true  =>  从头查找到endKey的前一个对象为止
 *   4. bound(startKey, endKey, isStartNext, isEndNext) // 指定游标开始和结束的位置,默认isStartNext=false, isEndNext=false
 *        isStartNext = false  => 从startKey开始查找
 *        isStartNext = true  => 从startKey的下一个对象开始查找
 *        isEndNext = false  => 到endKey结束
 *        isEndNext = true  => 到endKey的上一个对象结束
 */

  键范围的使用示例,放在最后辣 ~

6. 索引

/**
 * 索引
 * @desc 类似对象存储空间。(为一个存储对象空间,指定多个键)
 * @意义 为提高查询速度而基于特定属性创建的。
 * 例如:通过用户id和用户名两种方式存储数据,就需要通过这两个键来存储记录。用户id作为主键,在为用户名创建索引。
 *
 * @func 创建索引 creatIndex(indexName, indexAttrName, options)
 * @param indexName 索引名
 * @param indexAttrName 索引属性名
 * @param options 包含unique的配置项
 * @return 返回IDBIndex实例
 *
 * @func 获取某个索引 index(indexName)
 * @param indexName 索引名
 * 在索引上,调用 openCursor() 来创建游标。
 *   此时,event.target.result.key 存储的是索引键;
 *        event.target.result.primaryKey 存储的是主键;
 *        event.target.result.value 存储的是整个对象。
 * 在索引上,调用 openKeyCursor() 来创建一个特殊的,只返回每条记录主键的游标。
 *   此时,event.target.result.key 存储的是索引键;
 *        event.target.result.primaryKey 存储的是主键;
 *        无 event.target.result.value。
 * 在索引上,调用 get(indexKey) 来获取整个对象。
 *   此时,event.target.result 即是这个对象。
 * 在索引上,调用 getKey(indexKey) 来通过索引键获取主键。
 *   此时,event.target.result 即是这个主键。
 *
 * @func 获取某个存储空间下的所有索引 indexNames属性
 * @return 返回 DOMStringList 实例,即包含某个存储空间下所有索引名的对象
 *
 * @func 删除某个索引 deleteIndex(indexName)
 * @return 无任何返回,因为删除索引不影响存储空间,所以无任何返回。
 */

 索引的使用示例,放在最后辣 ~

7. 并发问题

/**
 * 并发问题
 * @desc IndexedDB提供的API是异步的,但是仍然存储并发问题。
 * @eg 例如:浏览器两个不同的标签打开同一页面,那么一个页面试图更新另一个页面尚未准备就绪的数据库,有可能引起并发问题
 * @eg 例如:把数据库设置为新版,也有可能引发并发问题(只有当浏览器仅有一个标签页打开使用数据库时,调用setVersion()才能完成)
 *     解决法一:每次成功打开数据库,都指定onversionchange事件处理程序。
 *             当同一个来源的另一个内标签页调用setVersion()时,会执行这个函数,在函数内部立即关掉数据库即可。
 *             关闭数据路方法:db.close()
 *     解决法二:调用setVersion()时,指定请求的onblocked()事件处理程序。
 *             当你想要更新数据库版本但另一个标签页已经打开数据库的情况下,就会触发这个事件。
 *             此时可通知用户关闭其他标签页,再重新调用setVersion()。
 */

8. 限制

/**
 * 限制
 * @desc 与Web Storage类似。
 * @限制1 IndexedDB只能由同源页面操作,不能跨域共享信息。
 * @限制2 每个数据库占用磁盘空间有限。
 *         FireFox 4+ 的上限是每个源 50M,其他几乎都是5M
 * @限制3 FireFox 不允许本地文件访问IndexedDB,Chrome无限制。
 */

9. 封装操作数据库的工具类

/**
 * IndexedDB数据库操作方法类
 */
class IDBUtils {
  constructor(db, dbName, store) {
    this.db = db; // {IDBDatabase} 已被打开的数据库
    this.dbName = dbName; // {String | Array<string>} 存储空间名
    this.store = store; // {String} 存储空间名
    this.type = {
      success: "SUCCESS", // 操作成功
      error: "ERROR", // 操作失败
      done: "DONE", // 检索结束
      abort: "ABORT", // 操作回滚
    };
  }

  /**
   * 新增数据
   * @param {*} data 要写入的数据
   * @param {Function} cb 回调函数
   */
  add(data, cb) {
    let request = this.db
      .transaction(this.dbName, "readwrite") // 新建事务
      .objectStore(this.store) // 拿到 IDBObjectStore 对象
      .add(data);
    request.onsuccess = (event) => {
      if (cb) cb(this.type.success, event.target.result);
    };
    request.onerror = (event) => {
      if (cb) cb(this.type.error, event.target.error);
    };
    request.onabort = (event) => {
      if (cb) cb(this.type.abort, event.target);
    };
  }

  /**
   * 根据主键,读取数据
   * @param {String | Number} key 主键
   * @param {Function} cb 回调函数
   */
  read(key, cb) {
    let request = this.db
      .transaction(this.dbName, "readonly") // 新建事务
      .objectStore(this.store)
      .get(key);
    request.onsuccess = (event) => {
      if (cb) cb(this.type.success, event.target.result);
    };
    request.onerror = (event) => {
      if (cb) cb(this.type.error, event.target.error);
    };
    request.onabort = (event) => {
      if (cb) cb(this.type.abort, event.target);
    };
  }

  /**
   * 根据主键要么更新,要么插入
   * @param {*} param 一条完整的信息
   * @param {Function} cb 回调函数
   */
  update(param, cb) {
    let request = this.db
      .transaction(this.dbName, "readwrite")
      .objectStore(this.store)
      .put(param);
    request.onsuccess = (event) => {
      if (cb) cb(this.type.success, event.target.result);
    };
    request.onerror = (event) => {
      if (cb) cb(this.type.error, event.target.error);
    };
    request.onabort = (event) => {
      if (cb) cb(this.type.abort, event.target);
    };
  }

  /**
   * 删除数据
   * @param {key} key 主键
   * @param {Function} cb 回调函数
   */
  remove(key, cb) {
    let request = this.db
      .transaction(this.dbName, "readwrite")
      .objectStore(this.store)
      .delete(key);
    request.onsuccess = () => {
      if (cb) cb(this.type.success);
    };
    request.onerror = () => {
      if (cb) cb(this.type.error);
    };
    request.onabort = (event) => {
      if (cb) cb(this.type.abort, event.target);
    };
  }

  /**
   * 清空数据
   * @param {Function} cb 回调函数
   */
  clear(cb) {
    let request = this.db
      .transaction(this.dbName, "readwrite")
      .objectStore(this.store)
      .clear();
    request.onsuccess = () => {
      if (cb) cb(this.type.success);
    };
    request.onerror = (event) => {
      if (cb) cb(this.type.error, event.target);
    };
    request.onabort = (event) => {
      if (cb) cb(this.type.abort, event.target);
    };
  }

  /**
   * 关闭数据库
   */
  closeDB() {
    this.db.close();
  }

  /**
   * 删除数据库
   * @条件 在数据库没被打开的情况下,可以删除
   */
  deleteDB() {
    this.db.close();

    let request = window.indexedDB.deleteDatabase(this.dbName);
    request.onsuccess = (event) => {
      console.log("success", event);
    };
    request.onerror = (event) => {
      console.log("error", event);
    };
  }

  // =================================================================
  // ============================== 游标 ==============================
  // =================================================================

  /**
   * 读取全部(游标)
   * @param {Function} cb 回调函数
   */
  readAll(direction, cb) {
    let request = this.db
      .transaction(this.dbName)
      .objectStore(this.store)
      .openCursor(null, direction);
    let res = [];
    request.onsuccess = (event) => {
      let cursor = event.target.result; // 也可以在索引上打开 objectStore.index("id").openCursor()
      // cursor 有值,代表有下一项; cursor = null,代表没有下一项
      if (cursor) {
        res.push({ key: cursor.key, value: cursor.value });
        cursor.continue(); // 继续查找下一项
      } else {
        if (cb) cb(this.type.success, res);
      }
    };
    request.onerror = (event) => {
      if (cb) cb(this.type.error, event.target);
    };
  }

  /**
   * 利用游标更新 cursor.update()
   * @param {*} key 主键
   * @param {*} data 待更新数据
   * @param {Function} cb 回调函数
   */
  cursorUpdate(key, data, cb) {
    let request = this.db
      .transaction(this.dbName)
      .objectStore(this.store)
      .openCursor();
    request.onsuccess = (event) => {
      let cursor = event.target.result;
      if (cursor) {
        if (cursor.key === key) {
          let updateRequest = cursor.update(data);
          updateRequest.onsuccess = (event) => {
            if (cb) cb(this.type.success, event.target);
          };
          updateRequest.onerror = (event) => {
            if (cb) cb(this.type.error, event.target);
          };
        } else {
          cursor.continue(); // 继续查找下一项
        }
      } else {
        if (cb) cb(this.type.done); // 检索结束
      }
    };
    request.onerror = (event) => {
      if (cb) cb(this.type.error, event.target);
    };
  }

  /**
   * 利用游标删除 cursor.delete()
   * @param {*} key 主键
   * @param {Function} cb 回调函数
   */
  cursorDelete(key, cb) {
    let request = this.db
      .transaction(this.dbName)
      .objectStore(this.store)
      .openCursor();
    request.onsuccess = (event) => {
      let cursor = event.target.result;
      if (cursor) {
        if (cursor.key === key) {
          let deleteRequest = cursor.delete();
          deleteRequest.onsuccess = () => {
            if (cb) cb(this.type.success);
          };
          deleteRequest.onerror = (event) => {
            if (cb) cb(this.type.error, event.target);
          };
        } else {
          cursor.continue(); // 继续查找下一项
        }
      } else {
        if (cb) cb(this.type.done); // 检索结束
      }
    };
    request.onerror = (event) => {
      if (cb) cb(this.type.error, event.target);
    };
  }

  // ==================================================================
  // ============================== 键范围 =============================
  // ==================================================================
  IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
  searchOnly(key, cb) {
    let range = IDBKeyRange.only(key);
    let request = this.db
      .transaction(this.dbName)
      .objectStore(this.store)
      .openCursor(range);
    request.onsuccess = (event) => {
      let cursor = event.target.result;
      if (cursor) {
        if (cb) cb({ key: cursor.key, value: cursor.value });
      } else {
        if (cb) cb(this.type.done);
      }
    };
    request.onerror = (event) => {
      if (cb) cb(this.type.error, event.target.error);
    };
  }
  searchLowerBound(key, isNext = false, cb) {
    let range = IDBKeyRange.lowerBound(key, isNext);
    let request = this.db
      .transaction(this.dbName)
      .objectStore(this.store)
      .openCursor(range, "prev");
    let res = [];
    request.onsuccess = (event) => {
      let cursor = event.target.result;
      if (cursor) {
        res.push({ key: cursor.key, value: cursor.value });
        cursor.continue(); // 继续查找下一项
      } else {
        if (cb) cb(this.type.success, res);
      }
    };
    request.onerror = (event) => {
      if (cb) cb(this.type.error, event.target.error);
    };
  }
  searchUpperBound(key, isPre = false, cb) {
    let range = IDBKeyRange.upperBound(key, isPre);
    let request = this.db
      .transaction(this.dbName)
      .objectStore(this.store)
      .openCursor(range);
    let res = [];
    request.onsuccess = (event) => {
      let cursor = event.target.result;
      if (cursor) {
        res.push({ key: cursor.key, value: cursor.value });
        cursor.continue(); // 继续查找下一项
      } else {
        if (cb) cb(this.type.success, res);
      }
    };
    request.onerror = (event) => {
      if (cb) cb(this.type.error, event.target.error);
    };
  }
  searchBound(startKey, endKey, isNext = false, isPre = false, cb) {
    let range = IDBKeyRange.bound(startKey, endKey, isNext, isPre);
    let request = this.db
      .transaction(this.dbName)
      .objectStore(this.store)
      .openCursor(range);
    let res = [];
    request.onsuccess = (event) => {
      let cursor = event.target.result;
      if (cursor) {
        res.push({ key: cursor.key, value: cursor.value });
        cursor.continue(); // 继续查找下一项
      } else {
        if (cb) cb(this.type.success, res);
      }
    };
    request.onerror = (event) => {
      if (cb) cb(this.type.error, event.target.error);
    };
  }

  // =================================================================
  // ============================== 索引 ==============================
  // =================================================================
  /**
   * 获取存储空间对象上的索引
   */
  getIndexNames() {
    let indexNameList = this.db
      .transaction(this.dbName, "readwrite") // 新建事务
      .objectStore(this.store).indexNames; // 拿到 IDBObjectStore 对象
    console.log("indexNameList", indexNameList);
  }
  /**
   * 通过索引键使用游标
   * @param {*} indexName 
   */
  getIndexCursor(indexName) {
    let index = this.db
      .transaction(this.dbName, "readwrite") // 新建事务
      .objectStore(this.store) // 拿到 IDBObjectStore 对象
      .index(indexName);
    let request = index.openCursor();
    request.onsuccess = (event) => {
      console.log("event", event.target.result);
      // 进行游标的使用、continue等逻辑
    };
  }

  /**
   * 通过索引键获取主键游标
   * @param {*} indexName 
   */
  getIndexKeyCursor(indexName) {
    let index = this.db
      .transaction(this.dbName, "readwrite") // 新建事务
      .objectStore(this.store) // 拿到 IDBObjectStore 对象
      .index(indexName);
    let request = index.openKeyCursor();
    request.onsuccess = (event) => {
      console.log("event", event.target.result);
      // 进行游标的使用、continue等逻辑
    };
  }

  /**
   * 通过索引键获取数据
   * @param {*} indexName 
   * @param {*} indexKey 
   */
  formIndexGet(indexName, indexKey) {
    let index = this.db
      .transaction(this.dbName, "readwrite") // 新建事务
      .objectStore(this.store) // 拿到 IDBObjectStore 对象
      .index(indexName);
    let request = index.get(indexKey);
    request.onsuccess = (event) => {
      console.log("event", event.target.result);
    };
  }

  /**
   * 通过索引键获取主键
   * @param {*} indexName 
   * @param {*} indexKey 
   */
  fromIndexGetKey(indexName, indexKey) {
    let index = this.db
      .transaction(this.dbName, "readwrite") // 新建事务
      .objectStore(this.store) // 拿到 IDBObjectStore 对象
      .index(indexName);
    let request = index.getKey(indexKey);
    request.onsuccess = (event) => {
      console.log("event", event.target.result);
    };
  }
}

索引、游标、键范围的基本使用如上,当然还可以利用 索引+游标+键范围来查询数据,大家有需要可以自己封装

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值