Привязка данных к элементам пользовательского интерфейса с помощью IndexedDB

Введение

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

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

  1. Нам нужно настроить и инициализировать экземпляр IndexedDB. По большей части это просто, но заставить его работать как в Chrome, так и в Firefox оказывается немного сложно.
  2. Нам нужно посмотреть, есть ли у нас какие-либо данные, и если нет, скачать их. Обычно это делается с помощью вызовов AJAX. Для нашей демонстрации мы создали простой служебный класс для быстрого создания поддельных данных. Приложению необходимо будет распознать момент создания этих данных и запретить пользователю использовать их до этого момента. Это разовая операция. В следующий раз, когда пользователь запустит приложение, ему не придется проходить этот процесс. Более продвинутая демонстрация будет обрабатывать операции синхронизации между клиентом и сервером, но эта демонстрация больше ориентирована на аспекты пользовательского интерфейса.
  3. Когда приложение будет готово, мы сможем использовать элемент управления автозаполнением пользовательского интерфейса jQuery для синхронизации с IndexedDB. Хотя элемент управления автозаполнением позволяет создавать базовые списки и массивы данных, он имеет API, позволяющий использовать любой источник данных. Мы продемонстрируем, как мы можем использовать это для подключения к нашим данным IndexedDB.

Начиная

У нас есть несколько частей в этой демонстрации, поэтому для начала давайте посмотрим на HTML-часть.

<form>
  <p>
    <label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
    </p>
</form>

<div id="displayEmployee"></div>

Не так уж и много, правда? Есть три основных аспекта этого пользовательского интерфейса, которые нас волнуют. Во-первых, это поле «имя», которое будет использоваться для автозаполнения. Он загружается отключено и будет включен позже через JavaScript. Диапазон рядом с ним используется во время первоначального заполнения для предоставления обновлений пользователю. Наконец, элемент div с идентификатором displayEmployee будет использоваться при выборе сотрудника из автозаполнения.

Теперь давайте посмотрим на JavaScript. Здесь нужно многое переварить, поэтому мы будем действовать шаг за шагом. Полный код будет доступен в конце, чтобы вы могли увидеть его целиком.

Во-первых, есть некоторые проблемы с префиксами, о которых нам следует беспокоиться в браузерах, поддерживающих IndexedDB. Вот некоторый код из документации Mozilla, измененный для предоставления простых псевдонимов для основных компонентов IndexedDB, необходимых нашему приложению.

window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

Далее, несколько глобальных переменных, которые мы будем использовать в демонстрации:

var db;
var template;

Теперь мы начнем с блока готовности документа jQuery:

$(document).ready(function() {
  console.log("Startup...");
  ...
});

В нашей демонстрации используется Handlebars.js для отображения сведений о сотруднике. Это не будет использоваться позже, но мы можем скомпилировать наш шаблон сейчас и убрать его с дороги. У нас есть блок сценария, настроенный как тип, распознаваемый Handlebars. Это не так уж и необычно, но упрощает отображение динамического HTML.

<h2>, </h2>
Department: <br/>
Email: <a href='mailto:'></a>

Затем это компилируется обратно в наш JavaScript следующим образом:

//Create our template
var source = $("#employeeTemplate").html();
template = Handlebars.compile(source);

Теперь давайте начнем работать с нашей IndexedDB. Сначала – открываем его.

var openRequest = indexedDB.open("employees", 1);

Открытие соединения с IndexedDB дает нам доступ к чтению и записи данных, но прежде чем мы это сделаем, мы должны убедиться, что у нас есть объектное хранилище. ObjectStore похож на таблицу базы данных. Одна IndexedDB может иметь множество хранилищ объектов, каждое из которых содержит коллекцию связанных объектов. Наша демо-версия проста и требует только одного хранилища объектов, которое мы называем «сотрудник». Когда indexedDB открывается в первый раз или когда вы меняете версию в коде, запускается событие onupgradeneeded. Мы можем использовать это для настройки нашего ObjectStore.

// Handle setup.
openRequest.onupgradeneeded = function(e) {

  console.log("running onupgradeneeded");
  var thisDb = e.target.result;

  // Create Employee
  if(!thisDb.objectStoreNames.contains("employee")) {
    console.log("I need to make the employee objectstore");
    var objectStore = thisDb.createObjectStore("employee", {keyPath: "id", autoIncrement: true});
    objectStore.createIndex("searchkey", "searchkey", {unique: false});
  }

};

openRequest.onsuccess = function(e) {
  db = e.target.result;

  db.onerror = function(e) {
    alert("Sorry, an unforseen error was thrown.");
    console.log("***ERROR***");
    console.dir(e.target);
  };

  handleSeed();
};

В блоке обработчика событий onupgradeneeded мы проверяем objectStoreNames, массив хранилищ объектов, чтобы увидеть, содержит ли он сотрудника. Если нет, мы просто заставим это сделать. Вызов createIndex важен. Мы должны указать IndexedDB, какие методы, помимо ключей, мы будем использовать для получения данных. Мы будем использовать один под названием searchkey. Это объясняется немного.

Событие onungradeneeded запустится автоматически при первом запуске сценария. После его выполнения или пропуска при последующих запусках запускается обработчик onsuccess . У нас есть простой (и уродливый) обработчик ошибок, а затем мы вызываем handleSeed .

Итак, прежде чем мы продолжим, давайте быстро рассмотрим, что здесь происходит. Открываем базу данных. Мы проверяем, существует ли наше хранилище объектов. Если этого не происходит, мы создаем его. Наконец, мы вызываем функцию с именем handleSeed. Теперь давайте обратим внимание на часть заполнения данных в нашей демонстрации.

Дайте мне немного данных!

Как упоминалось во введении к этой статье, эта демонстрация воссоздает приложение в стиле интрасети, которому необходимо хранить копии всех известных сотрудников. Обычно это предполагает создание серверного API, который мог бы возвращать количество сотрудников и предоставлять нам возможность получать пакеты записей. Вы можете представить себе простой сервис, который поддерживает подсчет стартов и возвращает 100 человек одновременно. Это может работать асинхронно в фоновом режиме, пока пользователь занят другими делами.

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

function handleSeed() {
  // This is how we handle the initial data seed. Normally this would be via AJAX.

  db.transaction(["employee"], "readonly").objectStore("employee").count().onsuccess = function(e) {
    var count = e.target.result;
    if (count == 0) {
      console.log("Need to generate fake data - stand by please...");
      $("#status").text("Please stand by, loading in our initial data.");
      var done = 0;
      var employees = db.transaction(["employee"], "readwrite").objectStore("employee");
      // Generate 1k people
      for (var i = 0; i < 1000; i++) {
         var person = generateFakePerson();
         // Modify our data to add a searchable field
         person.searchkey = person.lastname.toLowerCase();
         resp = employees.add(person);
         resp.onsuccess = function(e) {
           done++;
           if (done == 1000) {
             $("#name").removeAttr("disabled");
             $("#status").text("");
             setupAutoComplete();
           } else if (done % 100 == 0) {
             $("#status").text("Approximately "+Math.floor(done/10) +"% done.");
           }
         }
      }
    } else {
      $("#name").removeAttr("disabled");
      setupAutoComplete();
    }
  };
}

Самая первая строка немного сложна, поскольку у нас есть несколько операций, связанных друг с другом, поэтому давайте разберем ее:

db.transaction(["employee"], "readonly");

Это создает новую транзакцию только для чтения. Все операции с данными в IndexedDB требуют какой-либо транзакции.

objectStore("employee");

Получите хранилище объектов сотрудников.

count()

Запустите API подсчета, который, как вы можете догадаться, выполняет подсчет.

onsuccess = function(e) {

И когда все будет готово, выполните этот обратный вызов. Внутри обратного вызова мы можем получить значение результата, которое представляет собой количество объектов. Если счетчик равен нулю, мы начинаем процесс заполнения.

Мы используем этот раздел статуса, упомянутый ранее, чтобы дать пользователю сообщение о том, что мы собираемся начать получать данные. Из-за асинхронной природы IndexedDB мы создали простую переменную Done, которая будет отслеживать добавления. Мы повторяем цикл и вставляем фальшивых людей. Исходный код этой функции доступен для скачивания, но она возвращает объект, который выглядит следующим образом:

{
  firstname: "Random Name",
  lastname: "Some Random Last Name",
  department: "One of 8 random departments",
  email: "first letter of firstname+lastname@fakecorp.com"
}

Само по себе этого достаточно, чтобы определить человека. Но у нас есть особые требования для поиска наших данных. IndexedDB не предоставляет возможности поиска элементов без учета регистра. Поэтому мы копируем поле «Фамилия» в новое свойство searchkey. Если вы помните, мы говорили, что этот ключ следует создать в качестве индекса для наших данных.

// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();

Поскольку это модификация, специфичная для клиента, она выполняется здесь, а не на внутреннем сервере (или, в нашем случае, на воображаемом внутреннем сервере).

Чтобы обеспечить производительность добавления базы данных, вам следует повторно использовать транзакцию для всех пакетных операций записи. Если вы создаете новую транзакцию для каждой записи, браузер может вызывать запись на диск для каждой транзакции, и это приведет к ужасной производительности при добавлении большого количества элементов (подумайте: «1 минута для записи 1000 объектов» — ужасно).

Как только начальное число завершено, запускается следующая часть нашего приложения — setupAutoComplete.

Создание автозаполнения

Теперь самое интересное — подключение к плагину автозаполнения jQuery UI. Как и в большинстве случаев пользовательского интерфейса jQuery, мы начинаем с базового элемента HTML и расширяем его, вызывая для него метод конструктора. Мы абстрагировали весь процесс в функцию под названием setupAutoComplete. Давайте теперь посмотрим на этот код.

function setupAutoComplete() {

  //Create the autocomplete
  $("#name").autocomplete({
    source: function(request, response) {

      console.log("Going to look for "+request.term);

      $("#displayEmployee").hide();

      var transaction = db.transaction(["employee"], "readonly");
      var result = [];

      transaction.oncomplete = function(event) {
        response(result);
      };

      // TODO: Handle the error and return to it jQuery UI
      var objectStore = transaction.objectStore("employee");

      // Credit: http://stackoverflow.com/a/8961462/52160
      var range = IDBKeyRange.bound(request.term.toLowerCase(), request.term.toLowerCase() + "z");
      var index = objectStore.index("searchkey");

      index.openCursor(range).onsuccess = function(event) {
        var cursor = event.target.result;
        if(cursor) {
          result.push({
            value: cursor.value.lastname + ", " + cursor.value.firstname,
            person: cursor.value
          });
          cursor.continue();
        }
      };
    },
    minLength: 2,
    select: function(event, ui) {
      $("#displayEmployee").show().html(template(ui.item.person));
    }
  });

}

Самая сложная часть этого кода — создание исходного свойства. Элемент управления автозаполнением пользовательского интерфейса jQuery позволяет вам определить свойство источника, которое можно настроить в соответствии с любыми возможными потребностями — даже с нашими данными IndexedDB. API предоставляет вам запрос (в основном то, что было введено в поле формы) и обратный вызов ответа. Вы несете ответственность за отправку массива результатов обратно в этот обратный вызов.

Первое, что мы делаем, это скрываем элемент управления displayEmployee. Используется для отображения отдельного сотрудника и, если он уже загружен, для его очистки. Теперь мы можем начать поиск.

Мы начинаем с создания транзакции, доступной только для чтения, массива с именем result и обработчика завершения, который просто передает результат элементу управления автозаполнением.

Чтобы найти элементы, соответствующие нашим входным данным, давайте воспользуемся советом пользователя StackOverflow Фонг-Ван Чау: мы используем диапазон индексов, основанный на входных данных, в качестве нижней границы, а входные данные плюс букву z — в качестве верхней границы диапазона. . Также обратите внимание, что мы записываем термин строчными буквами, чтобы он соответствовал введенным нами строчным буквам.

После этого мы можем открыть курсор (думайте об этом как о выполнении запроса к базе данных) и перебирать результаты. Элемент управления автозаполнением пользовательского интерфейса jQuery позволяет вам возвращать любой тип данных, но требует как минимум ключа значения. Мы устанавливаем значение красиво отформатированной версии имени. Мы также возвращаем человека целиком. Вы поймете почему через секунду. Во-первых, вот снимок экрана автозаполнения в действии. Мы используем тему Vader для пользовательского интерфейса jQuery.

Само по себе этого достаточно, чтобы вернуть результаты наших совпадений IndexedDB в автозаполнение. Но мы также хотим поддержать показ подробного просмотра матча, когда он выбран. Мы указали обработчик выбора при создании автозаполнения, которое использует ранее использованный шаблон Handlebars.