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

Raymond Camden
Raymond Camden

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ę, do czego można go używać.

Nasza wersja demonstracyjna stanowi 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.

  1. Musimy skonfigurować i zainicjować instancję IndexedDB. W większości przypadków jest to całkiem proste, ale prawidłowe działanie tej przeglądarki zarówno w Chrome, jak i w Firefoksie jest nieco trudne.
  2. Musimy sprawdzić, czy mamy jakieś dane, a jeśli nie, pobierz 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żywanie 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.
  3. Gdy aplikacja będzie gotowa, możemy użyć elementu sterującego Autocomplete w bibliotece jQuery UI, aby zsynchronizować ją z IndexedDB. Ustawienie autouzupełniania umożliwia tworzenie podstawowych list i tablic z danymi, ale ma interfejs API, który pozwala na użycie dowolnego źródła danych. Pokażę, jak można użyć tego do połączenia 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. Trzeba tutaj bardzo szczegółowo opisać, co omówimy 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;

Następnie kilka zmiennych globalnych, które będziemy używać w prezentacji:

var db;
var template;

Zaczniemy od bloku jQuery document ready:

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

W naszej wersji demonstracyjnej użyto pliku Handlebars.js do wyświetlania danych pracownika. Nie jest on używany dopóki nie skompilujemy szablonu, ale możemy to zrobić teraz, aby nie zajmował miejsca. 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. Nasza wersja demonstracyjna jest prosta i wymaga tylko jednego obiektuobjectStore nazywanego „pracownikiem”. Przy pierwszym otwarciu zasobu IndexDB lub zmianie wersji w kodzie uruchamiane jest zdarzenie onupgradeneeded. Możemy użyć tego do skonfigurowania naszego 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();
};

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, jakich metod (poza kluczami) będziemy używać 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 zostaje uruchomiony onsuccess. Mamy zdefiniowany prosty (i brzydki) moduł obsługi błędów, który nazywamy handleSeed.

Zanim przejdziemy dalej, sprawdźmy, co tu się dzieje. Otwieramy bazę danych. Sprawdzamy, czy nasz magazyn obiektów istnieje. Jeśli nie, tworzymy je. Na koniec wywołujemy funkcję o nazwie handleSeed. Teraz skupmy się na 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. Możesz sobie wyobrazić prostą usługę, która obsługuje liczbę początkową i zwraca 100 osób naraz. Narzędzie to może działać asynchronicznie w tle, gdy użytkownik nie może wykonywać innych czynności.

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 z użyciem IndexedDB wymagają jakiejś transakcji.

objectStore("employee");

Pobierz pamięć obiektów pracownika.

count()

Uruchom interfejs API liczenia, który, jak łatwo się domyślić, zlicza liczbę.

onsuccess = function(e) {

A gdy skończysz – wykonaj to wywołanie zwrotne. Wewnątrz funkcji 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 wystarczająco dużo, aby zdefiniować osobę. Mamy jednak szczególne wymagania dotyczące wyszukiwania danych. IndexedDB nie umożliwia wyszukiwania elementów bez rozróżniania wielkości liter. Dlatego skopiujemy pole lastname do nowej właściwości – 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 specyficzna dla klienta, przeprowadzana jest tutaj, a nie na serwerze backendu (lub w naszym przypadku na fikcyjnym serwerze backendu).

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

Najciekawsza jest część, którą teraz zajmiemy się wtyczką autouzupełniania interfejsu jQuery. 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. Spójrzmy teraz na ten 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. 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. Twoim obowiązkiem jest wysłanie tablicy wyników z powrotem do tego 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 zapisujemy hasło małymi literami, aby dopasować je 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. Ustawiamy wartość na dobrze sformatowaną wersję nazwy. Zwracamy także 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.