使用 IndexedDB
IndexedDB 是一种可以让你在用户的浏览器内持久化存储数据的方法。IndexedDB 为生成 Web Application 提供了丰富的查询能力,使我们的应用在在线和离线时都可以正常工作。
关于本文档
本篇教程将教会你如何使用 IndexedDB 的异步 API。如果你对 IndexedDB 还不熟悉,你应该首先阅读有关 IndexedDB 的基本概念。
有关 IndexedDB API 的参考手册,请参见 IndexedDB 这篇文章及其子页面,包括 IndexedDB 使用的对象类型,以及同步和异步 API 的方法。
基本模式
IndexedDB 鼓励使用的基本模式如下所示:
- 打开数据库并且开始一个事务。 创建一个 object store。 构建一个请求来执行一些数据库操作,像增加或提取数据等。 通过监听正确类型的 DOM 事件以等待操作完成。 在操作结果上进行一些操作(可以在 request 对象中找到)
生成和构建一个对象存储空间
由于 IndexedDB 本身的规范还在持续演进中,当前的 IndexedDB 的实现还是使用浏览器前缀。在规范更加稳定之前,浏览器厂商对于标准 IndexedDB API 可能都会有不同的实现。但是一旦大家对规范达成共识的话,厂商就会不带前缀标记地进行实现。实际上一些实现已经移除了浏览器前缀:IE 10,Firefox 16 和 Chrome 24。当使用前缀的时候,基于 Gecko 内核的浏览器使用moz 前缀,基于 WebKit 内核的浏览器会使用 webkit 前缀。
使用体验版本的 IndexedDB
如果你希望在仍旧使用前缀的浏览器中测试你的代码, 可以使用下列代码:
Key Path Key Generator Description NoNo这种对象存储空间可以持有任意类型的值,甚至是像数字和字符串这种基本数据类型的值。每当我们想要增加一个新值的时候,必须提供一个单独的键参数。YesNo这种对象存储空间只能持有 JavaScript 对象。这些对象必须具有一个和 key path 同名的属性。NoYes这种对象存储空间可以持有任意类型的值。键会为我们自动生成,或者如果你想要使用一个特定键的话你可以提供一个单独的键参数。YesYes这种对象存储空间只能持有 JavaScript 对象。通常一个键被生成的同时,生成的键的值被存储在对象中的一个和 key path 同名的属性中。然而,如果这样的一个属性已经存在的话,这个属性的值被用作键而不会生成一个新的键。你也可以使用对象存储空间持有的对象,不是基本数据类型,在任何对象存储空间上创建索引。索引可以让你使用被存储的对象的属性的值来查找存储在对象存储空间的值,而不是用对象的键来查找。
此外,索引具有对存储的数据执行简单限制的能力。通过在创建索引时设置 unique 标记,索引可以确保不会有两个具有同样索引 key path 值的对象被储存。因此,举例来说,如果你有一个用于持有一组 people 的对象存储空间,并且你想要确保不会有两个拥有同样 email 地址的 people,你可以使用一个带有 unique 标识的索引来确保这些。
这听起来可能有点混乱,但下面这个简单的例子应该可以演示这些个概念:
一个完整的 IndexedDB 示例HTML 内容
<h1>IndexedDB Demo: storing blobs, e-publications example</h1> <div id="msg"> </div> <form id="register-form"> <table> <tbody> <tr> <td> <label for="pub-title" class="required"> Title: </label> </td> <td> <input type="text" id="pub-title" name="pub-title" /> </td> </tr> <tr> <td> <label for="pub-year" class="required"> Year: </label> </td> <td> <input type="number" id="pub-year" name="pub-year" /> </td> </tr> <tr> <td> <label for="pub-biblioid" class="required"> Bibliographic ID <span class="note">(ISBN, ISSN, etc.)</span>: </label> </td> <td> <input type="text" id="pub-biblioid" name="pub-biblioid"/> </td> </tr> </tbody> <tbody> <tr> <td> <label for="pub-file"> File image: </label> </td> <td> <input type="file" id="pub-file" accept="image/*"/> </td> </tr> <tr> <td> <label for="pub-content-url"> Online-file image URL: </label> </td> <td> <input type="text" id="pub-content-url" name="pub-content-url"/> </td> </tr> </tbody> </table> <div class="button-pane"> <input type="button" id="add-button" value="Add Publication" /> <input type="reset" id="register-form-reset"/> <span id="action-status"></span> </div> </form> <form id="delete-form"> <div> <label for="pub-biblioid-to-delete" class="required"> Key (for example 1, 2, 3, etc.): </label> <input type="text" id="pub-biblioid-to-delete" name="pub-biblioid-to-delete" /> </div> <div class="button-pane"> <input type="button" id="delete-button" value="Delete Publication" /> </div> </form> <form id="search-form"> <div class="button-pane"> <input type="button" id="search-list-button" value="List database content" /> </div> </form> <div> <div id="pub-msg"> </div> <ul id="pub-list"> </ul> </div>CSS 内容
body { font-size: 0.8em; font-family: Sans-Serif;}form { background-color: #cccccc; border-radius: 0.3em; display: inline-block; margin-bottom: 0.5em; padding: 1em;}table { border-collapse: collapse;}input { padding: 0.3em; border-color: #cccccc; border-radius: 0.3em;}.required:after { content: "*"; color: red;}.button-pane { margin-top: 1em;}#documents-list { background-color: #eeeeee;}.action-success { margin-left: 1em; padding: 0.5em; color: #00d21e; background-color: #eeeeee; border-radius: 0.2em;}.action-failure { margin-left: 1em; padding: 0.5em; color: #ff1408; background-color: #eeeeee; border-radius: 0.2em;}.presence-no { font-style: italic;}.note { font-size: smaller;}JavaScript 内容
(function () { // Works with: // * Firefox >= 16.0 // * Google Chrome >= 24.0 (you may need to get Google Chrome Canary) const DB_NAME = 'mdn-demo-indexeddb-epublications'; const DB_VERSION = 1; // Use a long long for this value (don't use a float) const DB_STORE_NAME = 'publications'; var db; function initDb() { console.debug("initDb ..."); var req = indexedDB.open(DB_NAME, DB_VERSION); req.onsuccess = function (evt) { // Better use "this" than "req" to get the result to avoid problems with // garbage collection. // db = req.result; db = this.result; console.debug("initDb DONE"); }; req.onerror = function (evt) { console.error("initDb:", evt.target.errorCode); }; req.onupgradeneeded = function (evt) { console.debug("initDb.onupgradeneeded"); var store = evt.currentTarget.result.createObjectStore( DB_STORE_NAME, { keyPath: 'id', autoIncrement: true }); store.createIndex('biblioid', 'biblioid', { unique: true }); store.createIndex('title', 'title', { unique: false }); store.createIndex('year', 'year', { unique: false }); }; } function getFile(key, success_callback) { var tx = db.transaction(DB_STORE_NAME, 'readonly'); var store = tx.objectStore(DB_STORE_NAME); var req = store.get(key); req.onsuccess = function(evt) { var value = evt.target.result; if (value) success_callback(value.file); }; } function displayPubList() { console.debug("displayPubList"); var pub_msg = $('#pub-msg'); pub_msg.empty(); var pub_list = $('#pub-list'); pub_list.empty(); var tx = db.transaction(DB_STORE_NAME, 'readonly'); var store = tx.objectStore(DB_STORE_NAME); var req; req = store.count(); // Requests are executed in the order in which they were made against the // transaction, and their results are returned in the same order. // Thus the count text below will be displayed before the actual pub list // (not that it is algorithmically important in this case). req.onsuccess = function(evt) { pub_msg.append('<p>There are <strong>' + evt.target.result + '</strong> record(s) in the object store.</p>'); }; req.onerror = function(evt) { console.error("add error", this.error); displayActionFailure(this.error); }; var i = 0; var img_id; var file_presence; var presence_html; req = store.openCursor(); req.onsuccess = function(evt) { var cursor = evt.target.result; if (cursor) { presence_html = "<span class='presence-no'>No image</span>"; file_presence = cursor.value.file != null; console.debug("cursor.value:", cursor.value); if (file_presence) { img_id = 'pub-img-' + i; presence_html = '<img id="' + img_id + '"/>'; getFile(cursor.key, function(file) { console.debug("file:", file); // Note that here it is not possible to set a link to the file to // make it possible to download it. // The only possible options are: // * display the file if it is an image // * getting text/other info from the file and display them var obj_url = window.URL.createObjectURL(file); $('#' + img_id).attr('src', obj_url); window.URL.revokeObjectURL(obj_url); }); } pub_list.append('<li>' + '[' + cursor.key + '] ' + '(biblioid: ' + cursor.value.biblioid + ') ' + cursor.value.title + ' - ' + cursor.value.year + ' / ' + presence_html + '</li>'); // Move on to the next object in store cursor.continue(); // This counter serves only to create distinct img ids i++; } else { console.debug("No more entries!"); } }; }; function addPublication(biblioid, title, year, file) { console.debug("addPublication arguments:", arguments); if (!db) { console.error("addPublication: the db is not initialized"); return; } var tx = db.transaction(DB_STORE_NAME, 'readwrite'); var store = tx.objectStore(DB_STORE_NAME); var req = store.add({ biblioid: biblioid, title: title, year: year, file: file }); req.onsuccess = function (evt) { console.debug("Insertion in DB successful"); displayPubList(); }; req.onerror = function() { console.error("add error", this.error); displayActionFailure(this.error); }; } function displayActionSuccess(msg) { msg = typeof msg !== 'undefined' ? "Success: " + msg : "Success"; $('#action-status').html('<span class="action-success">' + msg + '</span>'); } function displayActionFailure(msg) { msg = typeof msg !== 'undefined' ? "Failure: " + msg : "Failure"; $('#action-status').html('<span class="action-failure">' + msg + '</span>'); } function resetActionStatus() { console.debug("resetActionStatus ..."); $('#action-status').empty(); console.debug("resetActionStatus DONE"); } function addEventListeners() { console.debug("addEventListeners"); initDb(); $('#register-form-reset').click(function(evt) { resetActionStatus(); }); $('#add-button').click(function(evt) { console.debug("add ..."); var title = $('#pub-title').val(); var year = $('#pub-year').val(); var biblioid = $('#pub-biblioid').val(); if (!title || !year || !biblioid) { displayActionFailure("Required field(s) missing"); return; } var file_input = $('#pub-file'); var selected_file = file_input.get(0).files[0]; console.debug("selected_file:", selected_file); file_input.val(null); var content_url = $('#pub-content-url').val(); if (selected_file) { addPublication(biblioid, title, year, selected_file); } else { addPublication(biblioid, title, year); displayActionSuccess(); } }); $('#delete-button').click(function(evt) { console.debug("delete ..."); var k = $('#pub-biblioid-to-delete').val(); console.debug("delete k:", k); var tx = db.transaction(DB_STORE_NAME, 'readwrite'); var store = tx.objectStore(DB_STORE_NAME); // Warning: The exact same key used for creation needs to be passed for // the deletion. If the key was a Number for creation, then it needs to be // a Number for deletion. k = Number(k); // The code that could be nice if it worked // var req = store.delete(k); // req.onsuccess = function(evt) { // var record = evt.target.result; // console.debug("record:", record); // if (typeof record !== 'undefined') { // displayActionSuccess("Deletion successful"); // displayPubList(); // } else { // displayActionFailure("No matching record found"); // } // }; // req.onerror = function (evt) { // console.error("delete:", evt.target.errorCode); // }; // The code that actually works // // As per spec http://www.w3.org/TR/IndexedDB/#object-store-deletion-operation // the result of the Object Store Deletion Operation algorithm is // undefined, so it's not possible to know if some records were actually // deleted by looking at the request result. var req = store.get(k); req.onsuccess = function(evt) { var record = evt.target.result; console.debug("record:", record); if (typeof record !== 'undefined') { req = store.delete(k); req.onsuccess = function(evt) { console.debug("evt:", evt); console.debug("evt.target:", evt.target); console.debug("evt.target.result:", evt.target.result); console.debug("delete successful"); displayActionSuccess("Deletion successful"); displayPubList(); }; req.onerror = function (evt) { console.error("delete:", evt.target.errorCode); }; } else { displayActionFailure("No matching record found"); } }; req.onerror = function (evt) { console.error("delete:", evt.target.errorCode); }; }); var search_button = $('#search-list-button'); search_button.click(function(evt) { displayPubList(); }); }// function dbAddAndGetBlob(title, year, url) {// // Create XHR// var xhr = new XMLHttpRequest();// var blob;// // We can't use jQuery here because as of jQuery 1.7.2 the new "blob"// // responseType is not handled.// // http://bugs.jquery.com/ticket/7248// //// // Attention Same origin policy, the URL must come from the same origin than// // the web app.// xhr.open('GET', url, true);// // Set the responseType to blob// xhr.responseType = 'blob';// xhr.addEventListener("load", function () {// if (xhr.status === 200) {// console.debug("File retrieved");// // Blob as response// blob = xhr.response;// console.debug("Blob:" + blob);// // Insert the received blob into IndexedDB// dbAdd(title, year, blob);// } else {// console.error("loadConfigFormHtmlContent error:",// xhr.responseText, xhr.status);// }// }, false);// // Send XHR// xhr.send();// } addEventListeners();})(); // Immediately-Invoked Function Expression (IIFE){{ EmbedLiveSample('Full_IndexedDB_example', 800, 400) }}
下一步
如果你想要多试试这些 API 的用法,请跳转到 参考手册 以查看其他不同的用法。
另请参阅
参考
IndexedDB API 参考Indexed Database API 规范教程
A simple TODO list using HTML5 IndexedDB. {{Note("该教程基于旧版本的规范因此无法在最新版的浏览器上正常工作-它仍然使用已经被移除的setVersion() 方法。") }} Databinding UI Elements with IndexedDB相关文章
IndexedDB — The Store in Your BrowserStorage - html5demosFirefox
Mozilla interface files Using JavaScript Generators in Firefox本文由范圣刚翻译自 MDN -> IndexedDB -> Using IndexedDB,因为水平有限,翻译不当的地方请大家批评指正。