本例使用indexedDB API设计一个电子刊物发布的应用,演示效果如图1所示。在示例页面中,显示3个表单框,第一个表单框登录电子刊物信息,并保存在indexedDB数据库中。第二个表单框用于管理数据库中的记录,可以根据需要清空全部记录,或者删除指定的记录。第三个表单框用于显示数据库中所有的电子刊物记录。
图1 电子刊物发布应用效果
【操作步骤】
第1步,设计HTML结构。整个页面包含2部分:第一部分是3个表单,分别用于实现信息录入、信息删除和信息显示;第2部分是4个包含框,其中顶部包含框用于操作提示,底部3个包含框用于显示记录信息。
- 第1个表单框:用于信息录入。
<form id="register-form"> <table><tbody><tr> <td><label for="pub-title" class="required">书名: </label></td> <td><input type="text" id="pub-title" name="pub-title" /></td> </tr><tr> <td><label for="pub-biblioid" class="required"> 出版编号ID:<br/> <span class="note">(ISBN, ISSN, etc.)</span> </label></td> <td><input type="text" id="pub-biblioid" name="pub-biblioid"/></td> </tr> <tr> <td><label for="pub-year">出版日期(年份): </label></td> <td><input type="number" id="pub-year" name="pub-year" /></td> </tr> </tbody><tbody><tr> <td><label for="pub-file">封面图片: </label></td> <td><input type="file" id="pub-file"/></td> </tr><tr> <td><label for="pub-file-url">封面图片在线URL:<br/> <span class="note">(同源URL)</span> </label></td> <td><input type="text" id="pub-file-url" name="pub-file-url"/></td> </tr> </tbody> </table> <div class="button-pane"> <input type="button" id="add-button" value="添加发布" /> <input type="reset" id="register-form-reset"/> </div> </form>
- 第2个表单框:用于记录删除。
<form id="delete-form"> <table><tbody> <tr> <td><label for="pub-biblioid-to-delete">出版编号ID:<br/> <span class="note">(ISBN, ISSN, etc.)</span> </label></td> <td><input type="text" id="pub-biblioid-to-delete" name="pub-biblioid-to-delete" /></td> </tr><tr> <td><label for="key-to-delete">索引:<br/> <span class="note">(如1、2、3等)</span> </label></td> <td><input type="text" id="key-to-delete" name="key-to-delete" /></td> </tr> </tbody> </table> <div class="button-pane"> <input type="button" id="delete-button" value="删除发布" /> <input type="button" id="clear-store-button" value="清空所有发布信息" class="destructive" /> </div> </form>
- 第3个表单框:用于显示记录。
<form id="search-form"> <div class="button-pane"> <input type="button" id="search-list-button" value="显示数据库中所有发布信息" /> </div> </form>
- 顶部的操作提示提示包含框。
<h1>电子出版物仓储</h1> <div class="note"> <p>浏览器支持: </p> <div id="compat"> </div> </div> <div id="msg"> </div>
- 底部包含3个包含框,用于显示记录信息。
<div> <div id="pub-msg"> </div> <div id="pub-viewer"> </div> <ul id="pub-list"> </ul> </div>
第2步,设计Javascript脚本部分。把所有Javascript代码都放在一个函数表达式中,并进行调用,这样做的目的是定义一个独立的作用域,把其中所有的变量与外界代码隔绝起来。首先,初始化所有变量,并重置UI标签。
(function () { var COMPAT_ENVS = [ ['Firefox', ">= 16.0"], ['Google Chrome', ">= 24.0 (可能需谷歌浏览器),没有BLOB存储支持"] ]; var compat = $('#compat'); compat.empty(); compat.append('<ul id="compat-list"></ul>'); COMPAT_ENVS.forEach(function(val, idx, array) { $('#compat-list').append('<li>' + val[0] + ': ' + val[1] + '</li>'); }); const DB_NAME = 'mdn-demo-indexeddb-epublications'; const DB_VERSION = 1; // 使用long long型值(不要使用float值) const DB_STORE_NAME = 'publications'; var db; // 用来跟踪哪些视图显示,避免无用的加载它 var current_view_pub_key; })(); // 执行函数表达式 (IIFE)
第3步,创建本地数据库,设计数据表,定义字段类型。
function openDb() { console.log("openDb ..."); var req = indexedDB.open(DB_NAME, DB_VERSION); req.onsuccess = function (evt) { // 更好地使用this比req得到的结果好,以避免垃圾回收问题 // db = req.result; db = this.result; console.log("openDb DONE"); }; req.onerror = function (evt) { console.error("openDb:", evt.target.errorCode); }; req.onupgradeneeded = function (evt) { console.log("openDb.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 }); }; }
第4步,定义数据库操作函数。getObjectStore()用于读取匹配的记录,clearObjectStore()用于清除数据库中记录表,getBlob()能够根据键值找到本地数据库中存储的附件文件,displayPubList()函数能够从本地数据库中查询所有记录,并显示在页面中。
//* @参数 {string} store_name //* @参数 {string} mode 可以是"readonly"或"readwrite" function getObjectStore(store_name, mode) { var tx = db.transaction(store_name, mode); return tx.objectStore(store_name); } function clearObjectStore(store_name) { var store = getObjectStore(DB_STORE_NAME, 'readwrite'); var req = store.clear(); req.onsuccess = function(evt) { displayActionSuccess("Store cleared"); displayPubList(store); }; req.onerror = function (evt) { console.error("clearObjectStore:", evt.target.errorCode); displayActionFailure(this.error); }; } function getBlob(key, store, success_callback) { var req = store.get(key); req.onsuccess = function(evt) { var value = evt.target.result; if (value) success_callback(value.blob); }; } // * @参数 {IDBObjectStore=} store function displayPubList(store) { console.log("displayPubList"); if (typeof store == 'undefined') store = getObjectStore(DB_STORE_NAME, 'readonly'); var pub_msg = $('#pub-msg'); pub_msg.empty(); var pub_list = $('#pub-list'); pub_list.empty(); // 重置iframe,不显示以前的内容 newViewerFrame(); var req; req = store.count(); req.onsuccess = function(evt) { pub_msg.append('<p>在对象仓库中共计有 <strong>' + evt.target.result + '</strong> 记录。</p>'); }; req.onerror = function(evt) { console.error("add error", this.error); displayActionFailure(this.error); }; var i = 0; req = store.openCursor(); req.onsuccess = function(evt) { var cursor = evt.target.result; // 如果游标指向某个位置,则访问该数据. if (cursor) { console.log("displayPubList cursor:", cursor); req = store.get(cursor.key); req.onsuccess = function (evt) { var value = evt.target.result; var list_item = $('<li>' + '[' + cursor.key + '] ' + '(ID:' + value.biblioid + ') ' + value.title + '</li>'); if (value.year != null) list_item.append(' - ' + value.year); if (value.hasOwnProperty('blob') && typeof value.blob != 'undefined') { var link = $('<a href="' + cursor.key + '">附件</a>'); link.on('click', function() { return false; }); link.on('mouseenter', function(evt) { setInViewer(evt.target.getAttribute('href')); }); list_item.append(' / '); list_item.append(link); } else { list_item.append(" / 没有附件"); } pub_list.append(list_item); }; // 移动到存储中的下一个对象 cursor.continue(); // 此计数器仅用于创建不同的id。 i++; } else { console.log("没有更多的条目"); } }; }
第5步,定义视图操作函数。newViewerFrame()函数用于在<div id="pub-viewer">包含框中嵌入一个浮动框架,以便显示附件文件信息;setInViewer()函数根据键值,把对应附件文件中图片以HTML字符串形式在浮动框架中显示。
function newViewerFrame() { var viewer = $('#pub-viewer'); viewer.empty(); var iframe = $('<iframe />'); viewer.append(iframe); return iframe; } function setInViewer(key) { console.log("setInViewer:", arguments); key = Number(key); if (key == current_view_pub_key) return; current_view_pub_key = key; var store = getObjectStore(DB_STORE_NAME, 'readonly'); getBlob(key, store, function(blob) { console.log("setInViewer blob:", blob); var iframe = newViewerFrame(); if (blob.type == 'text/html') { var reader = new FileReader(); reader.onload = (function(evt) { var html = evt.target.result; iframe.load(function() { $(this).contents().find('html').html(html); }); }); reader.readAsText(blob); } else if (blob.type.indexOf('image/') == 0) { iframe.load(function() { var img_id = 'image-' + key; var img = $('<img id="' + img_id + '"/>'); $(this).contents().find('body').html(img); var obj_url = window.URL.createObjectURL(blob); $(this).contents().find('#' + img_id).attr('src', obj_url); window.URL.revokeObjectURL(obj_url); }); } else if (blob.type == 'application/pdf') { $('*').css('cursor', 'wait'); var obj_url = window.URL.createObjectURL(blob); iframe.load(function() { $('*').css('cursor', 'auto'); }); iframe.attr('src', obj_url); window.URL.revokeObjectURL(obj_url); } else { iframe.load(function() { $(this).contents().find('body').html("没有查看可用"); }); } }); }
第6步,向数据库中添加记录。其中addPublicationFromUrl()函数根据在线URL,把电子出版物的相关字段信息添加到本地数据库中;addPublication()函数根据用户选择的附件文件,把附件文件以Blob对象的形式保存到本地数据库中。
//* @参数 {string} biblioid //* @参数 {string} title //* @参数 {number} year //* @参数 {string} url 图像下载的URL,存储在本地IndexedDB数据库 function addPublicationFromUrl(biblioid, title, year, url) { console.log("addPublicationFromUrl:", arguments); var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'blob'; xhr.onload = function (evt) { if (xhr.status == 200) { console.log("Blob恢复"); var blob = xhr.response; console.log("Blob:", blob); addPublication(biblioid, title, year, blob); } else { console.error("addPublicationFromUrl error:", xhr.responseText, xhr.status); } }; xhr.send(); } //* @参数 {string} biblioid //* @参数 {string} title //* @参数 {number} year //* @参数 {Blob=} blob function addPublication(biblioid, title, year, blob) { console.log("添加发布的参数:", arguments); var obj = { biblioid: biblioid, title: title, year: year }; if (typeof blob != 'undefined') obj.blob = blob; var store = getObjectStore(DB_STORE_NAME, 'readwrite'); var req; try { req = store.add(obj); } catch (e) { if (e.name == 'DataCloneError') displayActionFailure("这个引擎不知道如何克隆一个Blob, " + "使用Firefox"); throw e; } req.onsuccess = function (evt) { console.log("添加成功"); displayActionSuccess(); displayPubList(store); }; req.onerror = function() { console.error("addPublication error", this.error); displayActionFailure(this.error); }; }
第7步,删除数据库中记录。其中deletePublicationFromBib()函数能够根据id信息删除指定的记录,deletePublication()能够根据键值删除指定记录。
//* @参数 {string} biblioid function deletePublicationFromBib(biblioid) { console.log("deletePublication:", arguments); var store = getObjectStore(DB_STORE_NAME, 'readwrite'); var req = store.index('biblioid'); req.get(biblioid).onsuccess = function(evt) { if (typeof evt.target.result == 'undefined') { displayActionFailure("没有匹配的记录"); return; } deletePublication(evt.target.result.id, store); }; req.onerror = function (evt) { console.error("deletePublicationFromBib:", evt.target.errorCode); }; } //* @参数 {number} key //* @参数 {IDBObjectStore=} store function deletePublication(key, store) { console.log("deletePublication:", arguments); if (typeof store == 'undefined') store = getObjectStore(DB_STORE_NAME, 'readwrite'); var req = store.get(key); req.onsuccess = function(evt) { var record = evt.target.result; console.log("记录:", record); if (typeof record == 'undefined') { displayActionFailure("没有匹配的记录"); return; } req = store.delete(key); req.onsuccess = function(evt) { console.log("evt:", evt); console.log("evt.target:", evt.target); console.log("evt.target.result:", evt.target.result); console.log("删除成功"); displayActionSuccess("删除成功"); displayPubList(store); }; req.onerror = function (evt) { console.error("删除发布:", evt.target.errorCode); }; }; req.onerror = function (evt) { console.error("删除发布:", evt.target.errorCode); }; }
第8步,定义各种提示信息函数。
function displayActionSuccess(msg) { msg = typeof msg != 'undefined' ? "Success: " + msg : "成功"; $('#msg').html('<span class="action-success">' + msg + '</span>'); } function displayActionFailure(msg) { msg = typeof msg != 'undefined' ? "Failure: " + msg : "失败"; $('#msg').html('<span class="action-failure">' + msg + '</span>'); } function resetActionStatus() { console.log("更新状态中 ..."); $('#msg').empty(); console.log("已完成更新"); }
第9步,定义事件监听函数,主要是根据用户单击的按钮,分别调用对应的操作函数。
function addEventListeners() { console.log("addEventListeners"); $('#register-form-reset').click(function(evt) { resetActionStatus(); }); $('#add-button').click(function(evt) { console.log("添加中 ..."); var title = $('#pub-title').val(); var biblioid = $('#pub-biblioid').val(); if (!title || !biblioid) { displayActionFailure("所需字段丢失"); return; } var year = $('#pub-year').val(); if (year != '') { //如果引擎支持EcmaScript 6,最好使用Number.isInteger if (isNaN(year)) { displayActionFailure("Invalid year"); return; } year = Number(year); } else { year = null; } var file_input = $('#pub-file'); var selected_file = file_input.get(0).files[0]; console.log("选定的文件:", selected_file); var file_url = $('#pub-file-url').val(); if (selected_file) { addPublication(biblioid, title, year, selected_file); } else if (file_url) { addPublicationFromUrl(biblioid, title, year, file_url); } else { addPublication(biblioid, title, year); } }); $('#delete-button').click(function(evt) { console.log("删除中 ..."); var biblioid = $('#pub-biblioid-to-delete').val(); var key = $('#key-to-delete').val(); if (biblioid != '') { deletePublicationFromBib(biblioid); } else if (key != '') { // 如果引擎支持EcmaScript 6,最好使用Number.isInteger if (key == '' || isNaN(key)) { displayActionFailure("非法的key"); return; } key = Number(key); deletePublication(key); } }); $('#clear-store-button').click(function(evt) { clearObjectStore(); }); var search_button = $('#search-list-button'); search_button.click(function(evt) { displayPubList(); }); }
第10步,页面初始化操作。当页面加载完成之后,调用openDb()函数创建数据库,调用addEventListeners()函数开始监听各个按钮的操作。
openDb(); addEventListeners();