Wprowadzenie
IndexedDB to skuteczny sposób przechowywania danych po stronie klienta. Jeśli jeszcze nie znasz tego tematu, zachęcam do zapoznania się z przydatnymi samouczkami MDN. W tym artykule zakładamy, że masz podstawową wiedzę o interfejsach API i funkcjach. Nawet jeśli nie znasz IndexedDB, wersja demonstracyjna w tym artykule powinna dać Ci wyobrażenie o tym, co można zrobić za pomocą tej bazy danych.
Nasza wersja demonstracyjna to prosta aplikacja intranetowa dla firmy, która stanowi model koncepcyjny. Aplikacja umożliwi pracownikom wyszukiwanie innych pracowników. Aby zapewnić szybsze i bardziej płynne działanie, baza danych pracowników jest kopiowana na komputer klienta i przechowywana za pomocą IndexedDB. Wersja demonstracyjna udostępnia po prostu wyszukiwanie w stylu autouzupełniania i wyświetlanie pojedynczego rekordu pracownika, ale zaletą jest to, że gdy te dane są dostępne na kliencie, możemy ich używać na wiele innych sposobów. Oto podstawowy zarys tego, co musi 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 zarówno w Chrome, jak i w Firefoksie, może być nieco trudne.
- Musimy sprawdzić, czy mamy jakieś dane, a jeśli nie, pobrać je. Zwykle odbywa się to za pomocą wywołań AJAX. Na potrzeby tej prezentacji utworzyliśmy prostą klasę narzędziową, która pozwala szybko generować fałszywe dane. Aplikacja musi rozpoznawać, kiedy tworzy te dane, i do tego czasu uniemożliwiać użytkownikowi korzystanie z nich. To jednorazowa operacja. Gdy użytkownik uruchomi aplikację następnym razem, nie będzie musiał przechodzić przez ten proces. Bardziej zaawansowana wersja demonstracyjna obsługiwałaby operacje synchronizacji między klientem a serwerem, ale ta wersja skupia się bardziej na aspektach interfejsu.
- Gdy aplikacja będzie gotowa, możemy użyć elementu sterującego Autouzupełnianie w jQuery UI, aby zsynchronizować ją z IndexedDB. Chociaż element sterujący autouzupełniania umożliwia tworzenie podstawowych list i tablic danych, ma interfejs API, który umożliwia korzystanie z dowolnego źródła danych. Pokażemy, jak można go użyć do połączenia z danymi IndexedDB.
Pierwsze kroki
Ta wersja demonstracyjna składa się z kilku części. Aby uprościć sprawę, zacznijmy od 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? Interfejs ten ma 3 główne aspekty, które nas interesują. Pierwsze to pole „name”, które będzie używane do autouzupełniania. Wczytuje się jako wyłączony i jest później włączany za pomocą JavaScriptu. Span obok niego jest używany podczas początkowego wypełniania, aby przekazywać użytkownikowi aktualizacje. Na koniec element div z identyfikatorem displayEmployee zostanie użyty, gdy wybierzesz pracownika z automatycznych sugestii.
Teraz przyjrzyjmy się kodowi JavaScript. Jest tu wiele informacji, więc omówimy je krok po kroku. Pełny kod będzie dostępny na końcu, aby można było go zobaczyć w całości.
Po pierwsze – w przypadku przeglądarek obsługujących IndexedDB musimy się martwić o pewne problemy z prefiksami. Oto kod z dokumentacji Mozilli zmodyfikowany tak, aby udostępniał proste aliasy 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;
Następnie kilka zmiennych globalnych, których będziemy używać w ramach demonstracji:
var db;
var template;
Zaczniemy od bloku jQuery document ready:
$(document).ready(function() {
console.log("Startup...");
...
});
Nasza wersja demonstracyjna używa Handlebars.js do wyświetlania danych pracownika. Nie będzie on używany od razu, ale możemy go teraz skompilować i mieć to z głowy. Mamy blok skryptu skonfigurowany jako typ rozpoznawany przez Handlebars. Nie jest to zbyt skomplikowane, 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 w ten sposób:
//Create our template
var source = $("#employeeTemplate").html();
template = Handlebars.compile(source);
Teraz zacznijmy pracę z IndexedDB. Najpierw otwieramy.
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 upewnić się, że mamy obiekt objectStore. Magazyn obiektów jest podobny do tabeli bazy danych. Jedna baza IndexedDB może mieć wiele magazynów obiektów, z których każdy zawiera kolekcję powiązanych obiektów. Nasza wersja demonstracyjna jest prosta i wymaga tylko jednego obiektu objectStore, który nazywamy „employee”. Gdy baza danych indexedDB jest otwierana po raz pierwszy lub gdy zmieniasz wersję w kodzie, uruchamiane jest zdarzenie onupgradeneeded. Możemy go użyć do skonfigurowania naszego magazynu obiektów.
// 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 modułu obsługi zdarzeń onupgradeneeded sprawdzamy, czy tablica magazynów obiektów objectStoreNames zawiera magazyn employee. Jeśli nie, po prostu to robimy. Wywołanie createIndex jest ważne. Musimy poinformować IndexedDB, jakich metod, poza kluczami, będziemy używać do pobierania danych. Użyjemy klucza o nazwie searchkey. Wyjaśnimy to za chwilę.
Zdarzenie onungradeneeded zostanie uruchomione automatycznie przy pierwszym uruchomieniu skryptu. Po wykonaniu lub pominięciu w przyszłych uruchomieniach zostanie uruchomiony moduł obsługi onsuccess. Mamy zdefiniowany prosty (i brzydki) moduł obsługi błędów, a następnie wywołujemy handleSeed.
Zanim przejdziemy dalej, podsumujmy, co się dzieje. Otwieramy bazę danych. Sprawdzamy, czy nasz magazyn obiektów istnieje. Jeśli nie, tworzymy go. Na koniec wywołujemy funkcję o nazwie handleSeed. Teraz skupmy się na części prezentacji dotyczącej wypełniania bazy danych.
Daj mi jakieś dane!
Jak wspomniano we wprowadzeniu do tego artykułu, ta wersja demonstracyjna odtwarza aplikację w stylu intranetu, która musi przechowywać kopię wszystkich znanych pracowników. Zwykle wymagałoby to utworzenia interfejsu API opartego na serwerze, który mógłby zwracać liczbę pracowników i umożliwiać pobieranie partii rekordów. Możesz wyobrazić 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 wykonuje inne czynności.
Na potrzeby tej prezentacji zrobimy coś prostego. Możemy sprawdzić, ile obiektów (jeśli w ogóle) znajduje się w IndexedDB. Jeśli liczba ta jest mniejsza od określonej, po prostu utworzymy fałszywych użytkowników. W przeciwnym razie uznajemy, że część dotyczącą danych początkowych mamy za sobą i możemy włączyć część demonstracyjną 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 kilka połączonych ze sobą operacji, więc omówmy go szczegółowo:
db.transaction(["employee"], "readonly");
Spowoduje to utworzenie nowej transakcji tylko do odczytu. Wszystkie operacje na danych w IndexedDB wymagają transakcji określonego rodzaju.
objectStore("employee");
Pobierz magazyn obiektów pracowników.
count()
Uruchom interfejs API count, który – jak można się domyślić – wykonuje zliczanie.
onsuccess = function(e) {
Po zakończeniu wykonaj to wywołanie zwrotne. W wywołaniu zwrotnym możemy uzyskać wartość wyniku, czyli liczbę obiektów. Jeśli liczba wynosiła 0, rozpoczynamy proces generowania.
Używamy wspomnianego wcześniej elementu div stanu, aby wyświetlić użytkownikowi komunikat, że zaczynamy pobierać dane. Ze względu na asynchroniczny charakter IndexedDB skonfigurowaliśmy prostą zmienną done, która będzie śledzić dodawanie. Przechodzimy przez pętlę i wstawiamy fałszywe osoby. Kod źródłowy tej funkcji jest dostępny w pobranym pliku, ale zwraca ona obiekt, który wygląda tak:
{
firstname: "Random Name",
lastname: "Some Random Last Name",
department: "One of 8 random departments",
email: "first letter of firstname+lastname@fakecorp.com"
}
To wystarczy, aby zdefiniować osobę. Mamy jednak specjalne wymagania, aby móc przeszukiwać nasze dane. IndexedDB nie umożliwia wyszukiwania elementów bez rozróżniania wielkości liter. Dlatego tworzymy kopię pola lastname w nowej właściwości searchkey. Jeśli pamiętasz, jest to klucz, który powinien zostać utworzony jako indeks naszych danych.
// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();
Ponieważ jest to modyfikacja specyficzna dla klienta, jest ona wykonywana tutaj, a nie na serwerze backendu (lub w naszym przypadku na wyimaginowanym serwerze backendu).
Aby wydajnie dodawać dane do bazy danych, należy ponownie użyć transakcji w przypadku wszystkich zapisów grupowych. Jeśli utworzysz nową transakcję dla każdego zapisu, przeglądarka może spowodować zapis na dysku dla każdej transakcji, co znacznie obniży wydajność podczas dodawania wielu elementów (np. „1 minuta na zapisanie 1000 obiektów”).
Po zakończeniu wypełniania bazy danych uruchamiana jest kolejna część naszej aplikacji – setupAutoComplete.
Tworzenie autouzupełniania
Teraz najprzyjemniejsza część – połączenie z wtyczką Autocomplete interfejsu jQuery. Podobnie jak w przypadku większości interfejsu jQuery, zaczynamy od podstawowego elementu HTML i ulepszamy go, wywołując na nim metodę konstruktora. Cały proces wyodrębniliśmy do funkcji o nazwie setupAutoComplete. Przyjrzyjmy się teraz temu kodowi.
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 złożoną częścią tego kodu jest utworzenie usługi źródłowej. Element sterujący autouzupełniania w jQuery UI umożliwia zdefiniowanie właściwości źródła, którą można dostosować do dowolnych potrzeb – nawet do danych IndexedDB. Interfejs API udostępnia żądanie (czyli to, co zostało wpisane w polu formularza) i wywołanie zwrotne odpowiedzi. Odpowiadasz za wysłanie tablicy wyników z powrotem do tego wywołania zwrotnego.
Najpierw ukrywamy element div displayEmployee. Służy on do wyświetlania pojedynczego pracownika i wyczyszczenia go, jeśli został wcześniej wczytany. Teraz możemy rozpocząć wyszukiwanie.
Zaczynamy od utworzenia transakcji tylko do odczytu, tablicy o nazwie result i modułu obsługi oncomplete, który po prostu przekazuje wynik do elementu sterującego autouzupełniania.
Aby znaleźć elementy pasujące do danych wejściowych, skorzystajmy ze wskazówki użytkownika StackOverflow o nicku Fong-Wan Chau: użyjemy zakresu indeksu opartego na danych wejściowych jako dolnej granicy zakresu, a danych wejściowych z literą „z” jako górnej granicy zakresu. Zwróć też uwagę, że używamy małych liter, aby dopasować wyszukiwane hasło do wprowadzonych danych.
Po zakończeniu możemy otworzyć kursor (pomyśl o tym jak o uruchomieniu zapytania do bazy danych) i przejść przez wyniki. Kontrolka autouzupełniania jQuery UI umożliwia zwracanie dowolnego typu danych, ale wymaga co najmniej klucza wartości. Ustawimy wartość na ładnie sformatowaną wersję nazwy. Zwracamy też całą osobę. Za chwilę dowiesz się dlaczego. Najpierw zrzut ekranu przedstawiający działanie autouzupełniania. Używamy motywu Vader dla jQuery UI.
To wystarczy, aby zwrócić wyniki dopasowań IndexedDB do autouzupełniania. Chcemy też jednak umożliwić wyświetlanie widoku szczegółowego wybranego meczu. Podczas tworzenia autouzupełniania określiliśmy moduł obsługi wyboru, który korzysta z utworzonego wcześniej szablonu Handlebars.