使用 HTML5 IndexedDB 作为客户端数据存储





5.00/5 (12投票s)
将 IndexedDB 用作客户端存储库。
什么是 IndexedDB
IndexedDB 数据库是一个相对较新的技术,它取代了旧的(W3C 已弃用的)Web SQL 数据库。IndexedDB Web 数据库允许您的 HTML5 Web 应用程序将与主机/协议/端口相关的数据本地存储在客户端的硬盘上。与仅允许您使用简单的键值对存储数据的 LocalStorage 不同,IndexedDB 功能更强大,适用于需要存储大量数据的应用程序。此外,凭借其丰富的查询能力,这些应用程序可以加载更快、响应更灵敏,而无需执行服务器端事务并将结果发送到客户端 HTML 下拉菜单中显示。
IndexedDB 本质上是浏览器中的一个持久化数据存储——一个客户端数据库。像常规关系型数据库一样,它会维护其存储记录的索引,应用程序使用 IndexedDB JavaScript API 通过键或查找索引来定位记录。每个数据库都按“源”(即创建数据库的站点的域)进行范围限定。
如果您是 IndexedDB 新手,可以从这里开始
异步 API
IndexedDB API围绕着非阻塞调用线程的异步方法。要异步访问数据库,请调用open
方法,该方法在 window
对象的 indexedDB
属性上。此方法返回一个 IDBRequest
对象 (IDBOpenDBRequest
);异步操作通过在 IDBRequest
对象上触发事件来与调用应用程序通信。
浏览器兼容性
桌面
功能 |
Chrome |
Firefox (Gecko) |
Internet Explorer |
Opera |
Safari (WebKit) |
异步 API |
24.0 |
16.0 (16.0) |
10 |
15.0 |
不支持 |
同步 API |
不支持 |
不支持 |
不支持 |
不支持 |
不支持 |
移动
功能 |
Android |
Firefox 移动版 (Gecko) |
IE 手机版 |
Opera 移动版 |
Safari 移动版 |
异步 API |
不支持 |
6.0 (6.0) moz |
不支持 |
不支持 |
不支持 |
存储限制
- Firefox:IndexedDB 数据库大小无限制。用户界面会询问存储大于 50 MB 的 blob 的权限。此大小配额可以通过 `dom.indexedDB.warningQuota` 首选项进行自定义(在 http://mxr.mozilla.org/mozilla-central/source/modules/libpref/src/init/all.js 中定义)。
- Google Chrome:请参阅 https://developers.google.com/chrome...rage#temporary
- IE10 每个应用程序的存储大小为 250MB:请参阅 http://msdnrss.thecoderblogs.com/2012/12/using-html5javascript-in-windows-store-apps-data-access-and-storage-mechanism-ii/
为什么使用 IndexedDB
W3C 宣布 Web SQL 数据库(HTML5 存储的另一种选择)是一项已弃用的规范,Web 开发人员不应再使用此技术。相反,他们建议使用其替代品——IndexedDB。
与现代关系型数据库的区别IndexedDB 没有对象之间关系的概念,但这并不意味着您不能基于查询另一个对象存储的值来过滤其他对象存储。您可以将对象存储视为具有属性的类对象,这些属性可以具有唯一字段(如果没有,您可以在创建对象存储时使用自动生成键选项)。
IndexedDB 的用法
IndexedDB 的主要用途是将数据本地存储在浏览器/客户端,以便您的应用程序支持离线模式。例如,您可以对公司 HR 模块的员工数据库表执行 CUD(创建、更新和删除)操作。这样,当重新在线时,可以减少与数据库服务器的网络延迟。
客户端与后端关系型数据库服务器之间的数据同步需要您自己设计/实现,但实际上,这只是将存储的 JSON(客户端)对象重新序列化为服务器端类,并对每个编辑过的对象执行数据库操作。
IndexedDB 架构组件
- 每个源(主机、协议和端口)都有自己的数据库集。每个源内的数据库都由唯一名称标识。IndexedDB 具有同源策略,要求数据库和应用程序来自同一源。
- 数据库由名称和版本号标识。一个数据库一次只能有一个版本。
- 对象存储由唯一名称标识。您只能在“升级需要”事件期间创建对象存储。您将数据存储在对象存储的记录中。一个数据库可以有多个命名对象存储。
- 事务提供对数据库的可靠数据访问和数据修改。所有与数据库数据的交互都必须在事务范围内进行。
- 记录是键值对,其中键是对应数据值的唯一标识符。您可以设置自己的键,也可以让对象存储为您创建它们。值可以是序列化的 JSON 对象。
- 索引是一种特殊的对象存储,它将数据库键映射到已保存对象中的键字段。使用索引是可选的。对象存储可以有多个索引(与群集索引的概念不同——查询对象存储时一次只能使用一个索引)。
注意:应用程序可以使用多个数据库,每个数据库可以有多个对象存储,每个对象存储可以有多个索引。
IndexedDB 接口对象
-
IDBFactory
提供对数据库的访问。这是全局对象IndexedDB
实现的接口,因此是 API 的入口点。 -
IDBCursor
遍历对象存储和索引。 -
IDBCursorWithValue
遍历对象存储和索引并返回游标的当前值。 -
IDBDatabase
代表对数据库的连接。这是获取数据库事务的唯一方式。 -
IDBEnvironment
提供对客户端数据库的访问。它由window
对象实现。 -
IDBIndex
提供对索引元数据的访问。 - IDBKeyRange 定义键的范围。
-
IDBObjectStore
代表一个对象存储。 -
IDBOpenDBRequest
代表打开数据库的请求。
IDBRequest
提供对数据库和数据库对象异步请求结果的访问。它是调用异步方法时获得的对象。IDBTransaction
代表一个事务。您在数据库上创建一个事务,指定范围(例如要访问的对象存储),并确定所需的访问类型(只读或写入)。IDBVersionChangeEvent
表示数据库版本已更改。
Using the Code
回调处理程序
IndexedDB 的异步设计意味着需要回调来处理事务的返回值,无论是错误状态还是成功状态。回调与任何 JavaScript 异步回调方法(如下所示)类似。
request.onerror = function (event) {
// Do something with request.errorCode!
};
request.onsuccess = function (event) {
// Do something with request.result!
};
一个常见的回调是在全局级别捕获错误(抛出错误)。
// Generic error handler for all errors targeted at this database's request
db.onerror = function (event) {
alert("Database error: " + event.target.errorCode);
};
检查 IndexedDB 支持
您的应用程序需要执行验证检查,以确定您的浏览器是否支持 IndexedDB。
if (!window.indexedDB) {
window.alert("Your browser doesn't support a stable version of
IndexedDB. Such and such feature will not be available.");
}
创建和打开数据库
下面的代码片段将在数据库已存在时删除它,然后执行创建操作。在创建的回调函数中,创建索引等。创建数据库时,会先调用“onupgradeneeded”事件,然后是“onsuccess”或“onerror”回调。
function createDatabase() {
var deleteDbRequest;
try {
if (localDatabase.db != null) localDatabase.db.close();
deleteDbRequest = localDatabase.indexedDB.deleteDatabase(dbName); // delete old db
deleteDbRequest.onsuccess = function (event) {
var openRequest = localDatabase.indexedDB.open(dbName, 1); //version used
openRequest.onerror = function (e) {
writeToConsoleScreen("Database error: " + e.target.errorCode);
};
openRequest.onsuccess = function (event) {
localDatabase.db = openRequest.result;
};
openRequest.onupgradeneeded = function (evt) {
// check if OS\table not already added
if (!evt.currentTarget.result.objectStoreNames.contains(osTableName)) {
var employeeStore = evt.currentTarget.result.createObjectStore(osTableName, { keyPath: "recid" }); // key id ID
employeeStore.createIndex("lnameIndex", "lname", { unique: false });
employeeStore.createIndex("emailIndex", "email", { unique: true }); // email has to be unique (a constraint)
employeeStore.createIndex("sdateIndex", "sdate", { unique: false });
}
};
deleteDbRequest.onerror = function (e) {
writeToConsoleScreen("Database error: " + e.target.errorCode);
};
};
}
catch (e) {
writeToConsoleScreen(e.message);
}
}
在另一个浏览器标签页中打开的 Web 应用程序期间版本更改
当您的 Web 应用程序更改导致需要更改数据库版本时,您需要考虑如果用户在一个标签页中打开了旧版本的应用程序,然后在另一个标签页中加载了新版本的应用程序会发生什么。当您使用大于数据库实际版本的值调用`open()`时,所有其他打开的数据库都必须明确确认请求,然后您才能开始更改数据库。工作原理如下:
数据库版本控制
IndexedDB 数据库附带一个版本字符串。Web 应用程序可以使用它来确定特定客户端上的数据库是否具有最新结构。
这在您更改数据库数据模型并希望将这些更改传播到先前数据模型的现有客户端时非常有用。您可以简单地更改新结构的**版本号**,并在用户下次运行您的应用程序时进行检查。
创建对象存储(表)
调用数据库`open`方法后,如果指定了较新的数据库版本,则会执行`onupgradeneeded`回调方法。
var openRequest = localDatabase.indexedDB.open(dbName, 2); //version used
openRequest.onupgradeneeded = function (evt) {
// check if OS\table not already added
if (!evt.currentTarget.result.objectStoreNames.contains(osTableName)) {
var employeeStore = evt.currentTarget.result.createObjectStore(osTableName, { keyPath: "recid" }); // key id ID
employeeStore.createIndex("lnameIndex", "lname", { unique: false });
employeeStore.createIndex("emailIndex", "email", { unique: true }); // email has to be unique (a constraint)
employeeStore.createIndex("sdateIndex", "sdate", { unique: false });
}
writeToConsoleScreen("Finished creating object-store - '" + osTableName); // onupgradeneeded called first
};
创建键
var employeeStore = evt.currentTarget.result.createObjectStore(osTableName, { keyPath: "recid" });
创建名称索引
employeeStore.createIndex("lnameIndex", "lname", { unique: false });
employeeStore.createIndex("emailIndex", "email", { unique: true }); // email has to be unique (a constraint)
employeeStore.createIndex("sdateIndex", "sdate", { unique: false });
创建事务
与关系型数据库一样,IndexedDB 也在事务的上下文下执行所有 I/O 操作。事务通过连接对象创建,并支持原子、持久的数据访问和修改。事务对象有两个关键属性:
1. 范围
范围决定了数据库的哪些部分可以通过事务受到影响。这基本上有助于 IndexedDB 实现确定事务生命周期内应用的隔离级别。可以将范围简单地看作是构成事务一部分的表(称为“对象存储”)的列表。
2. 模式
事务模式决定了事务中允许的 I/O 操作类型。模式可以是:
- 只读:仅允许对事务范围内的对象执行“读取”操作。
- 读/写:允许对事务范围内的对象执行“读取”和“写入”操作。
- 版本更改:“版本更改”模式允许“读取”和“写入”操作,还允许创建和删除对象存储和索引。
事务对象会自动提交,除非被显式中止。事务对象会公开事件来通知客户端:
- 何时完成
- 何时中止
- 何时超时
if (localDatabase != null && localDatabase.db != null) {
var transaction = localDatabase.db.transaction(osTableName, "readwrite");
添加数据
下面的代码片段将创建 10,000 条记录并将其添加到 employee 对象存储中。
if (transaction) {
transaction.oncomplete = function () {
}
transaction.onabort = function () {
writeToConsoleScreen("transaction aborted.");
localDatabase.db.close();
}
transaction.ontimeout = function () {
writeToConsoleScreen("transaction timeout.");
localDatabase.db.close();
}
var store = transaction.objectStore(osTableName);
if (store) {
var req;
var customer = {};
// create ten thousand records
for (var loop = 0; loop < 10000; loop++) {
customer = {};
customer.recid = loop;
customer.fname = 'Susan';
customer.lname = 'Ottie';
customer.email = 'NewEmployee@' + loop + '.com'; //unique
customer.sdate = '4/3/2012';
req = store.add(customer);
req.onsuccess = function (ev) {
}
req.onerror = function (ev) {
writeToConsoleScreen("Failed to add record." + " Error: " + ev.message);
}
}
}
}
移除数据
下面的代码片段将删除“recid”为 7 的记录。
function deleteEmployee() {
try {
writeToConsoleScreen('Started deleting record # 7');
var transaction = localDatabase.db.transaction(osTableName, "readwrite");
var store = transaction.objectStore(osTableName);
var jsonStr;
var employee;
if (localDatabase != null && localDatabase.db != null) {
var request = store.delete(7);
request.onsuccess = function (e) {
fetchAllEmployees();
};
request.onerror = function (e) {
writeToConsoleScreen(e);
};
}
}
catch (e) {
console.log(e);
}
}
更新数据
下面的代码片段将检索“recid”为 7 的记录,更新其电子邮件地址,然后将其“PUT”回对象存储。
function updateEmployee() {
try {
writeToConsoleScreen('Started record update');
var transaction = localDatabase.db.transaction(osTableName, "readwrite");
var store = transaction.objectStore(osTableName);
var jsonStr;
var employee;
if (localDatabase != null && localDatabase.db != null) {
store.get(7).onsuccess = function(event) {
employee = event.target.result;
// save old value
jsonStr = "Old: " + JSON.stringify(employee);
writeToConsoleScreen(jsonStr);
// update record
employee.email = "bert.oneill@kofax.com";
jsonStr = "New: " + JSON.stringify(employee);
var request = store.put(employee);
request.onsuccess = function (e) {
writeToConsoleScreen("Finished Updating employee - " + jsonStr);
};
request.onerror = function (e) {
writeToConsoleScreen("Error " + e.value);
};
};
}
}
catch(e){
writeToConsoleScreen("Error " + e.value); }
}
清空所有数据/对象存储
function clearAllEmployees() {
try {
if (localDatabase != null && localDatabase.db != null) {
var store = localDatabase.db.transaction(osTableName, "readwrite").objectStore(osTableName);
store.clear().onsuccess = function (event) {
writeToConsoleScreen('Finished clearing records');
};
}
}
catch(e){
writeToConsoleScreen("Error " + e.value);
}
}
光标
IndexedDB 枚举对象存储记录的方式是使用“游标”对象。游标会遍历底层对象存储的记录。游标具有以下关键属性:
- 在索引或对象存储中的记录**范围**。
- 引用游标正在遍历的索引或对象存储的**源**。
- 指示游标在给定记录范围内的当前**位置**。
尽管游标的概念相当直接,但考虑到所有 API 调用的异步性,编写实际遍历对象存储的代码有些棘手。
要执行异步游标获取,您需要使用递归编程策略。以下是一个递归遍历游标的操作示例:
下面的代码片段将对“records”数组中的每条记录调用“addData”进行递归。
function addData(txn, store, records, i, commitT) {
try {
if (i < records.length) {
var rec = records[i];
var req = store.add(rec);
req.onsuccess = function (ev) {
i++;
addData(txn, store, records, i, commitT);
}
req.onerror = function (ev) {
writeToConsoleScreen("Failed to add record." + " Error: " + ev.message);
}
}
else if (i == records.length) {
// writeToConsoleScreen('Finished adding ' + records.length + " records");
}
}
catch (e) {
writeToConsoleScreen(e.message);
}
}
使用游标范围
在 `IDBObjectStore` 接口中,我们有一个“openCursor”方法来创建用于检索数据的新游标。在 `IDBIndex` 接口中,我们有两种创建新游标的方法。这些方法是“openCursor”用于从索引检索值,以及“openKeyCursor”用于检索键。调用这些方法时可以提供两个可选参数。第一个参数是 `IDBKeyRange`,使用它可以定义要检索的键的边界来缩小结果。第二个参数是游标必须导航结果的方向。
IDBKeyrange
键范围是某个数据类型上的一系列连续区间。键范围可以有以下几种情况:
- 下限:键必须小于提供的下限值
- 上限:键必须大于提供的上限值
- 上下限:键必须在下限和上限之间
- 无界:所有键都有效
- 单个值:键必须是提供的值
上限和下限可以是开放的(表示边界值不包含在内)或闭合的(表示边界值包含在内)。
使用游标获取所有记录
下面的 `fetchAllEmployees()` 方法是一个使用游标遍历所有记录的示例。
function fetchAllEmployees() {
try {
if (localDatabase != null && localDatabase.db != null) {
records = [];
if (!localDatabase.db.objectStoreNames.contains(osTableName)) {
writeToConsoleScreen("No employees table exists - click on Create");
return;
}
var store = localDatabase.db.transaction(osTableName).objectStore(osTableName);
var request = store.openCursor();
request.onsuccess = function (evt) {
var cursor = evt.target.result;
if (cursor) {
var employee = cursor.value;
records.push(employee);
cursor.continue();
}
else {
try {
writeToConsoleScreen('Finished fetching ' + records.length + ' recrds');
w2ui.grid.clear();
w2ui.grid.add(records); // bind to grid - auto refresh
} catch (ex) {
writeToConsoleScreen("Exception..." + ex);
}
}
};
}
}
catch (e) {
writeToConsoleScreen(e.message);
}
}
如何按多个字段(包括非索引字段)过滤
代码片段执行一个游标搜索,查找姓氏为“Silver”的记录,然后检查电子邮件是否包含数字“1”。如果是,则将记录添加到集合中并绑定到网格。
function fetchMultiFilterByEmailAndSurname() {
try {
records = [];
if (localDatabase != null && localDatabase.db != null) {
var range = IDBKeyRange.only("Silver");
var store = localDatabase.db.transaction(osTableName).objectStore(osTableName);
var index = store.index("lnameIndex");
index.openCursor(range).onsuccess = function (evt) {
var cursor = evt.target.result;
if (cursor) {
var employee = cursor.value;
if (employee.email.indexOf('1') > 0) {
// filter by another field in json object (could of used the extra indexes)
var jsonStr = JSON.stringify(employee);
records.push(employee);
}
cursor.continue();
}
else {
w2ui.grid.clear();
w2ui.grid.add(records); // bind to grid - auto refresh
}
};
}
}
catch (e) {
writeToConsoleScreen(e.message);
}
}
执行记录计数
IndexedDB 框架目前还没有提供简单的方法来执行记录计数(尽管根据 W3C 规范,这将会改变)。但通过使用游标,可以相对简单地获得对象存储的记录计数。
function countRecords()
{
if (localDatabase != null && localDatabase.db != null) {
var transaction = localDatabase.db.transaction(osTableName, "readwrite");
if (transaction) {
transaction.oncomplete = function () {
}
transaction.onabort = function () {
writeToConsoleScreen("transaction aborted.");
localDatabase.db.close();
}
transaction.ontimeout = function () {
writeToConsoleScreen("transaction timeout.");
localDatabase.db.close();
}
var store = transaction.objectStore(osTableName);
if (store) {
var keyRange = IDBKeyRange.lowerBound(0);
var cursorRequest = store.openCursor(keyRange);
var count = 0;
cursorRequest.onsuccess = function (e) { // success called for each cursor action
var result = e.target.result;
result ? ++count && result.continue() : alert(count);
};
}
}
}
else
{
writeToConsoleScreen("Database needs to be created first");
}
}
安全
IndexedDB 使用同源原则,这意味着它将存储与创建它的站点的源(通常是站点域名或子域名)绑定,因此任何其他源都无法访问它。
需要注意的是,IndexedDB 不适用于从另一个站点加载到框架中的内容(无论是<frame>
还是<iframe>
)。
关于浏览器关闭的警告
当浏览器关闭时(例如,用户选择“退出”或单击“关闭”按钮),任何挂起的 IndexedDB 事务都会(静默地)中止——它们不会完成,也不会触发错误处理程序。由于用户可以随时退出浏览器,这意味着您不能依赖任何特定的事务来完成或知道它是否已完成。此行为有几个影响。
首先,您应该始终小心地在每个事务结束时将数据库保留在一致状态。例如,假设您正在使用 IndexedDB 存储用户可以编辑的项目列表。您在编辑后通过清除对象存储并写出新列表来保存列表。如果您在一个事务中清除对象存储,而在另一个事务中写入新列表,则存在浏览器可能在清除后但在写入前关闭的风险,导致您得到一个空数据库。为避免这种情况,您应该将清除和写入合并到一个事务中。
其次,您绝不应将数据库事务与卸载事件相关联。如果卸载事件是由浏览器关闭触发的,则在卸载事件处理程序中创建的任何事务都将永远无法完成。
事实上,即使正常关闭浏览器,也无法保证 IndexedDB 事务会完成。请参阅bug 870645。
无跨浏览器共享(即使是同源)
如果数据库是使用 Chrome 创建(并填充)的,然后在 IE10 中打开相同的 URL,即使两个源相同——每个浏览器都会创建一个数据库,并且它们不会共享任何数据。
浏览器支持
IE |
Firefox |
Chrome |
Safari |
Opera |
iOS Safari |
Android 版 Chrome |
IE 移动版 |
回溯 26 个版本 |
4.0:不支持 |
||||||
回溯 25 个版本 |
5.0:不支持 |
||||||
回溯 24 个版本 |
2.0:不支持 |
6.0:不支持 |
|||||
回溯 23 个版本 |
3.0:不支持 |
7.0:不支持 |
|||||
回溯 22 个版本 |
3.5:不支持 |
8.0:不支持 |
|||||
回溯 21 个版本 |
3.6:不支持 |
9.0:不支持 |
|||||
回溯 20 个版本 |
4.0:部分支持 moz |
10.0:不支持 |
|||||
回溯 19 个版本 |
5.0:部分支持 moz |
11.0:部分支持 webkit |
|||||
回溯 18 个版本 |
6.0:部分支持 moz |
12.0:部分支持 webkit |
|||||
回溯 17 个版本 |
7.0:部分支持 moz |
13.0:部分支持 webkit |
|||||
回溯 16 个版本 |
8.0:部分支持 moz |
14.0:部分支持 webkit |
|||||
回溯 15 个版本 |
9.0:部分支持 moz |
15.0:部分支持 webkit |
|||||
回溯 14 个版本 |
10.0:支持 moz |
16.0:部分支持 webkit |
|||||
回溯 13 个版本 |
11.0:支持 moz |
17.0:部分支持 webkit |
9.0:不支持 |
||||
回溯 12 个版本 |
12.0:支持 moz |
18.0:部分支持 webkit |
9.5-9.6:不支持 |
||||
回溯 11 个版本 |
13.0:支持 moz |
19.0:部分支持 webkit |
10.0-10.1:不支持 |
||||
回溯 10 个版本 |
14.0:支持 moz |
20.0:部分支持 webkit |
10.5:不支持 |
||||
回溯 9 个版本 |
15.0:支持 moz |
21.0:部分支持 webkit |
10.6:不支持 |
||||
回溯 8 个版本 |
16.0:支持 |
22.0:部分支持 webkit |
11.0:不支持 |
||||
回溯 7 个版本 |
17.0:支持 |
23.0:支持 webkit |
11.1:不支持 |
||||
回溯 6 个版本 |
18.0:支持 |
24.0:支持 |
11.5:不支持 |
10.0:不支持 |
|||
回溯 5 个版本 |
5.5:不支持 |
19.0:支持 |
25.0:支持 |
3.1:不支持 |
11.6:不支持 |
11.0:不支持 |
|
回溯 4 个版本 |
6.0:不支持 |
20.0:支持 |
26.0:支持 |
3.2:不支持 |
12.0:不支持 |
11.1:不支持 |
|
回溯 3 个版本 |
7.0:不支持 |
21.0:支持 |
27.0:支持 |
4.0:不支持 |
12.1:不支持 |
11.5:不支持 |
|
回溯 2 个版本 |
8.0:不支持 |
22.0:支持 |
28.0:支持 |
5.0:不支持 |
15.0:支持 |
12.0:不支持 |
|
上一版本 |
9.0:不支持 |
23.0:支持 |
29.0:支持 |
5.1:不支持 |
16.0:支持 |
12.1:不支持 |
|
Current |
10.0:支持 |
24.0:支持 |
30.0:支持 |
6.0:不支持 |
17.0:支持 |
16.0:支持 |
24.0:支持 |
不久的将来 |
11.0:支持 |
25.0:支持 |
31.0:支持 |
7.0:支持未知 |
|||
更遥远的未来 |
26.0:支持 |
32.0:支持 |
在 Chrome 开发者工具中查看数据库数据\组件
您可以在 Chrome 的开发者工具选项(Ctrl+Shift+i)中查看数据库的对象存储、索引、键路径和数据。
代码
结论
HTML5 IndexedDB API 非常有用且功能强大。您可以利用它来创建丰富、在线和离线的 HTML5 应用程序。此外,通过 IndexedDB API,您可以缓存数据,使传统的 Web 应用程序,尤其是移动 Web 应用程序,加载更快、响应更灵敏,而无需每次都从 Web 服务器检索数据(特别适合静态或长期数据)。
参考文献
- IndexedDB API 参考
- Indexed Database API 规范
- 在 Chrome 中使用 IndexedDB
- https://mdn.org.cn/en/docs/IndexedDB
- IETestDrive 上的 Cookbook 演示 IETestDrive
- IE10 更新 - https://blogs.msdn.com/b/ie/archive/2012/03/21/indexeddb-updates-for-ie10-and-metro-style-apps.aspx?Redirected=true
- Microsoft Labs - http://www.html5labs.com/prototypes/indexeddb/indexeddb/download