Введение
IndexedDB – это мощный инструмент для хранения данных на стороне клиента. Если вы ещё не знакомы с ними, рекомендуем изучить руководства MDN по этой теме. Для понимания этой статьи необходимы базовые знания об API и функциях. Даже если вы раньше не сталкивались с IndexedDB, демонстрация в этой статье даст вам представление о том, что можно сделать с помощью этой технологии.
Наша демонстрация – это простая проверка концепции интранет-приложения для компании. Приложение позволит сотрудникам искать других сотрудников. Чтобы обеспечить более быструю и оперативную работу, база данных сотрудников копируется на компьютер клиента и сохраняется с помощью IndexedDB. В демоверсии показан только поиск с автозаполнением и отображение одной записи о сотруднике, но, когда эти данные доступны на клиенте, мы можем использовать их и другими способами. Вот основные требования к нашему приложению.
- Нам нужно настроить и инициализировать экземпляр IndexedDB. В большинстве случаев это несложно, но заставить его работать и в Chrome, и в Firefox может быть непросто.
- Нам нужно проверить, есть ли у нас данные, и если нет, скачать их. Обычно это делается с помощью вызовов AJAX. Для демонстрации мы создали простой класс утилит, чтобы быстро генерировать фиктивные данные. Приложение должно распознавать, когда создаются эти данные, и не позволять пользователю использовать их до этого момента. Это однократная операция. При следующем запуске приложения пользователю не придется проходить эту процедуру. В более продвинутой демоверсии будут обрабатываться операции синхронизации между клиентом и сервером, но эта демоверсия больше ориентирована на аспекты пользовательского интерфейса.
- Когда приложение будет готово, мы сможем использовать элемент управления Autocomplete из jQuery UI для синхронизации с 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>
Не так уж и много, правда? В этом интерфейсе есть три основных аспекта, которые нас интересуют. Первое – это поле name, которое будет использоваться для автозаполнения. Он загружается отключенным и будет включен позже с помощью JavaScript. Элемент span рядом с ним используется во время начального заполнения, чтобы предоставлять пользователю обновления. Наконец, 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:
$(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. objectStore – это аналог таблицы базы данных. В одной базе данных IndexedDB может быть несколько хранилищ objectStore, каждое из которых содержит коллекцию связанных объектов. В нашем примере используется только одно хранилище объектов, которое мы назвали employee. При первом открытии 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, массив хранилищ объектов, чтобы узнать, содержит ли он employee. Если нет, мы просто заставим его сделать это. Вызов 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");
Получите хранилище объектов employee.
count()
Запустите API count, который, как вы можете догадаться, выполняет подсчет.
onsuccess = function(e) {
и по завершении выполнить этот обратный вызов. Внутри обратного вызова мы можем получить значение результата, которое является количеством объектов. Если количество равно нулю, мы начинаем процесс заполнения.
Мы используем упомянутый ранее div статуса, чтобы сообщить пользователю, что мы собираемся начать получать данные. Из-за асинхронной природы 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 нет возможности искать элементы без учета регистра. Поэтому мы копируем поле lastname в новое свойство searchkey. Если вы помните, это ключ, который, по нашему мнению, должен быть создан как индекс для наших данных.
// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();
Поскольку это изменение относится к клиенту, оно выполняется здесь, а не на внутреннем сервере (или, в нашем случае, на воображаемом внутреннем сервере).
Чтобы эффективно добавлять данные в базу, используйте одну транзакцию для всех пакетных записей. Если вы создаете новую транзакцию для каждой записи, браузер может вызывать запись на диск для каждой транзакции, и это приведет к ужасной производительности при добавлении большого количества элементов (например, "1 минута для записи 1000 объектов").
После того как начальные данные будут загружены, запустится следующая часть нашего приложения – setupAutoComplete.
Создание автозаполнения
Теперь самое интересное – подключение плагина Autocomplete из jQuery UI. Как и в большинстве случаев с jQuery UI, мы начинаем с базового элемента 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));
}
});
}
Самая сложная часть этого кода – создание исходного ресурса. Элемент управления Autocomplete в jQuery UI позволяет определить свойство источника, которое можно настроить в соответствии с любыми возможными потребностями, даже с нашими данными IndexedDB. API предоставляет вам запрос (то, что было введено в поле формы) и обратный вызов ответа. Вы должны отправить массив результатов обратно в этот обратный вызов.
Сначала мы скрываем div displayEmployee. Он используется для отображения отдельного сотрудника и, если он был загружен ранее, для его очистки. Теперь можно начинать поиск.
Сначала мы создаем транзакцию только для чтения, массив result и обработчик oncomplete, который просто передает результат элементу управления автозаполнением.
Чтобы найти элементы, соответствующие введенным данным, воспользуемся советом пользователя StackOverflow Фонг-Ван Чау: в качестве нижней границы диапазона индекса используем введенные данные, а в качестве верхней – введенные данные плюс букву z. Обратите внимание, что мы используем строчные буквы, чтобы соответствовать введенным данным.
После этого мы можем открыть курсор (это похоже на выполнение запроса к базе данных) и перебрать результаты. Элемент управления автозаполнением jQuery UI позволяет возвращать любые типы данных, но требует как минимум ключ значения. Мы устанавливаем значение в красиво отформатированную версию имени. Мы также возвращаем весь объект Person. Сейчас вы поймете, почему. Ниже показан скриншот с примером работы автозаполнения. Мы используем тему Vader для jQuery UI.
Этого достаточно, чтобы вернуть результаты совпадений IndexedDB в автозаполнение. Но мы также хотим поддерживать отображение подробного представления совпадения, когда оно выбрано. При создании автозаполнения мы указали обработчик выбора, который использует шаблон Handlebars, созданный ранее.