使用 HTML5 数据库和离线功能

提供离线数据编辑和数据同步化

HTML5 将扩展 Web 体验和业务线应用程序。有了离线功能和本地持久存储特性,您不管是在线还是离线状态都可以交付同样丰富的用户体验,这些体验以前只可在专用的桌面应用程序开发框架中获得。在本文中,您将了解如何利用 HTML5 的离线功能和本地持久存储特性。示例应用程序说明了如何避免常见的问题。

Brian J Stewart, 首席顾问, Aqua Data Technologies, Inc.

Brian Stewart 的照片Brian J. Stewart 目前是 Aqua Data Technologies 的一位首席顾问。他是这家公司的创始人,该公司关注内容管理、XML 技术和企业客户端/服务器与 Web 系统。他架构并开发了基于 J2EE 和 .NET 平台的企业解决方案。



2012 年 11 月 30 日

简介

常用缩写词

  • CRUD:创建、读取、更新和删除
  • HTML:超文本标记语言
  • JSON:JavaScript 对象表示法
  • LOB:业务线
  • UI:用户界面

HTML 版本 5 (HTML5) 并不期望在 2012 年之前获得万维网联盟 (W3C) 推荐。尽管 HTML5 不是官方标准,但 Web 浏览器供应商正在增加和营销 HTML5 特性。HTML5 正在为 Internet 网站和业务线 (LOB) 应用程序扩展 Web 体验。像 Amazon Kindle Cloud Reader 等许多网站已经开始使用 HTML5。有两个关键的 HTML5 特性会显著更改 LOB 应用程序:离线应用程序支持和本地持久存储。由于 HTML5 不是一个官方标准,所以浏览器支持是不一致的。

在本文中,您将了解由 HTML5 标准提供的有关离线应用程序支持和不同持久存储功能的信息。示例应用程序有助于说明这些特性。


示例应用程序

Contact Manager 示例应用程序专为管理联系信息(姓名、地址和电话号码)而提供的。它提供了一种在线模式、一种离线模式,以及一种简单的数据同步化功能,以使本地数据更改与切换到在线模式服务器实现同步。当处于离线状态时,数据于本地持久存储。应用程序支持在线和离线模式下的四个基本持久存储功能:创建、读取、更新和删除 (CRUD)。

架构

图 1 展示了 Contact Manager 应用程序架构的概述。服务器架构包括两个 servlet:业务服务和数据提供程序。用户界面包括一个单一的 HTML 文件、四个 JavaScript 模块,以及一个 jQuery 库最新版本的外部引用。

图 1. 应用程序架构概述
该图显示了应用程序架构

数据模型

数据模型包括两个数据实体:联系人和状态,如 图 2 所示。联系人表包含实际的联系人数据;状态表包含状态选择列表的字典值。

图 2. 数据模型
该图显示了数据模型

服务器界面

服务器界面包含两个 servlet:ContactServletDictionaryServlet。表 1对 servlet 进行了总结。(servlet 的实现和相应业务服务与数据提供程序不在本文的讨论范围之内。)

表 1. servlet 摘要
Servlet 名称操作参数描述
DictionaryServletcode>getstatesN/A以 JavaScript 对象表示法 (JSON) 格式返回一个状态数组。
ContactServletgetallcontactsN/A以 JSON 格式返回一个联系人数组。
ContactServletdeletecontactId:需要删除的联系人 ID。删除指定的联系人记录;返回一个 JSON 对象,并带有一个 Boolean 标志来表明操作是否成功。
"{"result": true/false"}
ContactServletsave
  • contactId:需要保存的联系人 ID。(如果大于 0,则是一项更新操作。)
  • firstName:第一个名称字段的值。
  • lastName:最后一个名称字段的值。
  • street1:街道 1 字段的值。
  • street2:街道 2 字段的值。
  • city:城市字段的值。
  • state:状态字段的值。
  • zipCode:邮编字段的值。
返回一个带有 Boolean 标识的 JSON 对象,用于表明操作是否成功,是否是新的或更新的联系人 ID。
"{"contactId": <id>, "result": <true/false>"}

调用服务器界面

清单 1 中的代码显示了如何对联系人 servlet 进行异步调用来检索存储于在线数据库中的联系人。代码使用了 jQuery getJSON 函数来调用联系人 servlet。

清单 1. 从服务器中检索联系人
function loadOnlineContacts() 
{
	$('#contactList').empty();
	$('#contactList').append('Loading contact data...');
	
	var url = '/html5app/contact?operation=getallcontacts';
	
	$.getJSON(url, function(data) {
		saveOfflineContactData(data);
		displayContactData(data);		
	});
}

清单 2 中的代码显示了如何将一个新的或者更新的联系人保存到服务器中。它使用了 jQuery ajax 函数。代码使用 HTTP POST 将数据发送到联系人 servlet。

清单 2. 把联系人保存到服务器中
function postEditedContact(dataString) {
	postEditedContact(dataString, false);	
}

function postEditedContact(dataString, suppressAlert) {
var contactId = $('input[name="contactId"]')[0].value;

$.ajax({  
type: "POST",  
url: "/html5app/contact",  
data: dataString,  
cache: false,
dataType: "json",
success: function(data) {
	var result = data.result;
	
	if (result) 
	{
		if (contactId > 0)
		{
		if (!suppressAlert) {
		alert("Contact was successfully updated.");
		}
		var lastModifyDate = data.lastModifyDate;
		$('input[name="lastModifyDate"]')[0].value = lastModifyDate;
		}
		else 
		{
		if (!suppressAlert) {
			alert("Contact was successfully created.");
		}
		
		var lastModifyDate = data.lastModifyDate;
		$('input[name="lastModifyDate"]')[0].value = lastModifyDate;
		$('input[name="contactId"]')[0].value = data.contactId;
		}
		
		loadOnlineContacts();

		hideEditForm();
	}
	else 
	{
		alert('An error occurred saving contact ' + contactId + '.');
	}
}
});  
}

最后一个函数从在线数据库中删除记录。清单 3 显示了如何从服务器中删除记录。代码使用 jQuery getJSON 函数来调用联系人 servlet。

清单 3. 删除服务器上的联系人
function deleteOnlineContact(contactId, suppressAlert){
	var url = '/html5app/contact?operation=delete&contactId=' + contactId;
	
	$.getJSON(url, function(data) {
		var result = data.result;
		if (result) {
			if (!suppressAlert) {
				alert('Contact deleted');
			}
			loadOnlineContacts();
		}
		else {
			alert('Contact ' + contactId + 'not deleted');
		}
	});		
}

构建本地数据提供程序

本地数据提供程序在本地保存所有的选择列表和联系人数据。HTML5 规范包含用于持久存储的内容。使用哪一种技术取决于您的数据存储和浏览器支持。以下部分将讨论三种持久存储技术,以及使用所有 Web 浏览器支持的持久存储技术实现一个本地数据提供程序。

三种持久存储技术与 HTML5 有关:

  • localStorage:localStorage 使用平键-值存储来提供简单的数据存储。主要的 Web 浏览器,包括 Apple® Safari®、Google Chrome™、Microsoft® Windows® Internet Explorer®、Mozilla® Firefox® 和 Opera™ 均支持 localStorage。HTML5 localStorage 可实现同步,而且是如今惟一一个跨平台和跨浏览器支持数据库的存储机制。
  • WebSQL:WebSQL 最初的目的是为 Web 浏览器带来一个基于 Transact-SQL 的数据库。学习曲线很少以它同常见关系数据库的相似性为基础,这些关系数据库有 IBM® DB2®、Microsoft SQL Server®、Oracle® MySQL® Server 和 Oracle Database。在 Safari、Chrome 和 Opera 等浏览器中均支持 WebSQL。但是 Firefox 或 Internet Explorer 并不支持 WebSQL。WebSQL 可能会逐渐被淘汰掉,因为已经不再对 WebSQL 推荐的规范进行开发。
  • 索引数据库 (Indexed DB):Indexed DB 是一种索引的层次性键-值存储,类似于许多商用云数据存储产品。放弃 WebSQL 对 Indexed DB 有利,而且现在 Firefox 和 Chrome 都支持 Indexed DB,将来发行的 Internet Explorer 10 也将提供相关支持。用于 Indexed DB 的应用程序编程接口 (API) 是异步的,支持索引、查询和事务。

本文中的示例解决方案利用 JSON 和 localStorage 来实现持久存储,主要是因为 localStorage 受到浏览器的广泛支持。

本地数据提供程序

localStorage 方法通过将联系人和字典数据序列化为一个 JSON 字符串,并将字符串保存在 localStorage 中,从而持久保存 approach 联系人和字典数据。当检索数据时,它就被并行化为一个 JSON 对象的数组并进行相应的处理。

在本地保存数据

清单 4 显示了如何将联系人数据保存到 localStorage 中。JavaScript 函数 JSON.stringify 用于序列化 JSON 数据,服务器将这些数据返回到一个字符串,这样它才能存储在 localStorage 中。

清单 4. 将数据保存到 localStorage 中
// fetch data from server
...
			
// convert JSON data to a string
var contactDataString = JSON.stringify(data);

// persist contact data to localstorage
localStorage.setItem("contactData", contactDataString);

在本地检索数据

清单 5 显示了如何在 localStorage 中检索数据。第一步是从 localStorage 中获取 JSON 字符串。接着使用 JavaScript 函数 eval 将字符串转换成 JSON 对象。使用自定义 JavaScript 函数 displayContactData 来显示数据。

清单 5. 从 localStorage 中读取数据
function loadOfflineContacts() 
{
	$('#contactList').empty();
	$('#contactList').append('Loading contact data...');
	
	var dataStr = localStorage.getItem("contactData");
	var data = eval('(' + dataStr + ')');
	
	displayContactData(data);
}

在本地删除记录

清单 6 显示了如何从 localStorage 中删除一个记录。

清单 6. 从 localStorage 中删除一个记录
function deleteOfflineContact(contactId) {
	var dataStr = localStorage.getItem("contactData");
	var data = eval('(' + dataStr + ')');
	
	var recordUpdated = false;
	$.each(data, function(i,item){
		if (item.id == contactId) {
			item.isDeleted=true;
			recordUpdated = true;
			return false;
		}	
	});
	
	if (recordUpdated) {
		dataStr = JSON.stringify(data);
		localStorage.setItem("contactData", dataStr);
		alert("Contact was successfully deleted.");
		
		loadOfflineContacts();
	}
}

代码为:

  • 从本地存储中读取数据库并使其并行。
  • 彻底迭代记录直至找到 contactId 记录为止。
  • isDeleted 标志设置为 true
  • 在数据同步函数中使用 isDeleted 标志。(参见 “数据同步” 部分。)
  • 将数据持久保存到 localStorage 中,并刷新数据网格。

在本地更新和创建记录

清单 7 显示了如何在 localStorage 更新或创建一个记录。

清单 7. 在 localStorage 更新记录
function updateLocalContact() {
	var dataStr = localStorage.getItem("contactData");
	var data = eval('(' + dataStr + ')');
	
	var contactId = $('input[name="contactId"]')[0].value;
	
	var recordUpdated = false;
	if (contactId > 0) {
		$.each(data, function(i,item){
			if (item.id == contactId) {
				item.isDirty=true;
				item.firstName = $('input[name="firstName"]')[0].value;
				item.lastName = $('input[name="lastName"]')[0].value;
				item.street1 = $('input[name="street1"]')[0].value;
				item.street2 = $('input[name="street2"]')[0].value;
				item.city = $('input[name="city"]')[0].value;
				item.state = $('select[name="state"]')[0].value;
				item.zipCode = $('input[name="zipCode"]')[0].value;	
				recordUpdated = true;
				return false;
			}	
		});
	}
	else {		
		var newContactId = 0;
		var nextId = 0;
		while(newContactId == 0) {
			var found = false;

			nextId = nextId - 1;
			$.each(data, function(i,item){
				if (item.id == nextId) {
					found = true;
					return false;
				}
			});
			if (!found) {
				newContactId = nextId;
			}
		}
		var lastModifyDate = "";
		var newContact = {"street2": $('input[name="street2"]')[0].value,
				"id":newContactId,
				"street1":$('input[name="street1"]')[0].value,
				"lastName":$('input[name="lastName"]')[0].value,
				"isDirty":true,
				"zipCode":$('input[name="zipCode"]')[0].value,
				"state":$('select[name="state"]')[0].value,
				"lastModifyDate": lastModifyDate,
				"isDeleted":false,
				"firstName":$('input[name="firstName"]')[0].value,
				"city":$('input[name="city"]')[0].value};
		var nextIndex = data.length;
		data[nextIndex] = newContact;
		recordUpdated=true;
	} 
	
	if (recordUpdated) {
		dataStr = JSON.stringify(data);
		localStorage.setItem("contactData", dataStr);
		alert("Contact was successfully updated.");
	}
	
	hideEditForm();
}

如 清单 7 所示,您可以:

  • 从 localStorage 中读取数据库并将其并行化。
  • 如果将要保存的记录的 contactId 不是 0(已更新),查找记录直至找到 contactId 记录。随后对其进行相应的更新。
  • 或者,如果记录是新的(contactId 不是 0),查找下一个为负值的 contactId
  • 将其分配到新纪录。
  • 将新纪录加到数据库。

数据随后被序列化为一个 JSON 字符串并保存到 localStorage 中。一个有效的 contactId(大于 0)将会在服务器同步化期间进行分配。为负值的 ID 是一个临时的 ID,用于将记录确定为新的。

了解 localStorage 很重要:

  • 限制为 5MB。(Indexed DB 应当在需要更多的数据存储时使用。)
  • 所有主要的 Web 浏览器支持。
  • 只使用字符串值工作。

下一步是构建使用 HTML5 的 UI。


构建使用 HTML5 的 UI

Contact Manager 示例应用程序有一个简单的 UI,并带有一个单一页面。它支持编辑和删除记录,并提供创建新纪录的功能。级联样式表 (CSS) 和动态 HTML(通过 jQuery)用于在必要时隐藏和显示创建/编辑子表单。

为了提供一致的用户体验,无论是在线还是离线状态都使用相同的页面;惟一的区别是数据提供程序是在执行操作时才调用。图 3 显示了该应用程序。

图 3. Contact Manager 应用程序
该图显示了 Contact Manager 应用程序

JavaScript 模块

应用程序由四个自定义 JavaScript 模块组成:

  • core.js 提供常见的 JavaScript 函数并由另一个模块使用。
  • formEvents.js 提供表单和按钮事件处理。它根据在线或离线状态将数据库分派到正确的数据提供程序。
  • onlinedb.js 提供在线时与服务器通信的函数。
  • offlinedb.js 提供本地数据存储函数。

所有模块也使用最新版本的 jQuery 库遍历数据,从而形成异步的 Web 请求和动态 HTML。客户端使用 JSON 与服务器通信。

离线应用程序清单

HTML5 离线功能提供用于缓存的静态文件和资源。离线应用程序清单文件 (.appcache) 是为 Web 应用程序启动离线应用程序支持的关键文件。清单文件定义以下信息:

  • 哪些资源和页面在离线状态时可用。
  • 哪些资源只在线状态时可用。
  • 离线状态时显示资源的回退页面不可用。

清单文件包括三个部分:CACHENETWORKFALLBACKCACHE 下的页面和资源在本地缓存。NETWORK 下的页面和资源从不缓存,且在线时惟一可用。离线状态下被请求的页面不可用时显示 FALLBACK 中指定的页面。NETWORK 部分中的星号 (*) 确保所有其他页面和 servlet 在线时惟一可用。如果丢失了 *,servlet 调用将失败(即使在线状态时也如此)。清单 8 显示了用于 Contact Manager 的清单文件。

清单 8. 离线应用程序清单
CACHE MANIFEST
# Revision 1
CACHE:
default.html
list.html
scripts/core.js
scripts/localdb.js
scripts/onlinedb.js
scripts/formEvents.js
http://code.jquery.com/jquery-1.7.2.min.js
NETWORK:
*
FALLBACK:
/ offline.html

当使用离线应用程序时,重要的是要知道:

  • 离线应用程序清单文件扩展名 .appcache 需映射到 text/cache-manifest Multipurpose Internet Mail Extension (MIME) 类型。在 Apache Tomcat 中,通过将 mime-mapping 条目添加到服务器的 web.xml 文件(不是 Web 应用程序的 web.xml 文件)来完成此操作。如果 MIME 类型是错误的,大多数浏览器以静默方式忽略离线应用程序清单。
  • 如果出现一个离线应用程序清单文件,则始终使用在本地缓存的资源(即使是在线状态)。
  • 本地资源只在离线应用程序清单文件发生更改时更新,通常通过更改清单文件中评论的一个修订号来更新。对 HTML 或 CSS 资源所做的更改直到应用程序清单文件发生更改时才反映在 Web 浏览器中。
  • 任何支持离线使用的页面必需具有以下内容:
    <html lang="en" manifest="app.appcache">

在线或离线

有了 JavaScript,您可以使用 navigator.onLine Boolean 来检测应用程序是在线状态还是离线状态。如果应用程序是在线状态,它将返回 True。

表单事件(在线/离线处理)

在 Contact Manager 中,无论是在线还是离线状态都使用相同的表单。使这个解决方案起作用的关键在于按钮和表单事件处理程序。查看 navigator.onLine 来确定需要调用(本地或在线)哪项操作。清单 9 显示了用于加载联系人数据的示例。

清单 9. 加载数据(在 HTML BODY 的 onLoad 事件中)
if (navigator.onLine) 
{	
	// selection list needs to be populated prior to synchronizing data
	// the list is updated from the online dictionary later
	populateOfflineStates(); 
	
	setStatusText("Synchronizing contact data with server...");
	synchronizeContacts();

	setStatusText("Loading dictionary data from server...");		
	populateOnlineStates();
	
	setStatusText("Loading contact data from server...");
	loadOnlineContacts();
}
else 
{
	alert('You are currently offline.');
	populateOfflineStates();
	setStatusText("Loading contact data from local storage...");
	loadOfflineContacts();
}

数据同步

当处于在线状态时,所有的 CRUD 操作都使用 servlet 来创建、修改和删除。本地缓存也在在线数据库发生更改时更新。

当处于离线状态时,所有的 CRUD 操作使用本地数据提供程序来更改。在重新连接服务器时:

  • 在服务器上持久保存本地创建的所有记录。
  • 在服务器上更新本地修改的所有记录。
  • 在服务器上删除本地删除的所有记录。

清单 10 显示了完整的同步化方法。在同步化期间,相同的在线函数用于创建、更新和删除操作。第一步是使用 jQuery $.each 函数迭代本地记录。

在本地更新或创建的记录均使用 isDirty 属性进行标记。如果 Save 操作其惟一的记录 ID 是负值(不是由 MySQL 数据库分配的),则将其确定为新的。在本地删除的记录使用 isDeleted 属性进行标记。

清单 10. 使离线更改与服务器同步
var recordsUpdated = 0;
var recordsCreated = 0;
var recordsDeleted = 0;

$.each(data, function(i,item){
	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);

最新的数据从使用 getcontacts 操作的数据库中取得,并将其显示。其他用户所做的任何更改将会被反映出来。随后在本地保存数据以确保其在离线状态时可用。


结束语

在本文中,示例显示了一个很好的在线和离线支持的模式。一致的用户体验维护的方式是:使用在线和离线模式的单一 HTML 页面并根据在线/离线状态以事件处理的形式调用相应的在线/离线数据提供程序。

数据同步算法提供了一个很好的基础;它使得记录的离线创建、删除和修改实现同步化。然而,它不是一种生产就绪代码。例如,它并不处理由另一个用户在本地和服务器上修改同一个记录发生的冲突。

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=845681
ArticleTitle=使用 HTML5 数据库和离线功能
publish-date=11302012