网上最全的indexedDB使用指南

4,446 阅读8分钟

indexedDB是什么

IndexedDB是一种在用户浏览器中持久存储数据的方法,属于key-value键值对事务模式的数据库。它允许您不考虑网络可用性,创建具有丰富查询能力的可离线 Web 应用程序。

只要不主动清除浏览器存储数据indexedDB就不会清除;
存储数据可达200M以上,相比session,cookie,localstorage可存数据量大很多;

应用范围

  1. IndexedDB 对于存储大量数据的应用程序(例如借阅库中的 DVD 目录)和不需要持久 Internet 连接的应用程序(例如邮件客户端、待办事项列表或记事本)很有用;
  2. 也可以用于区块链技术需要大量下载加解密数据、electron开发桌面应用程序、人工智能等需要大量存储数据等特殊场景;

特性

  • IndexedDB 数据库使用 key-value 键值对储存数据.
  • IndexedDB 是事务模式的数据库.
  • The IndexedDB API 基本上是异步的.
  • IndexedDB数据库“请求”无处不在 我们上边提到,数据库“请求”负责接受成功或失败的DOM事件
  • IndexedDB在结果准备好之后通过DOM事件通知用户 DOM事件总是有一个类型(type)属性(在IndexedDB中,该属性通常设置为success或error)
  • IndexedDB是面向对象的。indexedDB不是用二维表来表示集合的关系型数据库
  • indexedDB不使用结构化查询语言(SQL)
  • IndexedDB遵循同源(same-origin)策略 “源”指脚本所在文档URL的域名、应用层协议和端口。每一个“源”都有与其相关联的数据库。在同一个“源”内的所有数据库都有唯一、可区别的名称

问题

如果要实现如下类似关系型数据库的多条件查询

select * from image_colors where h>=230 and h<=234 and s>=0.222 and s<=0.225 and v>=0.666 and v<=0.668;

那么可以怎么实现呢?下文将会告诉您答案。

使用样例

直接上代码,代码有完整等注释说明:

新建数据库

let db;
exports.newDB = (upgrade) => {
  return new Promise((resolve, reject) => {
    let request = window.indexedDB.open('image_tool', 6);//第一个参数是字符串,表示数据库的名字。如果指定的数据库不存在,就会新建数据库。第二个参数是整数,表示数据库的版本
    request.onerror = (e) => {
      console.log('数据库打开报错');
      reject(e);
    };
    //第一次打开数据库时,会先触发upgradeneeded事件,然后触发success事件。
    request.onsuccess = (e) => {
      db = request.result;
      console.log('onsuccess','数据库打开成功');
      // addSourceLibRecord(); // 操作数据在这开始
      resolve(db);
    };
    // 如果指定的版本号,大于数据库的实际版本号,就会发生数据库升级事件upgradeneeded
    // 时通过事件对象的target.result属性,拿到数据库实例
    request.onupgradeneeded = (e) => {
      console.log('onupgradeneeded');
      db = e.target.result;
      !!upgrade&&upgrade();
      // creatSourceLibStore(); // 创建表必须在这开始
    };
  });
};

这里特别需要注意:创建表必须在onupgradeneeded里执行;第一次打开数据库时,会先触发upgradeneeded事件,然后触发success事件。

新建store

//新建数据库与打开数据库是同一个操作。如果指定的数据库不存在,就会新建。不同之处在于,后续的操作主要在upgradeneeded事件的监听函数里面完成,因为这时版本从无到有,所以会触发这个事件。
// 创建资源库表格
exports.creatSourceLibStore = () => {
  let objectStore;
  if (!db.objectStoreNames.contains('source_lib')) {
    // objectStore = db.createObjectStore('source_lib', { keyPath: 'name' }); // 主键是name属性
    objectStore = db.createObjectStore('source_lib', { autoIncrement: true });
    // { autoIncrement: true } 自动生成主键
    objectStore.createIndex('path', 'path', { unique: true });// 创建索引
    objectStore.createIndex('name', 'name', { unique: true });
    //三个参数分别为索引名称、索引所在的属性、配置对象
  }
};

下面我们创建一个图片表库,下文中的操作都会用到这个库来讲解:

function createImageColors() {
  let objectStore;
  if (!db.objectStoreNames.contains('image_colors')) {
    objectStore = db.createObjectStore('image_colors', { autoIncrement: true });
    objectStore.createIndex('name', 'name');
    objectStore.createIndex('path', 'path');
    objectStore.createIndex('h', 'h');
    objectStore.createIndex('s', 's');
    objectStore.createIndex('v', 'v');
    objectStore.createIndex('h,s,v',['h','s','v']);// 声明个组合索引,用于条件组合查询
  }
}
exports.createImageColors = createImageColors;

请留意组合索引的定义

添加数据

先封装一个函数:
需要开启一个事务才能对你的创建的数据库进行操作。事务来自于数据库对象,而且你必须指定你想让这个事务跨越哪些对象仓库,事务提供了三种模式:readonlyreadwriteversionchange

想要修改数据库模式或结构——包括新建或删除对象仓库或索引,只能在 versionchange 事务中才能实现,使用 readonlyreadwrite 模式都可以从已存在的对象仓库里读取记录。但只有在 readwrite 事务中才能修改对象仓库。

exports.addRecord = (table, record) => {
  return new Promise((resolve, reject) => {
    let request = db.transaction([table], 'readwrite')
      .objectStore(table)
      .add(record);

    request.onsuccess = function (event) {
      console.log('数据写入成功');
      resolve();
    };

    request.onerror = function (event) {
      console.log('数据写入失败');
      reject();
    };
  });
};

再调用函数插入一些测试数据

await addRecord('image_colors', {name:'a.jpg',path:'1/a.jpg',h:230,s:0.222,v:0.666});
await addRecord('image_colors', {name:'b.jpg',path:'1/b.jpg',h:232,s:0.224,v:0.668});
await addRecord('image_colors', {name:'c.jpg',path:'1/c.jpg',h:234,s:0.226,v:0.668});

索引查询

封一个函数先:

//增删查改都要通过事务来完成
//读取一个数据,通过索引来获取,查询结果满足多条数据时只拿第一个数据
exports.readOneRecordByIndex = (table, indexName, value) => {
  return new Promise((resolve, reject) => {
    let transaction = db.transaction([table]);
    let objectStore = transaction.objectStore(table);
    let index = objectStore.index(indexName); //索引的意义在于,可以让你搜索任意字段,也就是说从任意字段拿到数据记录
    let request = index.get(value);

    request.onerror = function(event) {
      console.log('readSourceLibRecord 事务失败');
      reject(event);
    };

    request.onsuccess = function(e) {
      if (e.target.result) {
        // console.log('name: ' + e.target.result.name);
        resolve(e.target.result);
      } else {
        console.log('readSourceLibRecord 未获得数据记录');
        resolve(null);
      }
    };
  });
};

调用查询:

// 多个记录的话,只取第一个记录
let imagesR = await readOneRecordByIndex('image_colors','v',0.668);
console.log('imageR',imagesR);

游标查询

游标有两种 openCursor和openKeyCursor,游标有两个参数

  • 第一个参数,遍历范围,指定游标的访问范围。该范围通过一个IDBKeyRange参数的方法来获取
  • 第二个参数,遍历顺序(IDBCursor的常量字符串)如下:
    • "next"光标显示所有记录,包括重复记录。它从键范围的下限开始向上移动(按键的顺序单调递增)
    • "nextunique"光标显示所有记录,不包括重复记录。如果存在具有相同键的多个记录,则仅检索迭代的第一个记录。它从键范围的下限开始向上移动
    • "prev"光标显示所有记录,包括重复记录。它从键范围的上限开始向下移动(按键的顺序单调递减)
    • "prevunique"光标显示所有记录,不包括重复记录。如果存在具有相同键的多个记录,则仅检索迭代的第一个记录。它从键范围的上限开始向下移动
  1. 你想要在游标在索引迭代过程中过滤出重复的,你可以传递 nextunique (或 prevunique 如果你正在向后寻找)作为方向参数。 当 nextunique 或是 prevunique 被使用时,被返回的那个总是键最小的记录;
  2. 当游标便利整个存储空间但是并未找到给定条件的值时,仍然会触发onsuccess函数;

用游标遍历全表数据,先封一个函数来:

//通过游标遍历数据 全表
// table 表名
// eachFn每次循环时的执行钩子函数
exports.readAllRecord = (table, eachFn=(v)=>{}) => {
  return new Promise((resolve, reject) => {
    let transaction = db.transaction([table]);
    let objectStore = transaction.objectStore(table);
    let request = objectStore.openCursor();
    let datas = [];
    request.onsuccess = function (event) {
      let cursor = event.target.result;
      if (cursor) {
        //console.log('cursor',Object.assign(cursor.value));
        datas.push(cursor.value);
        eachFn(cursor.value);
        cursor.continue();
      } else {
        resolve(datas);
      }
    };

    request.onerror = function (event) {
      // 错误处理!
      reject(event);
    };
  });
};

遍历范围定义

游标的第一个参数是遍历范围

定义和说明见如下代码:

//IDBKeyRange.only(1); //===1
//IDBKeyRange.lowerBound(1); //>=1
//IDBKeyRange.lowerBound(1, true); //>1
//IDBKeyRange.upperBound(2, true); //<2
//IDBKeyRange.bound(1, 2, false, true); // >= 1 && < 2
// 索引范围
let hr = IDBKeyRange.bound(230,234,false,false);//h>=230&&h<=234
let sr = IDBKeyRange.bound(0.222,0.225,false,false);//s>=0.222&&s<=0.225
let vr = IDBKeyRange.bound(0.666,0.668,false,false);//v>=0.666&&v<=0.668

let kr = IDBKeyRange.bound([230,0.222,0.666],[234,0.225,0.668],false,false);//组合索引,这里相当于上面3条语句的与值,只能统一设置hsv的边界true or false

条件组合查询

这里我们使用上面代码定义的组合索引查询范围kr
我们先封装一个完整调索引游标入参遍历数据的函数:

//通过索引游标遍历数据
//table表名
//indexName索引名
//keyRange查询范围
//eachFn每次循环时的执行钩子函数
exports.readRecordsByIndexCursor = (table,indexName,keyRange,eachFn=(v)=>{}) => {
  return new Promise((resolve, reject) => {
    let transaction = db.transaction([table]);
    let objectStore = transaction.objectStore(table);
    let index = objectStore.index(indexName);
    let request = index.openCursor(keyRange);
    let datas = [];
    request.onsuccess = function (event) {
      let cursor = event.target.result;
      if (cursor) {
        //console.log('cursor',Object.assign(cursor.value));
        datas.push(cursor.value);
        eachFn(cursor.value);
        cursor.continue();
      } else {
        resolve(datas);
      }
    };

    request.onerror = function (event) {
      // 错误处理!
      reject(event);
    };
  });
};

接着调用:

let records2 = await readRecordsByIndexCursor('image_colors','h,s,v',kr);
console.log('record2',records2);

同理使用索引方式调用试试看:

// 多个记录的话,只取第一个记录
let imagesI = await readOneRecordByIndex('image_colors','h,s,v',kr);
console.log('组合索引条件查询imagesI',imagesI);

更新数据

更新数据是以主键为准来操作,看代码:

  1. 先创建表
if (!db.objectStoreNames.contains('source_lib')) {
    objectStore = db.createObjectStore('source_lib', { keyPath: 'path' }); // 主键是path属性
    //objectStore = db.createObjectStore('source_lib', { autoIncrement: true });
    // { autoIncrement: true } 自动生成主键
    objectStore.createIndex('path', 'path', { unique: true });// 创建索引
    objectStore.createIndex('name', 'name', { unique: false });
    //三个参数分别为索引名称、索引所在的属性、配置对象
}
  1. 添加数据
await addRecord('source_lib',{
  name: '我的资源库', path: __dirname
});
await addRecord('source_lib',{
  name: '我的资源库1', path: __filename
});
  1. 更新数据

使用.put,先封装一个函数:

// 更新数据,建议用查询到的数据对象
exports.updateRecord = (table, record) => {
  return new Promise((resolve, reject) => {
    let request = db.transaction([table], 'readwrite')
      .objectStore(table)
      .put(record);

    request.onsuccess = function (event) {
      console.log('数据更新成功');
      resolve();
    };

    request.onerror = function (event) {
      console.log('数据更新失败');
      reject();
    };
  });
};

执行更新:

// 先查找数据
oneRecord = await readOneRecordByIndex('source_lib','path',__filename);
console.log('record', oneRecord);
// 更改name属性值
oneRecord.name = 'xx/oo/aa/b.fuck'+(new Date().getTime());
await updateRecord('source_lib',oneRecord);
console.log('更新后',await readOneRecordByIndex('source_lib','path',__filename));

删除数据

使用.delete,与更新数据同理
先封一个函数:

// 删除数据 indexUniqueValue 主键(keyPath)值
exports.deleteRecord = (table, indexUniqueValue) => {
  return new Promise((resolve, reject) => {
    let request = db.transaction([table], 'readwrite')
      .objectStore(table)
      .delete(indexUniqueValue);

    request.onsuccess = function (event) {
      console.log('数据删除成功');
      resolve();
    };

    request.onerror = function (event) {
      console.log('数据删除失败');
      reject();
    };
  });
};

执行删除:

console.log('删除前',await readAllRecord('source_lib'));
await deleteRecord('source_lib',__dirname);
console.log('删除后',await readAllRecord('source_lib'));

参考资料

MDN IndexedDB_API

ufotool.com版权所有,转载请注明来源!