使用 HTML5 数据库和离线功能,第 2 部分: 在 HTML5 中利用 IndexedDB API

HTML5 在 2014 年之前不会成为官方的 World Wide Web Consortium (W3C) 标准,但 Web 浏览器厂商都已经在添加和使用 HTML5 相关特性。其中两个特性(离线应用支持和本地持久存储)使得在线和离线状态下都能提供同样丰富的用户体验,以前仅在桌面应用程序开发框架中提供这些体验。本文建立在 第 1 部分 的基础之上,介绍了如何利用 Indexed Database (IndexedDB) API 来构建离线应用程序与本地持久数据。

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

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



2013 年 1 月 07 日

"使用 HTML5 数据库和离线功能,第 1 部分" 介绍了 HTML5 规范中提供的离线应用程序和本地数据持久性,重点介绍了本地存储。本期文章将介绍 Indexed Database (IndexedDB),这是一项强大的数据持久技术,是 HTML5 标准的一部分,本文还将讨论如何集成 IndexedDB 的数据提供者与在第一篇文章中创建的 Contact Manager 应用程序。

示例 Contact Manager 应用程序(用于姓名、地址和电话号码)具有在线和离线模式。当处于离线状态时,数据驻留在本地持久存储中。当切换到在线模式时,一个简单的数据同步函数会将本地数据同步到服务器。无论处于在线还是离线模式,该应用程序均支持四个基本持久存储函数(创建、读取、更新和删除 [CRUD])。

应用程序架构概述

服务器接口

服务器接口包含两个 servlet:ContactServletDictionaryServlet。您可以在 第 1 部分 找到显示接口摘要信息的一个表格。这些 servlet 及相应业务服务和数据提供者的实现并非本系列文章的重点。

回顾一下已经介绍的内容,先来看看图 1,它显示了 Contact Manager 架构。服务器架构由业务服务和数据提供者的两个 Servlet 组成。UI 包含单个 HTML 文件和四个 JavaScript 模块,还有对最新版本 jQuery 库的外部引用。

在本文中,用一个基于 IndexedDB API 的数据库提供者来替换离线数据库提供者。具体而言,替换了 localdb.js 的 JavaScript 模块。

图 1. Contact Manager 应用程序架构
本图显示了应用程序架构,其中的框框显示了客户端架构和服务器架构下的 servlet

数据模型概述

数据模型包括两个数据实体:contact 和 state(参见图 2)。contact 表包含实际的联系人数据。state 表包含状态选择列表的字典值。

图 2. Contact Manager 应用程序数据模型
本图显示了数据模型

IndexedDB API

HTML5 规范包含几种持久存储技术。IndexedDB 是首选的 HTML5 浏览器的数据库,它取代了 WebSQL 数据库。

Web 浏览器支持 IndexedDB API 实现,在 Web 浏览器中并不是一致的。在撰写本文时,Google Chrome 11+、Mozilla Firefox 4+ 和 Windows® Internet Explorer® 10 均支持 IndexedDB。

一个网站可以包含一个或多个数据库,通过惟一的名称来标识它们。每个数据库可以包含一个或多个对象存储。对象存储与关系数据库中的表类似,表通过惟一的名称标识,是记录的集合。然而,对象存储处理数据存储、访问和查询的方式与关系数据库中的表不同。对象存储内的数据在存储时包含一个键和一个值,其中键必须是惟一的,且可由键生成器指定或生成。

IndexedDB API 规范包括常见的数据库构造(如事务、索引、查询数据和游标)的支持。该规范包括了同步和异步 API。同步 API 的目的是要在 Web Workers 内使用。然而,并不是所有 Web 浏览器都支持 Web Workers 和 IndexedDB 同步 API。异步 API 使用了请求和回调。所有的数据库操作(比如打开数据库、检索数据、查询数据和删除数据)都会请求 API 调用。每个请求都有一个 onsuccessonerror 回调,当操作成功或不成功时,会分别调用它们。回调为返回的数据提供一个事件结果参数。

示例 Contact Manager 应用程序包含两个对象存储的一个数据库。第一个存储是 contacts 数据存储,它包含实际的联系人记录。第二个对象存储是 states 对象存储,它包含状态选择列表的值。本文演示了如何使用异步 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 :提供对象存储的只读访问。
  • readwrite :提供对象存储的读写访问。
  • versionchange :除了对象存储的读写访问之外,还提供了创建和删除对象的存储能力。

使用以下语法创建事务:

var transaction = db.transaction("states", "readwrite");

在创建一个事务后,下一步是要获得对象存储的引用。transaction 对象包含 objectstores 属性,它提供与数据库关联的对象存储的访问。为了获得 states 对象存储的引用,请使用下面的语句:

var store = transaction.objectStore("states");

当您连接到服务器时,您希望用最新的服务器数据替换所有本地 states 数据。为了完成这一工作,在使用来自服务器的最新值填充它之前,您必须清空 states 对象存储。可以在对象存储上使用 clear 方法来实现此操作。

如果成功清空了对象存储,则需要调用 onsuccess 回调。在此回调中,现在可以遍历本地 states 对象存储,并添加每个值。您要添加到对象存储的数据包含在 stateArray 字符串数组中。使用 jQuery $.each 方法来遍历数组。对于 states 数组中的每一个值,可以调用 put 方法,将记录添加到 states 对象存储。

清单 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 方法将记录保存在对象存储中(类似于填充 states 对象存储的方式)。清单 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);
}

查询联系人

要从 contacts 对象存储检索联系人记录,可以使用一个 IndexedDB 游标。与关系数据库中的数据库游标相似,IndexedDB 游标提供l了一种遍历对象存储的记录方法。在遍历记录时,需要建立一个包含联系人记录的数组。isDeleted 标志设置为 true 的所有记录都会被忽略。清单 6 显示了如何使用 contacts 对象存储中包含的数据构建一个联系人数组。

清单 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 部分 中描述的在线函数也可用于创建、更新和删除操作。

第一步是使用游标遍历 contacts 对象存储中的记录,并构建一个数组,其中包含需要发布到服务器的所有记录。本地更新或创建的记录的 isDirty 属性被设置为 true。如果其惟一的记录 ID 为负(即没有通过 MySQL 数据库分配),则将保存操作标识为新操作。使用 isDeleted 属性标记在本地被删除的记录。

当您的数组包含所有需要发布到服务器的记录时,可以使用 jQuery $.each 方法来遍历数组,并将每个变更发布到服务器。

最后,在完成数据同步方法时,会使用服务器中的最新数据来刷新本地 contacts 对象存储。这包括您(和其他用户)在离线状态时进行的所有更改:

清单 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,介绍了IndexedDB API,数据同步算法处理同步离线创建、删除和修改记录。由于代码没有经过可靠的错误处理和消除冲突(在本地和服务器上,有其他用户修改了相同的记录),不能用于开发生产。不过,这为今后的工作提供了良好的基础。

参考资料

学习

获得产品和技术

讨论

  • The developerWorks 社区:探索由开发人员推动的博客、论坛、讨论组和维基,并与其他 developerWorks 用户进行交流。

条评论

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=854172
ArticleTitle=使用 HTML5 数据库和离线功能,第 2 部分: 在 HTML5 中利用 IndexedDB API
publish-date=01072013