Łączenie elementów interfejsu Databinding za pomocą IndexedDB

Raymonda Camdena
Raymond Camden

Wstęp

Technologia IndexedDB to zaawansowany sposób przechowywania danych po stronie klienta. Jeśli chcesz przeczytać ten artykuł, zapoznaj się z przydatnymi samouczkami MDN na ten temat. W tym artykule przyjęliśmy podstawowe informacje o interfejsach API i ich funkcjach. Nawet jeśli nie znasz jeszcze platformy IndexedDB, mamy nadzieję, że prezentacja przedstawiona w tym artykule podpowie Ci, do czego może służyć.

Nasza prezentacja to prosty przykład koncepcyjny aplikacji intranetowej dla firmy. Dzięki niej pracownicy będą mogli wyszukać innych pracowników. Aby przyspieszyć i usprawnić działanie usługi, baza danych pracowników jest kopiowana na komputer klienta i przechowywana przy użyciu IndexedDB. Wersja demonstracyjna zawiera wyszukiwanie w stylu autouzupełniania i wyświetlanie wpisów pojedynczego pracownika, ale przydatne jest to, że gdy dane te zostaną udostępnione u klienta, można ich używać również na wiele innych sposobów. Oto podstawowy zarys tego, co musi zrobić nasza aplikacja.

  1. Musimy skonfigurować i zainicjować instancję IndexedDB. W większości przypadków jest to proste, ale obsługa zarówno w Chrome, jak i w Firefoksie okazuje się niezbyt skomplikowana.
  2. Musimy sprawdzić, czy mamy jakieś dane, a jeśli nie, pobrać je. Obecnie robi się to zwykle za pomocą wywołań AJAX. Na potrzeby naszego przykładu utworzyliśmy prostą klasę użytkową do szybkiego generowania fałszywych danych. Aplikacja musi wykryć, kiedy tworzy dane, i uniemożliwić użytkownikowi korzystanie z nich do tego czasu. Jest to operacja jednorazowa. Przy następnym uruchomieniu aplikacji użytkownik nie będzie musiał przechodzić tego procesu. Bardziej zaawansowana prezentacja obsługuje operacje synchronizacji między klientem a serwerem, ale ta prezentacja koncentruje się na aspektach interfejsu użytkownika.
  3. Gdy aplikacja będzie gotowa, możemy użyć funkcji autouzupełniania w interfejsie jQuery, aby zsynchronizować dane z IndexedDB. Ustawienie autouzupełniania umożliwia używanie podstawowych list i tablic danych, ale ma interfejs API umożliwiający korzystanie z dowolnego źródła danych. Zademonstrujemy, jak użyć go do połączenia z danymi IndexedDB.

Pierwsze kroki

Składa się ona z wielu części, 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? Ten interfejs ma 3 główne aspekty. Pierwszym z nich jest pole „name”, które będzie używane do autouzupełniania. Jest on wczytywany i zostanie włączony później za pomocą JavaScriptu. Sąsiadujący z nim span jest używany podczas początkowego początkowego punktu początkowego, aby przekazywać użytkownikom aktualizacje. I wreszcie, po wybraniu pracownika z automatycznej sugestii zostanie użyty element div z identyfikatorem displayPracownik.

Przyjrzyjmy się teraz kodowi JavaScript. Jest tu bardzo dużo do przeanalizowania, więc omówimy to krok po kroku. Pełny kod będzie dostępny na końcu, dzięki czemu będzie można zobaczyć go w całości.

Po pierwsze, w przeglądarkach, które obsługują IndexedDB, musimy obawiać się pewnych problemów z prefiksami. Oto fragment kodu z dokumentacji Mozilla, który został zmodyfikowany w celu udostępnienia prostych aliasów dla 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 mamy kilka zmiennych globalnych, których będziemy używać w trakcie prezentacji:

var db;
var template;

Zaczniemy od bloku gotowego dokumentu jQuery:

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

W naszej wersji demonstracyjnej do wyświetlania informacji o pracowniku używamy pliku Handlebars.js. Nie użyjemy jej później, ale możemy śmiało skompilować nasz szablon i z niego korzystać. Mamy blok skryptu skonfigurowany jako typ rozpoznawany przez kierownicę. Nie jest to zbyt wymyślne, ale ułatwia wyświetlanie dynamicznego kodu HTML.

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

Następnie kod jest kompilowany w naszym JavaScripcie w ten sposób:

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

Teraz zacznijmy pracę z IndexedDB. Po pierwsze, 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 się upewnić, że mamy obiektObjectStore. ObiektObjectStore jest jak tabela bazy danych. Jedna baza IndexedDB może mieć wiele obiektów objectStores, z których każdy zawiera zbiór powiązanych obiektów. Nasza demonstracja jest prosta i potrzebuje tylko 1 obiektu ObjectStore o nazwie „person” (pracownik). Przy pierwszym otwarciu bazy danych lub zmianie wersji w kodzie wykonywane jest zdarzenie onupgradeneeded. Możemy użyć tego do skonfigurowania naszego obiektuobjectStore.

// 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 obiekt będący w tablicy magazynów obiektów, aby zobaczyć, czy zawiera on pracownika. Jeśli nie, to po prostu to robimy. Wywołanie createIndex jest ważne. Musimy wskazać usłudze IndexedDB, które metody poza kluczami będą używane do pobierania danych. Użyjemy klucza wyszukiwania. Wyjaśniliśmy to pokrótce.

Zdarzenie onungradeneeded zostanie uruchomione automatycznie po pierwszym uruchomieniu skryptu. Po wykonaniu lub pominięciu w przyszłych uruchomieniach moduł obsługi onsuccess jest uruchamiany. Zdefiniowaliśmy prosty (i obrzydliwy) moduł obsługi błędów, a następnie nazywamy go handleSeed.

Zanim przejdziemy dalej, sprawdźmy, co się tutaj dzieje. Otwieramy bazę danych. Sprawdzamy, czy nasza baza obiektów istnieje. Jeśli tak się nie stanie, utworzymy go. Na koniec wywołujemy funkcję o nazwie handleSeed. A teraz przyjrzyjmy się części prezentacji poświęconej modelowaniu danych.

Chcę trochę danych!

Jak wspomnieliśmy we wprowadzeniu do tego artykułu, ta wersja demonstracyjna odtwarza aplikację w stylu intranetowym, która musi przechowywać kopię wszystkich znanych pracowników. Zwykle wymaga to utworzenia opartego na serwerze interfejsu API, który zwraca liczbę pracowników i umożliwia pobranie ich partii. Możesz sobie wyobrazić prostą usługę, która obsługuje licznik uruchomień i zwraca 100 osób naraz. Działanie to może być uruchamiane asynchronicznie w tle, gdy użytkownik nie wykonuje innych czynności.

Na potrzeby naszej demonstracji robimy coś prostego. Widzimy, ile obiektów znajduje się w indeksie IndexedDB. Jeśli wartość spadnie poniżej pewnej wartości, utworzymy po prostu fałszywych użytkowników. W przeciwnym razie uznamy, że część wyjściowa jest gotowa i możemy włączyć autouzupełnianie w części demonstracyjnej. Spójrzmy na funkcję 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 trochę skomplikowany, ponieważ łączy ze sobą wiele operacji. Przyjrzyjmy się im:

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

Spowoduje to utworzenie nowej transakcji tylko do odczytu. Wszystkie operacje na danych za pomocą IndexedDB wymagają transakcji jakiegoś rodzaju.

objectStore("employee");

Pobranie magazynu obiektów pracownika.

count()

Uruchomienie interfejsu Count API wykonuje liczenie.

onsuccess = function(e) {

Gdy skończysz, wykonaj to wywołanie zwrotne. W wywołaniu zwrotnym możemy uzyskać wartość wynikową, która jest liczbą obiektów. Jeśli liczba wynosi zero, rozpoczynamy proces inicjowania.

Używamy wspomnianego wcześniej elementu div stanu, aby przekazać użytkownikowi informację, że zaczniemy pobierać dane. Ze względu na asynchroniczną naturę interfejsu IndexedDB skonfigurowaliśmy prostą zmienną, która będzie śledziła dodania. Wracamy do tematu i wstawiamy fałszywe osoby. Źródło tej funkcji jest dostępne do pobrania, ale zwraca obiekt podobny do tego:

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

To wystarczy do zdefiniowania osoby. Mamy jednak szczególne wymagania, które pozwolą nam przeszukiwać nasze dane. Indeks IndexedDB nie umożliwia wyszukiwania elementów z uwzględnieniem wielkości liter. Dlatego kopiujemy pole nazwiska do nowej usługi – klucza wyszukiwania. Jeśli pamiętasz, to klucz, który naszym zdaniem powinniśmy utworzyć jako indeks dla naszych danych.

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

Jest to modyfikacja specyficzna dla klienta, dlatego wprowadza się ją tutaj, a nie na serwerze backendu (lub w naszym przypadku na fikcyjnym serwerze backendu).

Aby skutecznie dodawać elementy do bazy danych, używaj tej samej transakcji ponownie we wszystkich zapisach wsadowych. Jeśli utworzysz nową transakcję dla każdego zapisu, przeglądarka może spowodować zapis na dysku dla każdej transakcji, co spowoduje straszne obniżenie wydajności przy dodawaniu wielu elementów (pomyśl, że „1 minuta na zapisanie 1000 obiektów” jest straszna).

Po wygenerowaniu uruchamiana jest kolejna część naszej aplikacji – setupAutoComplete.

Tworzenie autouzupełniania

Teraz zaczyna się najciekawsza część – nawiąż połączenie z wtyczką Autocomplete w interfejsie jQuery. Podobnie jak w przypadku większości interfejsu jQuery, rozpoczynamy od podstawowego elementu HTML i udoskonalamy go, wywołując w nim metodę konstruktora. Cały proces wyodrębniliśmy i wybraliśmy funkcję o nazwie setupAutoComplete. Spójrzmy na kod.

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. Ustawienie autouzupełniania w interfejsie jQuery umożliwia zdefiniowanie usługi źródłowej, którą można dostosować do dowolnych potrzeb – nawet danych IndexedDB. Interfejs API udostępnia żądanie (czyli to, co zostało wpisane w polu formularza) i wywołanie zwrotne odpowiedzi. Odpowiadasz za odesłanie tablicy wyników z powrotem do tego wywołania zwrotnego.

Pierwszą rzeczą, jaką robimy, jest ukrycie elementu div displayPracownik. Służy on do wyświetlania pojedynczego pracownika i jeśli został wczytany, aby go usunąć. Możemy zacząć szukać.

Zaczynamy od utworzenia transakcji tylko do odczytu, tablicy o nazwie „wynik” i modułu obsługi oncomplete, który po prostu przekazuje wynik do elementu sterującego autouzupełniania.

Aby znaleźć elementy pasujące do naszych danych wejściowych, posłużmy się wskazówką użytkownika StackOverflow, Fong-Wan Chau. Zakres indeksu oparty na wprowadzonych danych to dolna granica zakresu, a górna granica – litera Z. Uwaga: słowo jest pisane małymi literami, aby zgadzało się z wpisanymi małymi literami.

Gdy to zrobisz, możemy otworzyć kursor (to działa jak zapytanie do bazy danych) i iterować wyniki. Funkcja autouzupełniania w interfejsie jQuery umożliwia zwracanie dowolnego typu danych, przy czym wymaga co najmniej klucza wartości. Ustawiliśmy wartość na odpowiednio sformatowaną wersję nazwy. Zwracamy też całą osobę. Za chwilę zobaczysz, dlaczego tak jest. Pierwszy zrzut ekranu z działalnością autouzupełniania. Używamy motywu Vader dla interfejsu jQuery.

To wystarczy, aby wyniki naszych dopasowań IndexedDB były zwracane w trybie autouzupełniania. Chcemy też umożliwić wyświetlanie widoku szczegółowego pasującego wyniku po ich wybraniu. Podczas tworzenia autouzupełniania, które korzysta z szablonu Kierownica z wcześniejszego okresu, podaliśmy wybraną opcję obsługi.