Использование возможностей HTML5 для сохранения данных и оффлайновой работы: Часть 2. Использование API-интерфейса IndexedDB в HTML5

Технология HTML5 получит статус официального стандарта организации World Wide Web Consortium (W3C) не ранее 2014 года, однако поставщики веб-браузеров уже добавляют и продвигают HTML5-функции. Две такие функции — поддержка оффлайновых приложений и локальное персистентное хранение данных — позволяют предоставить пользователю одинаково богатые возможности в онлайновом и в оффлайновом режимах работы, что ранее было доступно только в закрытых инфраструктурах для разработки приложений для ПК. Опираясь на базовый материал, который был изложен в первой части этой серии, данная статья объясняет, как применить API-интерфейс IndexedDB (Indexed Database) для построения оффлайнового приложения, данные которого хранятся на локальной системе.

Брайан Дж. Стюарт, главный консультант, Aqua Data Technologies, Inc.

Фото Брайана СтюартаБрайан Стюарт (Brian J. Stewart) работает главным консультантом в компании Aqua Data Technologies, которую он основал и которая занимается управлением информацией, XML-технологиями и корпоративными клиент/серверными и Web-системами. Он проектирует и разрабатывает корпоративные решения на базе платформ J2EE и .NET.



20.05.2013

В первой статье серии "Использование базы данных HTML5 и оффлайновых возможностей HTML5" были представлены доступные в спецификации HTML5 опции для поддержки оффлайновых приложений и локального сохранения данных, при этом основное внимание было уделено технологии localStorage. В этой статье описывается мощная технология сохранения данных IndexedDB (Indexed Database), являющаяся частью стандарта HTML5, и рассматривается интеграция провайдера данных IndexedDB с приложением Contact Manager, которое было создано в первой статье.

Учебное приложение Contact Manager (для имен, адресов и номеров телефонов) имеет онлайновый и оффлайновый режимы работы. При нахождении приложения в оффлайновом режиме данные находятся в локальном персистентном хранилище. После переключения в онлайновый режим простая функция синхронизации данных синхронизирует локальные изменения данных с сервером. Описываемое приложение поддерживает четыре базовые функции персистентного хранения — создание/чтение/обновление/удаление (create/read/update/delete, CRUD) — и в онлайновом, и в оффлайновом режимах.

Архитектурный план приложения

Интерфейс сервера

Интерфейс сервера состоит из двух сервлетов: ContactServlet и DictionaryServlet. Таблица с описанием этих интерфейсов приведена в первой статье. Реализация этих сервлетов, а также соответствующих бизнес-сервисов и провайдеров данных выходит за рамки этой статьи.

Сначала обратимся к рисунку 1, на котором показана архитектурная схема приложения Contact Manager. Архитектура сервера состоит из двух сервлетов, которые соответствуют бизнес-сервисам и провайдерам данных. Пользовательский интерфейс состоит из одного HTML-файла, четырех JavaScript-модулей и внешней ссылки на новейшую версию библиотеки jQuery.

В этой статье мы заменим провайдера оффлайновой базы данных провайдером на основе API-интерфейса IndexedDB, а именно заменим JavaScript-модуль localdb.js.

Рисунок 1. Архитектурный план приложения Contact Manager
Image showing the application architecture, with boxes showing the servlets under client architecture and server architecture

Модель данных

Модель данных состоит из двух объектов данных: contact и state (см. рис. 2). Таблица contact содержит фактические контактные данные; таблица state содержит значения словаря для списка выбора состояния.

Рисунок 2. Модель данных приложения Contact Manager
Image showing the data model

API-интерфейс IndexedDB

Спецификация HTML5 включает несколько технологий персистентного хранения. Технология IndexedDB является предпочтительной базой данных для HTML5-браузеров; она заменяет выходящую из употребления технологию WebSQL.

Поддержка веб-браузерами технологии IndexedDB и реализации API-интерфейса IndexedDB в веб-браузерах не всегда единообразна. На момент написания этой статьи технология IndexedDB поддерживалась браузерами Google Chrome 11+, Mozilla Firefox 4+ и Windows® Internet Explorer® 10.

Веб-сайт может содержать одну или несколько баз данных, каждая из которых идентифицируется своим уникальным именем. Каждая база данных может содержать одно или несколько хранилищ объектов. Хранилище объектов подобно таблице в реляционной базе данных в том смысле, что оно представляет собой набор записей, который идентифицируется по своему уникальному имени. Однако хранилище объектов отличается от таблицы реляционной базы данных в таких аспектах, как хранение данных, доступ к данным и запросы к данным. Данные в хранилище объектов хранятся по принципу ключ-значение. Ключ должен быть уникальным в пределах хранилища объектов; он может быть задан целенаправленно или создан генератором ключей.

Спецификация API-интерфейса IndexedDB предусматривает поддержку таких распространенных конструкций для баз данных, как транзакции, индексация, запросы данных и указатели. Эта спецификация включает как синхронный, так и асинхронный API-интерфейсы. Синхронный API предназначен для использования внутри компонентов типа Web Worker. Следует, однако, отметить, что не все веб-браузеры поддерживают технологию Web Worker и синхронный API-интерфейс IndexedDB. Асинхронный API-интерфейс использует запросы и обратные вызовы. Все операции с базами данных, такие как открытие базы данных, извлечение данных, запрашивание данных и удаление данных, используют вызовы API-интерфейса. Каждый запрос сопровождается обратным вызовом onsuccess или onerror в случае успешного или неудачного выполнения операции соответственно. Обратные вызовы предоставляют параметр "результат события" для возвращенных данных.

Учебное приложение Contact Manager состоит из одной базы данных с двумя хранилищами объектов. Первое хранилище объектов является хранилищем объектов "контакты" (contacts) и содержит записи с фактическими контактными сведениями. Второе хранилище объектов является хранилищем объектов "состояния" (states) и содержит значения для списка выбора состояний. В этой статье демонстрируется использование асинхронного API-интерфейса для доступа к данным.


Подключение к базе данных и отключение от базы данных

Поскольку в каждом веб-браузере реализация технологии IndexedDB имеет свои особенности, наилучший подход состоит в том, чтобы создать глобальную переменную (localDatabase) и инициализировать ее в соответствии с особенностями реализации в разных веб-браузерах. Эта глобальная переменная предоставляет ссылку на API-интерфейс IndexedDB.

Следующий шаг состоит в открытии базы данных с помощью метода 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;

...
};

Для отключения от базы данных достаточно вызвать метод close объекта базы данных IndexedDB.


Создание хранилищ объектов

Следующий шаг состоит в создании хранилищ объектов в рамках базы данных. Создать хранилище объектов можно в любой момент времени, однако после создания его можно будет изменить только в рамках обратного вызова 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 идентифицирует поле ключа для хранилища объектов.


Создание словаря для списков выбора

Создание и изменение записей в хранилище объектов осуществляется с помощью транзакций. API-интерфейс IndexedDB предоставляет для транзакций три режима.

  • 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);
}

Теперь пора переходить к добавлению и обновлению записей контактов в оффлайновом режиме.


Добавление и обновление контакта

В этой статье используется такой же подход к созданию и обновлению записей, как и в первой статье (когда мы сохраняли оффлайновые данные в хранилище 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 (подобный метод описывался в первой статье этой серии). Флаг 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-операции используют сервлеты, а база данных на сервере обновляется немедленно. Локальная база данных (IndexedDB) также обновляется согласно изменениям онлайновой базы данных, что гарантирует постоянную доступность свежих данных как онлайновом, так и в оффлайновом режимах.

При нахождении в оффлайновом режиме все CRUD-операции обновляют данные в базе данных IndexedDB. После восстановления соединения с сервером выполняются следующие действия.

  • Все записи, которые были созданы в локальной базе данных, сохраняются на сервере.
  • Все записи, которые были изменены в локальной базе данных, обновляются на сервере.
  • Все записи, которые были удалены из локальной базы данных, удаляются с сервера.

В листинге 7 приведен полный текст метода синхронизации. Для операций создания, обновления и удаления используются те же онлайновые функции, которые были описаны в первой статье.

Первый шаг состоит в выполнении итеративного прохождения по записям в хранилище объектов контактов с помощью указателя и в построении массива, содержащего все записи, которые должны быть отправлены на сервер. У записей, которые были обновлены или созданы локально, свойство isDirty имеет значение true. Операция Save (сохранить) идентифицируется как новая, если ее уникальный идентификатор записи имеет отрицательное значение (т.е. база данных 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);

Заключение

Данная статья опирается на базовый материал, который был изложен в первой части данной серии статей. В ней используется один и тот же шаблон как для онлайновой, так и для оффлайновой поддержки, а также для предоставления пользователю единообразных возможностей. В статье представлен API-интерфейс IndexedDB, с помощью которого осуществляется синхронизация результатов оффлайнового создания, удаления и изменения записей. Используемый в статье программный код не имеет мощных механизмов для обработки ошибок и разрешения конфликтов (когда одна и та же запись изменена и вами на локальной системе, и другим пользователем на сервере), поэтому он не готов к использованию в производственной среде. Тем не менее он является хорошим фундаментом для будущей работы.

Ресурсы

  • Оригинал статьи: Using HTML5 database and offline capabilities, Part 2: Leveraging the IndexedDB API in HTML5.
  • Основы HTML5: Часть 1. Приступая к работе, developerWorks, май 2011 г. Первая статья серии из четырех частей, посвященной рассмотрению реализованных в HTML5 изменений.
  • HTML5, CSS3, and related technologies(HTML5, CSS3 и связанные технологии), developerWorks, апрель 2011 г. Познакомьтесь со спецификацией HTML5 и с ее влиянием на технологию CSS3.
  • Основы HTML5. Узнайте больше о технологии HTML5 с помощью этой учебной программы.
  • Технология Web Workers. Этот API-интерфейс позволяет разработчику создавать фоновые компоненты для исполнения скриптов параллельно с основной страницей.
  • Технология Web Storage предоставляет API-интерфейс для персистентных хранилищ данных типа key-value (ключ –значение) в веб-клиентах.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура
ArticleID=930451
ArticleTitle=Использование возможностей HTML5 для сохранения данных и оффлайновой работы: Часть 2. Использование API-интерфейса IndexedDB в HTML5
publish-date=05202013