Wprowadzenie
IndexedDB to skuteczny sposób przechowywania 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 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 działania w Chrome i Firefoksie jest nieco bardziej skomplikowane.
- 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 w bibliotece 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ą można się łączyć 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ą kodu JavaScript. 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 do omówienia, więc zajmijmy się tym krok po kroku. Na koniec 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 zwracać uwagę na 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;
Zaczniemy od bloku jQuery document ready:
$(document).ready(function() {
console.log("Startup...");
...
});
Nasz pokaz używa 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 jego wykonaniu lub pominięciu w przyszłych wywołaniach zostaje uruchomiony 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 skupmy się na części prezentacji poświęconej inicjowaniu 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 liczbę początkową i zwraca 100 osób naraz. Może to działać asynchronicznie w tle, gdy użytkownik robi coś innego.
Na potrzeby tego demonstracyjnego projektu 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 uważamy, że część z danymi wyjściowymi została już wykorzystana i możemy włączyć część demonstracyjną 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ę zwracającą wartość. 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. Wstawiamy sztucznych 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 [email protected]"
}
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 nazwanej „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, jest ona wykonywana 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ł przeniesiony do funkcji 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 powoduje jego wyczyszczenie. 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.