Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

indexedDB #29

Open
cy0707 opened this issue Dec 27, 2016 · 0 comments
Open

indexedDB #29

cy0707 opened this issue Dec 27, 2016 · 0 comments
Labels

Comments

@cy0707
Copy link
Owner

cy0707 commented Dec 27, 2016

indexedDB

indexed Database API 或者简称为IndexedDB,是在浏览器中保存的结构化数据的一种数据库。IndexedDB是为了代替目前已废弃的Web SQL Database API而出现的。

IndexedDB设计的操作完全是异步进行的,因此,多数操作会以请求方式进行,但这些操作会在后期执行,然后如果成功则返回结果,如果失败则返回错误。差不多每一次IndexedDB操作,都需要注册onerror和onsuccess事件处理程序,以确保适当地处理结果。

由于 IndexedDB 本身的规范还在持续演进中,当前的 IndexedDB 的实现还是使用浏览器前缀。在规范更加稳定之前,浏览器厂商对于标准 IndexedDB API 可能都会有不同的实现。但是一旦大家对规范达成共识的话,厂商就会不带前缀标记地进行实现。实际上一些实现已经移除了浏览器前缀:IE 10,Firefox 16 和 Chrome 24。当使用前缀的时候,基于 Gecko 内核的浏览器使用 moz 前缀,基于 WebKit 内核的浏览器会使用 webkit 前缀等等

// In the following line, you should include the prefixes of implementations you want to test.
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
// DON'T use "var indexedDB = ..." if you're not in a function.
// Moreover, you may need references to some window.IDB* objects:
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange
// (Mozilla has never prefixed these objects, so we don't need window.mozIDB*)
[/pre]

if (!window.indexedDB) {
    window.alert("Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.")
}

数据库

IndexedDB就是一个数据库,其最大的特色是使用对象保存数据,而不是使用表来保存数据。一个IndexedDB数据库,就是一组位于相同命名空间下的对象的集合。

  • 使用IndexedDB的第一步就是打开它,即把要打开的数据库名传给indexedDB.open()。如果传入的数据库已经存在,就会发送一个打开它的请求;如果传入的数据库还不存在,就会发送一个创建并打开它的请求。

  • indexDB.open()会返回一个IDBRequest对象,在这个对象上可以添加onerror和onsuccess事件处理程序

  • 该 open 方法接受第二个参数,就是数据库的版本号。这样我们就可以更新数据库的 schema ,也就是说如果我们打开的数据库不是我们期望的最新版本的话,我们可以对 object store 进行创建或是删除。

var request,
      database;
request = indexedDB.open('admin');
request.onerror = function(event){
    console.log('somthing bad happended while trying to open:' + event.target.errorCode);
}
request.onsuccess= function(event){
    database = event.target.result;
   console.log(database);
}

在上面两个事件处理程序中,event.target都指向request对象,因此他们可以互相换用。如果响应onsuccess事件处理程序,那么event.target.result中将有一个数据库实例对象(IDBData-base)。如果发生错误,那么event.target.errorCode中将保存一个错误码,表示问题的性质。参见一下

错误码 含义
IDBDatabaseException.UNKNOW_ERR(1) 意外错误,无法归类
IDBDatabaseException.NON_TRANSIENT_ERR(2) 操作不合法
IDBDatabaseException.NON_FOUND_ERR(3) 未发现要操作的数据库
IDBDatabaseException.CONSTRAINT_ERR(4) 违反了数据库约束
IDBDatabaseException.DATA_ERR(5) 提示给事务的数据不能满足要求
IDBDatabaseException.NOT_ALLOWED_ERR(6) 操作不合法
IDBDatabaseException.TRANSACTION_INACTIVE_ERR(7) 试图重用已完成的事务
IDBDatabaseException.ABORT_ERR(8) 请求中断,未成功
IDBDatabaseException.READ_ONLY_ERR(9) 试图在只读模式下写入或修改数据
IDBDatabaseException.TIMEOUT_ERR(10) 在有效时间内未完成操作
IDBDatabaseException.QUOTA_ERR(11) 磁盘空间不足

创建和更新数据库版本号

要更新数据库的 schema,也就是创建或者删除对象存储空间,需要实现 onupgradeneeded 处理程序,这个处理程序将会作为一个允许你处理对象存储空间的 versionchange 事务的一部分被调用。

// 该事件仅在较新的浏览器中被实现
request.onupgradeneeded = function(event) { 
   // 更新对象存储空间和索引 .... 
};

在数据库第一次被打开时或者当指定的版本号高于当前被持久化的数据库的版本号时,这个 versionchange 事务将被创建

版本号是一个unsigned long long数字,这意味着它可以是一个非常大的整数。

这也意味着你不能使用浮点数,否则它会被转换成最接近的较小的整数并且事务可能不会启动,同样 upgradeneeded 事件也不会作为一个结果返回。例如不要使用 2.4 作为版本:

var request = indexedDB.open("MyTestDatabase", 2.4); 
// don't do this, as the version will be rounded to 2

构建数据库

现在来构建数据库。IndexedDB 使用对象存储空间而不是表,并且一个单独的数据库可以包含任意数量的对象存储空间。每当一个值被存储进一个对象存储空间时,它会被和一个键相关联。键的提供可以有几种不同的方法,这取决于对象存储空间是使用 key path 还是 key generator。

下面的表格显示了几种不同的提供键的方法。

key Path key Generator 描述
No No 这种对象存储空间可以持有任意类型的值,甚至是像数字和字符串这种基本数据类型的值。每当我们想要增加一个新值的时候,必须提供一个单独的键参数。
Yes No 这种对象存储空间只能持有 JavaScript 对象。这些对象必须具有一个和 key path 同名的属性。
No Yes 这种对象存储空间可以持有任意类型的值。键会为我们自动生成,或者如果你想要使用一个特定键的话你可以提供一个单独的键参数。
Yes Yes 这种对象存储空间只能持有 JavaScript 对象。通常一个键被生成的同时,生成的键的值被存储在对象中的一个和 key path 同名的属性中。然而,如果这样的一个属性已经存在的话,这个属性的值被用作键而不会生成一个新的键。

你也可以使用对象存储空间持有的对象,不是基本数据类型,在任何对象存储空间上创建索引。索引可以让你使用被存储的对象的属性的值来查找存储在对象存储空间的值,而不是用对象的键来查找。

此外,索引具有对存储的数据执行简单限制的能力。通过在创建索引时设置 unique 标记,索引可以确保不会有两个具有同样索引 key path 值的对象被储存。因此,举例来说,如果你有一个用于持有一组 people 的对象存储空间,并且你想要确保不会有两个拥有同样 email 地址的 people,你可以使用一个带有 unique 标识的索引来确保这些。

// 我们的客户数据看起来像这样。
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" }
];
const dbName = "the_name";

var request = indexedDB.open(dbName, 2);

request.onerror = function(event) {
  // 错误处理程序在这里。
};
request.onupgradeneeded = function(event) {
  var db = event.target.result;

  // 创建一个对象存储空间来持有有关我们客户的信息。
  // 我们将使用 "ssn" 作为我们的 key path 因为它保证是唯一的。
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // 创建一个索引来通过 name 搜索客户。
  // 可能会有重复的,因此我们不能使用 unique 索引。
  objectStore.createIndex("name", "name", { unique: false });

  // 创建一个索引来通过 email 搜索客户。
  // 我们希望确保不会有两个客户使用相同的 email 地址,因此我们使用一个 unique 索引。
  objectStore.createIndex("email", "email", { unique: true });

  // 在新创建的对象存储空间中保存值
  for (var i in customerData) {
    objectStore.add(customerData[i]);
  }
};

正如前面提到的,onupgradeneeded 是我们唯一可以修改数据库结构的地方。在这里面,我们可以创建和删除对象存储空间以及构建和删除索引。

对象存储空间仅调用 createObjectStore() 就可以创建。这个方法使用存储空间的名称,和一个对象参数。
即便这个参数对象是可选的,它还是非常重要的,因为它可以让你定义重要的可选属性和完善你希望创建的对象存储空间的类型。

在我们的示例中,我们请求了一个名为“customers” 的对象存储空间并且定义了一个 使得存储空间中每个单独的对象都是唯一的属性作为 key path。

在这个示例中的属性是 “ssn”,被确保是唯一的。被存储在对象存储空间中的所有对象都必须存在“ssn”。

我们也请求了一个名为 “name” 的着眼于存储的对象的 name 属性的索引。如同 createObjectStore(),createIndex() 使用了一个完善了我们希望创建的索引类型的可选的 options 对象。添加一个不带 name 属性的对象也会成功,但是这个对象不会出现在 “name” 索引中。

增加和删除数据

在你可以对新数据库做任何事情之前,你需要开始一个事务。事务来自于数据库对象,而且你必须指定你想让这个事务跨越哪些对象存储空间。另外,你需要决定你是否将要对数据库进行更改或者你只是需要从它里面进行读取。虽然事务具有三种模式(只读,读写,和版本变更),在可以的情况下你最好还是使用只读事务,因为它们可以并发运行。

var transaction = db.transaction(["customers"], "readwrite");
// 注意: 旧版实验性的实现使用不建议使用的常量 IDBTransaction.READ_WRITE 而不是 "readwrite"。
// 如果你想支持这样的实现,你只要这样写就可以了: 
// var transaction = db.transaction(["customers"], IDBTransaction.READ_WRITE);

transaction()

transaction()接受三个参数,(其中两个是可选的)并且返回一个事务对象。

  • 第一个参数是事务希望跨越的对象存储空间的列表,如果你希望事务能够跨越所有的对象存储空间你可以传入一个空数组。
  • 如果你没有为第二个参数指定任何内容,你得到的是只读事务。因为这里我们是想要写入所以我们需要传入 "readwrite" 标识。
  • 事务可以接收三种不同类型的 DOM 事件: error,abort,以及 complete

现在有了一个事务了,你将需要从它拿到一个对象存储空间。事务只能让你拿到一个你在创建事务时已经指定过的对象存储空间。然后你可以增加所有你需要的数据。

// 当所有的数据都被增加到数据库时执行一些操作
transaction.oncomplete = function(event) {
  alert("All done!");
};

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

var objectStore = transaction.objectStore("customers");
for (var i in customerData) {
  var request = objectStore.add(customerData[i]);
  request.onsuccess = function(event) {
    // event.target.result == customerData[i].ssn
  };
}

产生自 add() 调用的请求的 result 是被添加的值的键。因此在这种情况下,它应该等于被添加的对象的 ssn 属性, 因为对象存储空间使用 ssn 属性作为 key path。

注意 add() 函数要求数据库中不能已经有相同键的对象存在。如果你正在试图修改一个现有条目,或者你并不关心是否有一个同样的条目已经存在,使用 put()函数。

从数据库中删除数据

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

从数据库中获取数据

现在数据库里已经有了一些信息,你可以通过几种方法对它进行提取。首先是简单的 get()。你需要提供键来提取值,像这样:

var transaction = db.transaction(["customers"]);
var objectStore = transaction.objectStore("customers");
var request = objectStore.get("444-44-4444");
request.onerror = function(event) {
  // 错误处理!
};
request.onsuccess = function(event) {
  // 对 request.result 做些操作!
  alert("Name for SSN 444-44-4444 is " + request.result.name);
};

对于一个“简单”的提取这里的代码有点多了。下面看我们怎么把它再缩短一点,假设你在数据库的级别上来进行的错误处理:

db.transaction("customers").objectStore("customers").get("444-44-4444").onsuccess = function(event) {
  alert("Name for SSN 444-44-4444 is " + event.target.result.name);
};

这是如何工作的呢?由于只有一个对象存储空间,你可以避免传入一个在你的事务中需要的对象存储空间的列表,而只是作为一个字符串把名字传入即可。

同样,你只是在从数据库读取数据,所以你不需要一个 "readwrite"事务。调用一个没有指定模式的 transaction()将给你一个 "readonly"事务。

这里的另外一个微妙之处在于你实际上不需要保存请求对象到一个变量。因为 DOM 事件把这个请求作为它的 target,你可以使用 event 来得到 result 属性。

使用游标

使用 get() 要求你知道你想要检索哪一个键。如果你想要遍历对象存储空间中的所有值,那么你可以使用游标。看起来会像下面这样:

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() 函数

openCursor() 函数需要几个参数如下:

  • 你可以使用一个 key range 对象来限制被检索的项目的范围。
  • 你可以指定你希望进行迭代的方向

在上面的示例中,我们在以升序迭代所有的对象。游标成功的回调有点特别。游标对象本身是请求的 result (上面我们使用的是简写形式,所以是 event.target.result)。

然后实际的 key 和 value 可以根据游标对象的 key 和 value 属性被找到。如果你想要保持继续前行,那么你必须调用游标上的 continue() 。

当你已经到达数据的末尾时(或者没有匹配 openCursor() 请求的条目)你仍然会得到一个成功回调,但是 result 属性是 undefined。

使用游标的一种常见模式是提取出在一个对象存储空间中的所有对象然后把它们添加到一个数组中,像这样:

var customers = [];

objectStore.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    customers.push(cursor.value);
    cursor.continue();
  }
  else {
    alert("Got all customers: " + customers);
  }
};

使用索引

使用 SSN 作为键来存储客户数据是合理的,因为 SSN 唯一地标识了一个个体(对隐私来说这是否是一个好的想法是另外一个话题,不在本文的讨论范围内)。如果你想要通过姓名来查找一个客户,那么,你将需要在数据库中迭代所有的 SSN 直到你找到正确的那个。以这种方式来查找将会非常的慢,相反你可以使用索引。

var index = objectStore.index("name");
index.get("Donna").onsuccess = function(event) {
  alert("Donna's SSN is " + event.target.result.ssn);
};

“name” 游标不是唯一的,因此 name 被设成 "Donna" 的记录可能不止一条。在这种情况下,你总是得到键值最小的那个。

如果你需要访问带有给定 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 is 一个 name, 就像 "Bill", 然后 cursor.value 是那个 SSN。
    // 没有办法可以得到存储对象的其余部分。
    alert("Name: " + cursor.key + ", SSN: " + cursor.value);
    cursor.continue();
  }
};

指定游标的范围和方向

如果你想要限定你在游标中看到的值的范围,你可以使用一个 key range 对象然后把它作为第一个参数传给 openCursor() 或是 openKeyCursor()。

你可以构造一个只允许一个单一 key 的 key range,或者一个具有下限或上限,或者一个既有上限也有下限。边界可以是闭合的(也就是说 key range 包含给定的值)或者是“开放的”(也就是说 key range 不包括给定的值)。这里是它如何工作的:

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

// 匹配所有在 "Bill" 前面的, 包括 "Bill"
var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");

// 匹配所有在 “Bill” 前面的, 但是不需要包括 "Bill"
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

// Match anything up to, but not including, "Donna"
var upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);

//Match anything between "Bill" and "Donna", but not including "Donna"
var boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);

index.openCursor(boundKeyRange).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};

有时候你可能想要以倒序而不是正序(所有游标的默认顺序)来遍历。切换方向是通过传递 prev 到 openCursor() 方法来实现的

objectStore.openCursor(null, IDBCursor.prev).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

因为 “name” 索引不是唯一的,那就有可能存在具有相同 name 的多条记录。要注意的是这种情况不可能发生在对象存储空间上,因为键必须永远是唯一的。如果你想要在游标在索引迭代过程中过滤出重复的,你可以传递 nextunique (或 prevunique 如果你正在向后寻找)作为方向参数。 当 nextunique 或是 prevunique 被使用时,被返回的那个总是键最小的记录。

index.openKeyCursor(null, IDBCursor.nextunique).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

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

当你的 web app 在这样一种方式下改变你的数据库时碰到被要求进行版本变化,你需要考虑如果用户已经在一个标签页中打开了你的应用的旧版本的数据库,然后他又在另一个标签页中加载了你的应用的新版本,这种情况下会发生什么事情。当你带着比数据库实际版本更高的版本号调用 open() 时,所有其他打开的数据库必须在你开始实际对数据库进行修改之前显式通知这个请求。这里是它如何工作的:

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

openReq.onblocked = function(event) {
  // 如果其他标签页已经加载了这个数据库,那么
  // 在我们可以继续处理之前它需要被关闭。
  alert("Please close all other tabs with this site open!");
};

openReq.onupgradeneeded = function(event) {
  // 所有其它数据库都已经被关掉了。Set everything up.
  db.createObjectStore(/* ... */);
  useDatabase(db);
}  

openReq.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!");
  };

  // 其他针对数据库的处理
}

数据库的删除

indexedDB.deleteDatabase('数据库的名字');

补充知识

   var objectStore = thisDb.createObjectStore(db_store_name, {keyPath: 'id', autoIncrement: true});
// keyPath是自增加的,所以每次都会增加1,虽然前面 的数据被删除了,但是
//先创建的还是从删除的最后一个keyPath开始加1

indexedDB的API

IndexedDB

参考文章

《javascript高级程序设计》
使用 IndexedDB
HTML5的存储

@cy0707 cy0707 added the 存储 label Dec 29, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant