Wprowadzenie
IndexedDB to skuteczny sposób na przechowywanie danych po stronie klienta. Jeśli jeszcze tego nie zrobisz, zachęcam do przeczytania przydatnych samouczków MDN na ten temat. Z tego artykułu dowiesz się, jak korzystać z interfejsów API i ich funkcji. Nawet jeśli nie znasz jeszcze IndexedDB, demo w tym artykule powinno Ci pomóc w zorientowaniu się, jak można z niego korzystać.
Nasz pokaz to prosty model koncepcyjny aplikacji intranetowej dla firmy. Aplikacja pozwoli pracownikom na wyszukiwanie innych pracowników. Aby zapewnić szybsze i wygodniejsze działanie, baza danych o pracownikach jest kopiowana na komputer klienta i przechowywana w usłudze IndexedDB. W tym filmie prezentujemy wyszukiwanie z automatycznym uzupełnianiem i wyświetlanie pojedynczego rekordu pracownika, ale warto pamiętać, że gdy te dane są dostępne na kliencie, możemy ich używać na wiele innych sposobów. Oto podstawowe informacje o tym, co powinna robić nasza aplikacja.
- Musimy skonfigurować i zainicjować instancję IndexedDB. W większości przypadków jest to proste, ale sprawienie, aby działało to w Chrome i Firefoksie, jest nieco trudne.
- Musimy sprawdzić, czy mamy jakieś dane, a jeśli nie, pobrać je. Obecnie zwykle odbywa się to za pomocą wywołań AJAX. Na potrzeby tego demonstracyjnego projektu utworzyliśmy prostą klasę pomocniczą, która pozwala szybko generować fałszywe dane. Aplikacja musi rozpoznać, kiedy tworzy te dane, i uniemożliwić użytkownikowi ich użycie do tego czasu. Jest to operacja jednorazowa. Przy następnym uruchomieniu aplikacji użytkownik nie będzie musiał przechodzić przez ten proces. Bardziej zaawansowane demo obejmowałoby operacje synchronizacji między klientem a serwerem, ale to demo skupia się bardziej na aspektach interfejsu użytkownika.
- Gdy aplikacja będzie gotowa, możemy użyć elementu sterującego Autocomplete z biblioteki jQuery UI, aby zsynchronizować ją z IndexedDB. Polecenie Autocomplete umożliwia tworzenie podstawowych list i tablic danych, ale ma też interfejs API, który umożliwia korzystanie z dowolnego źródła danych. Pokażę, jak za jego pomocą połączyć się z danymi IndexedDB.
Pierwsze kroki
Mamy kilka części tego demonstracyjnego filmu, więc na początek przyjrzyjmy się części HTML.
<form>
<p>
<label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
</p>
</form>
<div id="displayEmployee"></div>
Niewiele, prawda? W tym interfejsie są 3 główne aspekty, na których nam zależy. Najpierw jest pole „name” (nazwa), które będzie używane do autouzupełniania. Jest ona wczytana w wyłączonym stanie i zostanie włączona później za pomocą JavaScriptu. Element obok niego jest używany podczas początkowego zasiewu, aby dostarczać użytkownikowi aktualizacje. Na koniec div z identyfikatorem displayEmployee będzie używany, gdy wybierzesz pracownika z autouzupełniania.
Teraz przyjrzyjmy się kodom JavaScript. Mamy tu dużo informacji, więc omówimy je krok po kroku. Na końcu będzie dostępny pełny kod, który możesz wyświetlić w całości.
Po pierwsze, w przypadku przeglądarek obsługujących IndexedDB musimy się martwić o niektóre problemy z prefiksami. Oto kod z dokumentacji Mozilli zmodyfikowany w celu zapewnienia prostych aliasów podstawowych komponentów IndexedDB, których potrzebuje nasza aplikacja.
window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
Oto kilka zmiennych globalnych, których użyjemy w trakcie demonstracji:
var db;
var template;
Zacznijmy od bloku jQuery document ready:
$(document).ready(function() {
console.log("Startup...");
...
});
Nasz pokaz demonstracyjny korzysta z biblioteki Handlebars.js do wyświetlania informacji o pracowniku. Nie jest on używany dopóki nie skompilujemy szablonu, ale możemy go skompilować już teraz. Mamy blok skryptu skonfigurowany jako typ obsługiwany przez Handlebars. Nie jest to zbyt efektowne, ale ułatwia wyświetlanie dynamicznego kodu HTML.
<h2>, </h2>
Department: <br/>
Email: <a href='mailto:'></a>
Następnie jest on kompilowany z powrotem w naszym kodzie JavaScript:
//Create our template
var source = $("#employeeTemplate").html();
template = Handlebars.compile(source);
Zacznijmy teraz pracę z IndexedDB. Najpierw ją otworzymy.
var openRequest = indexedDB.open("employees", 1);
Otwarcie połączenia z IndexedDB daje nam dostęp do odczytu i zapisu danych, ale zanim to zrobimy, musimy się upewnić, że mamy obiektStore. Obiektowy magazyn danych jest jak tabela w bazie danych. Jedna baza IndexedDB może zawierać wiele obiektów, z których każdy przechowuje zbiór powiązanych obiektów. Nasz pokaz jest prosty i wymaga tylko jednego obiektu przechowywania, który nazywamy „employee”. Gdy IndexedDb zostanie otwarta po raz pierwszy lub gdy zmienisz wersję w kodzie, zostanie uruchomione zdarzenie onupgradeneeded. Możemy użyć tego do skonfigurowania obiektu obiektowego.
// 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();
};
W bloku onupgradeneeded
sprawdzamy tablicę obiektów objectStoreNames, aby sprawdzić, czy zawiera ona pracownika. Jeśli nie, po prostu to zrobimy. Wywołanie createIndex jest ważne. Musimy poinformować IndexedDB, których metod poza kluczami użyjemy do pobierania danych. Użyjemy klucza o nazwie searchkey. Więcej informacji znajdziesz poniżej.
Zdarzenie onungradeneeded
zostanie uruchomione automatycznie przy pierwszym uruchomieniu skryptu. Po wykonaniu lub pominięciu w przyszłych wywołaniach wykonywany jest przetwarzacz onsuccess
. Mamy zdefiniowany prosty (i brzydki) moduł obsługi błędów, który wywołuje funkcję handleSeed
.
Zanim przejdziemy dalej, sprawdźmy, co tu się dzieje. Otwieramy bazę danych. Sprawdzamy, czy istnieje nasz obiekt magazynu. Jeśli nie, tworzymy je. Na koniec wywołujemy funkcję o nazwie handleSeed. Teraz przyjrzyjmy się części prezentacji poświęconej zasiewaniu danych.
Gimme Some Data
Jak wspomniano we wstępie do tego artykułu, to demo odtwarza aplikację w stylu intranetu, która musi przechowywać kopię wszystkich znanych pracowników. Zwykle wymaga to utworzenia interfejsu API na serwerze, który zwraca liczbę pracowników i umożliwia nam pobieranie partii rekordów. Wyobraź sobie prostą usługę, która obsługuje początkową liczbę i zwraca 100 osób naraz. Może ona działać asynchronicznie w tle, gdy użytkownik robi coś innego.
Na potrzeby tego pokazu zrobimy coś prostego. Widzimy, ile obiektów (jeśli w ogóle) mamy w IndexedDB. Jeśli liczba użytkowników jest poniżej określonej wartości, po prostu tworzymy fałszywych użytkowników. W przeciwnym razie uznajemy, że część z danymi wyjściowymi została zakończona i możemy włączyć część demonstracji dotyczącą autouzupełniania. Przyjrzyjmy się funkcji 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();
}
};
}
Pierwszy wiersz jest nieco skomplikowany, ponieważ zawiera wiele operacji połączonych ze sobą, więc rozłóżmy go na części:
db.transaction(["employee"], "readonly");
Spowoduje to utworzenie nowej transakcji tylko do odczytu. Wszystkie operacje na danych w IndexedDB wymagają jakiejś transakcji.
objectStore("employee");
Pobierz pamięć obiektów pracownika.
count()
Uruchom interfejs API count, który – jak się domyślasz – zlicza elementy.
onsuccess = function(e) {
A gdy skończysz – wykonaj tę funkcję z argumentem zwrotnym. Wewnątrz wywołania zwrotnego możemy uzyskać wartość wyniku, która jest liczbą obiektów. Jeśli liczba jest równa 0, rozpoczynamy proces tworzenia próbki.
Używamy wspomnianego wcześniej elementu stanu, aby wyświetlić użytkownikowi komunikat o tym, że zaczniemy zbierać dane. Ze względu na asynchroniczny charakter IndexedDB skonfigurowaliśmy prostą zmienną done, która będzie śledzić dodawane elementy. Przewijamy i wstawiamy fałszywych ludzi. Źródło tej funkcji jest dostępne w pliku do pobrania, ale zwraca obiekt o takiej postaci:
{
firstname: "Random Name",
lastname: "Some Random Last Name",
department: "One of 8 random departments",
email: "first letter of firstname+lastname@fakecorp.com"
}
To samo w sobie wystarcza do zdefiniowania osoby. Mamy jednak szczególne wymagania dotyczące wyszukiwania danych. IndexedDB nie umożliwia wyszukiwania elementów bez rozróżniania wielkości liter. Dlatego tworzymy kopię pola „lastname” w nowej właściwości o nazwie „searchkey”. Jak pamiętasz, jest to klucz, który według nas powinien zostać utworzony jako indeks danych.
// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();
Ponieważ jest to modyfikacja po stronie klienta, została ona wykonana tutaj, a nie na serwerze zaplecza (w naszym przypadku wyimaginowanym).
Aby dodawanie danych do bazy danych było wydajne, należy ponownie użyć transakcji dla wszystkich zbiorczych operacji zapisu. Jeśli dla każdej operacji zapisu utworzysz nową transakcję, przeglądarka może wywołać zapis na dysku dla każdej transakcji, co spowoduje bardzo słabą wydajność podczas dodawania dużej liczby elementów (np. 1 minuta na zapis 1000 obiektów).
Gdy inicjalizacja zostanie zakończona, uruchamiana jest następna część aplikacji – setupAutoComplete.
Tworzenie autouzupełniania
Teraz najciekawsza część – połączenie z wtyczką Autouzupełnianie jQuery UI. Podobnie jak w przypadku większości elementów interfejsu jQuery, zaczynamy od podstawowego elementu HTML i ulepszamy go, wywołując metodę konstruktora. Cały proces został zastąpiony przez funkcję o nazwie setupAutoComplete. Przyjrzyjmy się teraz temu kodom.
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));
}
});
}
Najbardziej skomplikowaną częścią tego kodu jest utworzenie usługi źródłowej. Element sterujący Autouzupełnianie w bibliotece jQuery UI umożliwia zdefiniowanie właściwości źródłowej, którą można dostosować do wszelkich potrzeb, nawet do danych IndexedDB. Interfejs API przekazuje żądanie (czyli to, co zostało wpisane w polu formularza) i wywołanie zwrotne odpowiedzi. Odpowiadasz za wysłanie tablicy wyników do wywołania zwrotnego.
Najpierw ukrywamy element displayEmployee div. Służy on do wyświetlania informacji o konkretnym pracowniku, a jeśli taki pracownik został już wcześniej załadowany, to oczyszczamy go. Teraz możemy rozpocząć wyszukiwanie.
Najpierw tworzymy transakcję tylko do odczytu, tablicę o nazwie result i obsługę oncomplete, która po prostu przekazuje wynik do elementu sterującego autouzupełniania.
Aby znaleźć elementy pasujące do naszego wejścia, skorzystajmy z rady użytkownika StackOverflow Fong-Wan Chau: używamy zakresu indeksu na podstawie wejścia jako dolnej granicy i wejścia plus litery z jako górnej granicy. Pamiętaj też, że zmieniamy nazwę na wersję z małą litery, aby pasowała do wprowadzonych przez nas danych.
Gdy to zrobisz, możesz otworzyć kursor (traktuj go jak zapytanie do bazy danych) i przetworzyć wyniki. Element automatycznego uzupełniania jQuery UI umożliwia zwracanie dowolnego typu danych, ale wymaga co najmniej klucza wartości. Wartość ustawiamy na ładnie sformatowaną wersję nazwy. Zwracamy też całą osobę. Zaraz zobaczysz, dlaczego. Oto zrzut ekranu pokazujący działanie autouzupełniania. Do interfejsu jQuery UI używamy motywu Vader.
To samo wystarczy, aby zwrócić wyniki dopasowań IndexedDB do autouzupełniania. Chcemy jednak też wyświetlać szczegółowy widok dopasowania po wybraniu takiego dopasowania. Podczas tworzenia autouzupełniania użyliśmy selektora, który korzysta z wcześniejszego szablonu Handlebars.