indexeddb
“ 使用HTML5数据库和离线功能,第1部分 ”介绍了HTML5规范中可用的离线应用程序和本地数据持久性选项,重点是localStorage。 本部分介绍Indexed Database(IndexedDB),它是HTML5标准的一部分,是一种健壮的数据持久性技术,并讨论了如何将IndexedDB数据提供程序与您在第一篇文章中创建的Contact Manager应用程序集成。
Contact Manager示例应用程序(用于名称,地址和电话号码)具有联机和脱机模式。 脱机时,数据驻留在本地持久存储中。 切换到联机模式后,简单的数据同步功能会将本地数据更改同步到服务器。 该应用程序在联机和脱机模式下均支持四种基本的持久存储功能(创建,读取,更新和删除[CRUD])。
应用程序架构概述
回顾一下,首先看一下图1,它显示了Contact Manager的体系结构。 服务器体系结构包含两个与业务服务和数据提供者相对应的servlet。 UI由一个HTML文件和四个JavaScript模块组成,并带有对jQuery库最新版本的外部引用。
在本文中,您将使用基于IndexedDB API的脱机数据库提供程序代替。 具体来说,您将替换localdb.js JavaScript模块。
图1. Contact Manager应用程序架构
![该图显示了应用程序体系结构,其中的框显示了客户端体系结构和服务器体系结构下的servlet。](https://i-blog.csdnimg.cn/blog_migrate/41263fcf8e3583b2bc96ad707eb8f2fc.png)
数据模型概述
数据模型由两个数据实体组成:联系和状态(请参见图2)。 联系人表包含实际联系人数据。 状态表包含状态选择列表的字典值。
图2. Contact Manager应用程序数据模型
![该图显示了数据模型](https://i-blog.csdnimg.cn/blog_migrate/bcdf42779f991307188bc0f4cbe6e2db.png)
IndexedDB API
HTML5规范包含几种持久的存储技术。 IndexedDB是首选HTML5浏览器数据库; 它替换了不推荐使用的WebSQL数据库。
Web浏览器支持和IndexedDB API的实现在Web浏览器中并不总是一致的。 在撰写本文时,Google Chrome 11 +,Mozilla Firefox 4+和Windows®InternetExplorer®10支持IndexedDB。
一个网站可以包含一个或多个用唯一名称标识的数据库。 每个数据库可以包含一个或多个对象存储 。 对象存储类似于关系数据库中的表,因为它由唯一的名称标识并且是记录的集合。 但是,对象存储处理数据存储,访问和查询的方式与关系数据库中表的处理方式不同。 对象存储中的数据与键和值一起存储。 密钥在对象存储中必须唯一,并且可以由密钥生成器指定或生成。
IndexedDB API规范包括对常见数据库结构的支持,例如事务,索引,查询数据和游标。 该规范包括同步和异步API。 同步API旨在在Web Worker中使用。 但是,并非所有的Web浏览器都支持Web Worker和IndexedDB同步API。 异步API使用请求和回调。 所有数据库操作,例如打开数据库,检索数据,查询数据和删除数据,都具有请求API调用。 每个请求都有一个onsuccess
和onerror
回调,分别在操作成功或不成功时调用。 回调为返回的数据提供事件结果参数。
样本Contact Manager应用程序由具有两个对象存储的单个数据库组成。 第一个存储是联系人对象存储,其中包含实际的联系人记录。 第二个对象存储是状态对象存储,其中包含状态选择列表的值。 本文演示了如何使用异步API进行数据访问。
连接到数据库或从数据库断开连接
由于每种Web浏览器实现IndexedDB的方式都略有不同,因此最好创建一个全局变量( localDatabase
)并根据不同的Web浏览器实现对其进行初始化。 此全局变量提供对IndexedDB API的引用。
下一步是使用open
方法打开数据库。 如果打开数据库请求成功,则调用onsuccess
回调。 所有数据库操作代码都应在onsuccess
回调中发生。 如果打开数据库时发生错误,则调用onerror
回调。 清单1显示了初始化和打开数据库的代码。
清单1.打开一个数据库
var localDatabase = {};
localDatabase.indexedDB = window.indexedDB || window.mozIndexedDB ||
window.webkitIndexedDB || window.msIndexedDB;
localDatabase.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
localDatabase.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
console.log('opening local database');
var openRequest = localDatabase.indexedDB.open(dbName);
openRequest.onerror = function(e) {
console.log("Database error: " + e.target.errorCode);
};
openRequest.onsuccess = function(event) {
console.log("open database request succeeded ");
console.log('set db');
db = openRequest.result;
...
};
要与数据库断开连接,只需调用IndexedDB数据库对象的close
方法。
创建对象存储
下一步是在数据库中创建对象存储。 您可以随时创建对象存储,但是创建对象存储后,只能在打开数据库请求的onupgradeneeded
回调中修改它。 该回调表明必须升级数据库。
清单2中的代码显示了如何创建两个对象存储。
清单2.创建对象存储
openRequest.onupgradeneeded = function (evt) {
console.log('creating object stores');
var contactsStore = evt.currentTarget.result.createObjectStore
(contactStore, {keyPath: "id"});
var statesStore = evt.currentTarget.result.createObjectStore
(stateStore, {keyPath: "itemId"});
console.log('object stores created');
};
keyPath
标识对象存储库的键字段。
为选择列表创建字典
使用事务创建和修改对象存储中的记录。 IndexedDB API为事务提供了三种模式:
-
readonly
—提供对对象存储库的readonly
访问。 -
readwrite
—提供对对象存储的读写访问。 -
versionchange
—除了对对象库的读写访问之外,还提供了创建和删除对象库的功能。
您使用以下语法创建事务:
var transaction = db.transaction("states", "readwrite");
创建事务后,下一步是获取对对象存储的引用。 transaction
对象包含objectstores
属性,该属性提供对与数据库关联的对象存储的访问。 要获取对states
对象存储的引用,请使用以下语句:
var store = transaction.objectStore("states");
连接到服务器后,您希望将所有本地states
数据替换为最新的服务器数据。 为此,您必须先清除状态对象存储,然后再使用服务器中的最新值填充状态对象存储。 在对象库上使用clear
方法可以做到这一点。
如果成功清除了对象存储,则会调用onsuccess
回调。 在此回调中,您现在可以循环浏览并将每个值添加到本地状态对象存储中。 您要添加到对象存储中的数据包含在stateArray
字符串数组中。 使用jQuery $.each
方法迭代数组。 对于states
数组中的每个值,调用put
方法将记录添加到状态对象存储中。
清单3中的代码片段显示了如何使用states
数组中的值填充对象存储以进行本地和脱机访问:
清单3.将状态数据保存在对象存储中
try {
console.log('saving local state data');
var openRequest = localDatabase.indexedDB.open(dbName);
openRequest.onerror = function(e) {
console.log("Database error: " + e.target.errorCode);
};
openRequest.onsuccess = function(event) {
db = openRequest.result;
console.log('opening states store');
var transaction = db.transaction("states", "readwrite");
var store = transaction.objectStore("states");
var clearReq = store.clear();
clearReq.onsuccess = function (ev) {
console.log('cleared state store');
$.each(stateArray, function(i,item){
var itemId = generateUUID();
var request = store.put({
"text": item,
"itemId" : itemId
});
request.onsuccess = function(e) {
};
request.onerror = function(e) {
console.log(e.value);
};
});
};
db.close();
};
}
catch(e){
console.log(e);
}
现在是时候离线进行添加和更新联系人记录了。
添加和更新联系人
本文使用与第1部分 (将脱机数据存储在localstorage
)相同的方法来创建和更新记录。 新记录由ID字段的唯一生成的负数标识。 负数表示该记录是新记录,必须在服务器数据库表中创建。 另外, isDirty
标志指示该记录在脱机时已被修改或创建。 使用put
方法将记录保存在对象存储中(类似于填充状态对象存储的方式)。 清单4显示了在对象存储中创建和更新记录的完整代码:
清单4.创建和更新联系人记录
openRequest.onsuccess = function(event) {
db = openRequest.result;
var transaction = db.transaction(contactStore, "readwrite");
var objectStore = transaction.objectStore(contactStore);
var id = $('#contactId').val();
var firstName = $('#firstName').val();
var lastName = $('#lastName').val();
var street1 = $('#street1').val();
var street2 = $('#street2').val();
var city = $('#city').val();
var zipCode= $('#zipCode').val();
var state= $('#state').val();;
if (contactId > 0) {
var getRequest = objectStore.get(parseInt(contactId));
getRequest.onsuccess = function(event)
{
var contact = event.target.result;
contact.firstName = firstName;
contact.lastName = lastName;
contact.street1 = street1;
contact.street2 = street2;
contact.city = city;
contact.zipCode = zipCode;
contact.isDirty = true;
contact.lastModifyDate = "";
contact.isDeleted = false;
var addRequest = objectStore.put(contact);
addRequest.onsuccess = function(event) {
recordUpdated=true;
};
addRequest.onerror = function(e) {
console.log(e.value);
};
};
getRequest.onerror = function(e) {
console.log(e.value);
};
} // if update
else {
var newContactId = (-1) * Math.floor(Math.random()*100000);
var lastModifyDate = "";
var newContact = {
"timeStamp": "",
"id":newContactId,
"firstName": firstName,
"lastName": lastName,
"street1": street1,
"street2": street2,
"city": city,
"zipCode": zipCode,
"state": state,
"isDirty":true,
"lastModifyDate": "",
"isDeleted":false };
var request = objectStore.put(newContact);
var nextIndex = data.length;
data[nextIndex] = newContact;
recordUpdated=true;
} // if create
接下来,我展示了如何在离线时删除联系人记录。
离线时删除联系人
脱机时删除联系人时,您不想删除记录。 而是,您希望阻止它显示在脱机联系人列表中,并指示该记录已删除。 您可以通过使用isDeleted
标志(类似于本系列第1部分中描述的方法)来实现。 isDirty
标志也设置为true
以指示该记录已脱机修改。 在对象存储中更改记录后,刷新联系人列表以从列表中删除记录(不显示isDeleted
标志设置为true
记录)。 清单5中的代码显示了如何完成此任务。
清单5.删除联系人记录
try {
console.log('deleting local contact');
var openRequest = localDatabase.indexedDB.open(dbName);
console.log('after open');
openRequest.onerror = function(e) {
console.log("Database error: " + e.target.errorCode);
};
openRequest.onsuccess = function(event) {
db = openRequest.result;
console.log('opening contacts store');
var transaction = db.transaction(contactStore, "readwrite");
var store = transaction.objectStore(contactStore);
var getRequest = store.get(contactId);
getRequest.onsuccess = function (ev) {
var item = getRequest.result;
item.isDeleted=true;
item.isDirty=true;
var request = store.put(item);
request.onsuccess = function(e) {
alert('Contact deleted');
loadOfflineContacts();
};
request.onerror = function(e) {
console.log(e.value);
};
}
getRequest.onerror = function(e) {
console.log(e.value);
};
db.close();
};
loadOfflineContacts();
}
catch(e){
console.log(e);
}
查询联系人
要从联系人对象存储中检索联系人记录,请使用IndexedDB游标。 就像关系数据库中的数据库游标一样,IndexedDB游标提供了一种遍历对象存储中记录的方式。 在遍历记录时,您将构建一个包含联系人记录的数组。 isDeleted
标志设置为true
任何记录都将被忽略。 清单6显示了如何使用联系人对象存储库中包含的数据构建联系人数组。
清单6.查询联系人
var data = new Array();
...
var cursorRequest = objectStore.openCursor();
cursorRequest.onsuccess = function(evt) {
var cursor = evt.target.result;
if (cursor) {
if (!cursor.value.isDeleted) {
var newContact = {
"timeStamp":cursor.value.timeStamp,
"id":cursor.value.id,
"firstName": cursor.value.firstName,
"lastName": cursor.value.lastName,
"street1": cursor.value.street1,
"street2": cursor.value.street2,
"city": cursor.value.city,
"zipCode": cursor.value.zipCode,
"state": cursor.value.state,
"lastModifyDate": cursor.value.lastModifyDate,
"isDeleted": cursor.value.isDeleted,
"isDirty": cursor.value.isDirty
};
//console.log('adding ' + newContact.toString());
//console.log("adding contact to array: " + data.length);
data[data.length]= newContact;
}
cursor.continue();
} // more records
else {
displayContactData(data);
} // no more records
}; // open cursor
接下来,我演示了一种简单的算法和方法,用于将脱机添加和修改与服务器同步。
与服务器同步本地数据
当您在线工作时,所有CRUD操作都使用Servlet,并且服务器数据库会立即更新。 本地(IndexedDB)数据库也会通过在线数据库更改进行更新,以确保最新数据始终在线或离线可用。
脱机时,所有CRUD操作都会更新IndexedDB数据库中的数据。 与服务器重新连接后:
- 在本地数据库中创建的所有记录都将保留到服务器。
- 在本地数据库中修改的所有记录都会在服务器上更新。
- 在本地数据库中删除的所有记录都将在服务器上删除。
清单7中的代码显示了完整的同步方法。 第1部分中描述的相同的在线功能用于创建,更新和删除操作。
第一步是使用游标遍历联系人对象存储中的记录,并构建一个包含所有需要发布到服务器的记录的数组。 在本地更新或创建的记录的isDirty
属性设置为true
。 如果保存操作的唯一记录ID为负(即未由MySQL数据库分配),则将其标识为新操作。 使用isDeleted
属性标记在本地删除的记录。
当您拥有包含所有需要发布到服务器的所有记录的数组时,请使用jQuery $.each
方法遍历该数组并将每次更改都发布到服务器。
最后,数据同步方法完成后,它将使用服务器中的最新数据刷新本地联系人对象存储。 这包括您(和其他用户)离线时所做的任何更改:
清单7.与服务器同步本地数据
...
cursorRequest.onsuccess = function(evt) {
var cursor = evt.target.result;
if (cursor) {
var isDirty = cursor.value.isDirty;
var curId = cursor.value.id;
console.log(curId + ' isDirty = ' + isDirty);
if (isDirty) {
var newContact = {
"timeStamp":cursor.value.timeStamp,
"id":curId,
"firstName": cursor.value.firstName,
"lastName": cursor.value.lastName,
"street1": cursor.value.street1,
"street2": cursor.value.street2,
"city": cursor.value.city,
"zipCode": cursor.value.zipCode,
"state": cursor.value.state,
"lastModifyDate": cursor.value.lastModifyDate,
"isDeleted": cursor.value.isDeleted,
"isDirty": cursor.value.isDirty
};
//console.log('adding ' + newContact.toString());
//console.log("adding contact to array: " + data.length);
data[data.length]= newContact;
}
cursor.continue();
} // more records
else {
console.log("no more records");
console.log('number of modified records: ' + data.length);
var recordsUpdated = 0;
var recordsCreated = 0;
var recordsDeleted = 0;
$.each(data, function(i,item){
console.log("processing record " + item.id);
if (item.isDeleted) {
deleteOnlineContact(item.id, true);
recordsDeleted++;
}
else if (item.isDirty && !item.isDeleted) {
$('input[name="contactId"]')[0].value = item.id;
$('input[name="firstName"]')[0].value = item.firstName;
$('input[name="lastName"]')[0].value = item.lastName;
$('input[name="street1"]')[0].value = item.street1;
$('input[name="street2"]')[0].value = item.street2;
$('input[name="city"]')[0].value = item.city;
$('select[name="state"]')[0].value = item.state;
$('input[name="zipCode"]')[0].value = item.zipCode;
var dataString = $("#editContactForm").serialize();
postEditedContact(dataString, true);
if (item.id > 0) {
recordsUpdated++;
}
else {
recordsCreated++;
}
}
});
var msg = "Synchronization Summary\n\tRecords Updated: " + recordsUpdated
+ "\n\tRecords Created: " + recordsCreated
+ "\n\tRecords Deleted: " + recordsDeleted;
alert(msg);
结论
本文以第1部分中描述的基础为基础。 它为联机和脱机支持以及维护一致的用户体验使用相同的模式。 它引入了IndexedDB API,这是一种数据同步算法,用于同步记录的脱机创建,删除和修改。 由于该代码没有强大的错误处理和冲突解决方案(当同一记录在本地由另一用户在服务器上修改时),因此该代码无法投入生产。 但是,它确实为将来的工作提供了良好的基础。
翻译自: https://www.ibm.com/developerworks/web/library/wa-html5db-pt2/index.html
indexeddb