使用Web IndexedDB

759 阅读7分钟

IndexedDB 是一种可以让你在用户的浏览器内持久化存储数据的方法。IndexedDB 为生成 Web Application 提供了丰富的查询能力,使我们的应用在在线和离线时都可以正常工作。

IndexedDB 是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该 API 使用索引实现对数据的高性能搜索。虽然 Web Storage 在存储较少量的数据很有用,但对于存储更大量的结构化数据来说力不从心。而 IndexedDB 提供了这种场景的解决方案。

传送门

基于 window.indexedDB API

基本使用流程

IndexedDB 基本使用流程如下所示:

  1. 打开数据库 .open
  2. 在数据库中创建一个对象仓库(object store).createObjectStore
  3. 启动一个事务 .transaction,并发送一个请求来执行一些数据库操作,像增加或提取数据等。
  4. 通过监听正确类型的 DOM 事件以等待操作完成。
  5. 在操作结果上进行一些操作(可以在 request 对象中找到)

基本使用

新增IndexedDB数据库

声明一个创建IndexedDB请求

window.indexedDB.open(db_name:string, version:integer)

IndexedDB声明请求接收三种不同的 DOM 事件:success(声明成功)、error(声明失败) 和 onupgradeneeded(版本更新)

var db_name = 'the_name'
var db

var request = window.indexedDB.open(db_name, 1)

request.onerror = function(event) {
  // Do something with request.errorCode!
  alert("Database error: " + event.target.errorCode);
};

request.onsuccess = function(event) {
  // Do something with request.result!
  db = event.target.result
};

// 更新版本,用来创建或者删除对象存储仓库
request.onupgradeneeded = function(event) {
  // 保存 IDBDataBase 接口
  db = event.target.result;

  // 为该数据库创建一个对象存储仓库
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
  
 // 创建对象支持索引
 objectStore.createIndex('name', 'name', { unique: false });
 objectStore.createIndex('emial', 'emial', { unique: true });
};

新增IndexedDB对象存储仓库

使用 db.createObjectStore(name:string, opt:obj) 创建一个对象存储仓库,需要在onupgradeneeded监听中实现,否则会报错提示

opt 支持:

  • keyPath:string :使用键路径,指定主键索引名称,键值唯一值
  • autoIncrement:boolean : 使用键生成器,默认该设置是不开启的,从1开始,默认加1

声明完对象存储仓库,使用 store.createIndex(name, key, opt:{unique:boolean}) 声明对象索引,通过unique指明键值是否唯一

// 该事件仅在较新的浏览器中实现了
request.onupgradeneeded = function(event) {
  // 保存 IDBDataBase 接口
  db = event.target.result;

  // 为该数据库创建一个对象存储仓库
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });
  
 // 创建对象支持索引
 objectStore.createIndex('name', 'name', { unique: false });
 objectStore.createIndex('emial', 'emial', { unique: true });
};
};

使用事务(transaction)添加键值对

需要使用事务(transaction)才能对IndexedDB的对象存储进行操作

使用 db.transaction(stores:array, mode:string) 声明一个事务

支持三种模式 mode:

  • 'readonly':对象仓库只读,默认
  • 'readwrite':对象仓库支持读写
  • 'versionchange':支持新建或删除对象仓库或索引

事务接收三种不同的 DOM 事件:errorabort 和 complete

使用 transaction.objectStore(stores:array)读取对象存储仓库,只能从创建事务时指定的对象仓库中取出一个对象仓库

读取对象存储如截图

image.png

// 我们的客户数据看起来像这样。
const customerData = [
  { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },
  { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" }
];

// 声明一个事务
var transaction = db.transaction(["customers"], "readwrite");

// 在所有数据添加完毕后的处理
transaction.oncomplete = function(event) {
  alert("All done!");
};

transaction.onerror = function(event) {
  // 不要忘记错误处理!
};

// 获取对象存储仓库,执行添加
var objectStore = transaction.objectStore("customers");
customerData.forEach(function(customer) {
  var request = objectStore.add(customer);
  request.onsuccess = function(event) {
    // event.target.result === customer.ssn;
  };
});

使用事务(transaction)获取键值对

使用 store.get(key) 获取指定主键值(Key)的对象

var request = db.transaction(["customers"], "readwrite")
                .objectStore("customers")
                .get("444-44-4444");
request.onsuccess = function(event) {
  // 读取成功!
  alert("Name for SSN 444-44-4444 is " + request.result.name);
};

使用事务(transaction)编辑键值对

同添加,只要主键一样,则替换新值,或者删除后,重新添加

使用事务(transaction)删除对象索引

使用 store.deleteIndex(keyPath) 删除对象索引

var request = db.transaction(["customers"], "versionchange")
                .objectStore("customers")
                .deleteIndex("email");
request.onsuccess = function(event) {
  // 删除成功!
};

使用事务(transaction)删除键值对

使用 store.delete(key) 删除指定主键值(Key)的对象

var request = db.transaction(["customers"], "readwrite")
                .objectStore("customers")
                .delete("444-44-4444");
request.onsuccess = function(event) {
  // 删除成功!
};

使用事务(transaction)删除所有键值对

使用 store.clear() 删除仓库所有键值对

使用游标

如果你想要遍历对象存储空间中的所有值,那么你可以使用游标。

使用 store.openCursor() 打开一个游标,通过 .continue() 移动游标

var objectStore = db.transaction("customers").objectStore("customers");

objectStore.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
    cursor.continue();
  }
  else {
    alert("No more entries!");
  }
};

openCursor 支持参数:

使用索引

使用 store..index(key) 获取指定索引名称的IDBIndex对象

读取数据如截图

image.png

可以在索引上打开两个不同类型的游标:

  • 一个常规游标映射索引属性到对象存储空间中的对象
  • 一个键索引映射索引属性到用来存储对象存储空间中的对象的键

不同之处被展示如下:

// 获取索引name 的索引路径
var index = objectStore.index("name");

index.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key 是一个 name, 就像 "Bill", 然后 cursor.value 是整个对象。
    alert("Name: " + cursor.key + ", SSN: " + cursor.value.ssn + ", email: " + cursor.value.email);
    cursor.continue();
  }
};

index.openKeyCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key 是一个 name, 就像 "Bill", 然后 cursor.value 是那个 SSN。
    // 没有办法可以得到存储对象的其余部分。
    alert("Name: " + cursor.key + ", SSN: " + cursor.value);
    cursor.continue();
  }
};

指定索引范围

IDBKeyRange 传送门

// 仅匹配 "Donna"
var singleKeyRange = IDBKeyRange.only("Donna");

// 匹配所有超过“Bill”的,包括“Bill”
var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");

// 匹配所有超过“Bill”的,但不包括“Bill”
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

// 匹配所有不超过“Donna”的,但不包括“Donna”
var upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);

// 匹配所有在“Bill”和“Donna”之间的,但不包括“Donna”
var boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);

// 使用其中的一个键范围,把它作为 openCursor()/openKeyCursor 的第一个参数
index.openCursor(boundKeyRange).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // 当匹配时进行一些操作
    cursor.continue();
  }
};

指定游标方向

IDBCursor.direction 传送门

// 指定范围,游标方向
objectStore.openCursor(boundKeyRange, "prev").onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // 进行一些操作
    cursor.continue();
  }
};

// 只改变游标方向,不限制范围,则第一参数传null
objectStore.openCursor(null, "prev").onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

当一个 web app 在另一个标签页中被打开时的版本变更

当你的网页应用以数据库版本变更的方式发生改变时,你需要考虑,如果用户在一个标签页中打开的应用里使用了旧版本的数据库,在另一个标签页里加载新版本的数据库时会发生什么。当你使用更高的版本号调用 open() 方法时,其他所有打开的数据库必须显式地确认请求,你才能对数据库进行修改(onblocked 事件会被触发知道它们被关闭或重新加载):

var request = mozIndexedDB.open("MyTestDatabase", 2);

request.onblocked = function(event) {
  // 如果其他的一些页签加载了该数据库,在我们继续之前需要关闭它们
  alert("请关闭其他由该站点打开的页签!");
};

request.onupgradeneeded = function(event) {
  // 其他的数据已经被关闭,一切就绪
  db.createObjectStore(/* ... */);
  useDatabase(db);
};

request.onsuccess = function(event) {
  var db = event.target.result;
  useDatabase(db);
  return;
};

function useDatabase(db) {
  // 当由其他页签请求了版本变更时,确认添加了一个会被通知的事件处理程序。
  // 这里允许其他页签来更新数据库,如果不这样做,版本升级将不会发生知道用户关闭了这些页签。
  db.onversionchange = function(event) {
    db.close();
    alert("A new version of this page is ready. Please reload or close this tab!");
  };

  // 处理数据库
}

安全

IndexedDB 使用同源原则,这意味着它把存储空间绑定到了创建它的站点的源(典型情况下,就是站点的域或是子域),所以它不能被任何其他源访问。

第三方窗口内容(比如 <iframe> 内容)可以访问它所嵌入的源的 IndexedDB 仓库,除非浏览器被设置成从不接受第三方 cookies(参见 bug 1147821)。

IndexedDB检测

配合DevTools 的Application Panel 传送门

完整实例

传送门