IndexedDB ile veri bağlama kullanıcı arayüzü öğeleri

Raymond Camden
Raymond Camden

Giriş

IndexedDB, istemci tarafında veri depolamanın etkili bir yoludur. Henüz incelemediyseniz konuyla ilgili faydalı MDN eğitimlerini okumanızı öneririz. Bu makalede, API'ler ve özellikler hakkında temel bilgilere sahip olduğunuz varsayılmaktadır. IndexedDB'yi daha önce görmemiş olsanız bile bu makaledeki demo, IndexedDB ile neler yapılabileceği hakkında size fikir verecektir.

Demomuz, bir şirket için basit bir kavram kanıtlama intranet uygulamasıdır. Uygulama, çalışanların diğer çalışanları aramasına olanak tanıyacak. Daha hızlı ve akıcı bir deneyim sunmak için çalışan veritabanı, istemcinin makinesine kopyalanır ve IndexedDB kullanılarak depolanır. Demo, otomatik tamamlama tarzı bir arama ve tek bir çalışan kaydının görüntülenmesini sağlar. Ancak bu veriler istemcide kullanılabilir olduğunda, bunları başka şekillerde de kullanabiliriz. Uygulamamızın yapması gerekenleri kısaca özetleyelim.

  1. Bir IndexedDB örneği oluşturup başlatmamız gerekiyor. Bu işlem genellikle basittir ancak hem Chrome hem de Firefox'ta çalışmasını sağlamak biraz zor olabilir.
  2. Verimiz olup olmadığını kontrol etmemiz, yoksa indirmemiz gerekiyor. Normalde bu işlem AJAX çağrıları aracılığıyla yapılır. Demomuz için sahte verileri hızlıca oluşturmak üzere basit bir yardımcı sınıf oluşturduk. Uygulamanın bu verileri ne zaman oluşturduğunu tanıması ve o zamana kadar kullanıcının verileri kullanmasını engellemesi gerekir. Bu tek seferlik bir işlemdir. Kullanıcı uygulamayı bir sonraki çalıştırdığında bu işlemden geçmesi gerekmez. Daha gelişmiş bir demoda istemci ile sunucu arasındaki senkronizasyon işlemleri de ele alınır ancak bu demoda daha çok kullanıcı arayüzüyle ilgili konulara odaklanılmıştır.
  3. Uygulama hazır olduğunda, IndexedDB ile senkronize etmek için jQuery kullanıcı arayüzünün otomatik tamamlama kontrolünü kullanabiliriz. Otomatik tamamlama kontrolü, temel listelere ve veri dizilerine izin verirken herhangi bir veri kaynağına izin veren bir API'ye sahiptir. Bunu IndexedDB verilerimize bağlanmak için nasıl kullanabileceğimizi göstereceğiz.

Başlarken

Bu demoda birden fazla bölüm var. Basit bir başlangıç yapmak için HTML bölümüne bakalım.

<form>
  <p>
    <label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
    </p>
</form>

<div id="displayEmployee"></div>

Pek fazla değil, değil mi? Bu kullanıcı arayüzünde dikkat ettiğimiz üç ana unsur vardır. İlk olarak, otomatik tamamlama için kullanılacak "name" alanı var. Devre dışı olarak yüklenir ve daha sonra JavaScript aracılığıyla etkinleştirilir. Yanındaki span, ilk tohumlama sırasında kullanıcıya güncellemeler sağlamak için kullanılır. Son olarak, otomatik öneriden bir çalışan seçtiğinizde displayEmployee kimliğine sahip div kullanılır.

Şimdi JavaScript'e göz atalım. Burada öğrenilecek çok şey var. Bu yüzden adım adım ilerleyeceğiz. Kodun tamamını görebilmeniz için kodun tamamı sonda gösterilir.

Öncelikle, IndexedDB'yi destekleyen tarayıcılar arasında endişelenmemiz gereken bazı önek sorunları var. Aşağıda, uygulamamızın ihtiyaç duyduğu temel IndexedDB bileşenleri için basit takma adlar sağlamak üzere değiştirilmiş Mozilla dokümanlarından bazı kodlar verilmiştir.

window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

Ardından, demo boyunca kullanacağımız birkaç global değişkeni tanımlayalım:

var db;
var template;

Şimdi jQuery document ready bloğuyla başlayacağız:

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

Demomuzda çalışan ayrıntılarını görüntülemek için Handlebars.js kullanılır. Bu, daha sonra kullanılacak ancak şablonumuzu şimdi derleyip aradan çıkarabiliriz. Handlebars tarafından tanınan bir tür olarak ayarlanmış bir komut dosyası bloğumuz var. Çok gösterişli olmasa da dinamik HTML'yi görüntülemeyi kolaylaştırır.

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

Bu daha sonra JavaScript'imizde şu şekilde derlenir:

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

Şimdi IndexedDB ile çalışmaya başlayalım. Öncelikle dosyayı açalım.

var openRequest = indexedDB.open("employees", 1);

IndexedDB'ye bağlantı açmak, veri okuma ve yazma erişimi sağlar. Ancak bunu yapmadan önce bir objectStore'umuz olduğundan emin olmamız gerekir. objectStore, veritabanı tablosuna benzer. Bir IndexedDB'de, her biri ilgili nesnelerden oluşan bir koleksiyon içeren birçok objectStore olabilir. Demomuz basit ve yalnızca "employee" adını verdiğimiz bir objectStore'a ihtiyaç duyuyor. indexedDB ilk kez açıldığında veya koddaki sürümü değiştirdiğinizde onupgradeneeded etkinliği çalıştırılır. Bunu objectStore'umuzu oluşturmak için kullanabiliriz.

// 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();
};

onupgradeneeded etkinlik işleyici bloğunda, employee nesnesini içerip içermediğini görmek için nesne depoları dizisi olan objectStoreNames'i kontrol ederiz. Yoksa da oluştururuz. createIndex çağrısı önemlidir. IndexedDB'ye, verileri almak için anahtarlar dışında hangi yöntemleri kullanacağımızı söylememiz gerekir. searchkey adlı bir anahtar kullanacağız. Bu konuyu birazdan açıklayacağız.

onungradeneeded etkinliği, komut dosyasını ilk kez çalıştırdığımızda otomatik olarak çalışır. Yürütüldükten veya gelecekteki çalıştırmalarda atlandıktan sonra onsuccess işleyicisi çalıştırılır. Basit (ve çirkin) bir hata işleyici tanımladık ve ardından handleSeed yöntemini çağırdık.

Devam etmeden önce burada neler olduğuna kısaca göz atalım. Veritabanını açıyoruz. Nesne depolama alanımızın mevcut olup olmadığını kontrol ederiz. Yoksa oluştururuz. Son olarak, handleSeed adlı bir işlevi çağırırız. Şimdi demomuzun veri besleme kısmına geçelim.

Bana biraz veri verin.

Bu makalenin girişinde belirtildiği gibi, bu demoda bilinen tüm çalışanların bir kopyasını depolaması gereken intranet tarzı bir uygulama yeniden oluşturulmaktadır. Normalde bu işlem için çalışan sayısını döndürebilen ve kayıt gruplarını almamızı sağlayan sunucu tabanlı bir API oluşturulması gerekir. Başlangıç sayısını destekleyen ve tek seferde 100 kişi döndüren basit bir hizmet düşünün. Bu işlem, kullanıcı başka şeyler yaparken arka planda eşzamansız olarak çalışabilir.

Demomuz için basit bir şey yapalım. IndexedDB'mizde kaç nesne olduğunu (varsa) görürüz. Belirli bir sayının altındaysa sahte kullanıcılar oluştururuz. Aksi takdirde, ilk bölümü tamamlamış sayılırız ve demonun otomatik tamamlama bölümünü etkinleştirebiliriz. handleSeed'e bakalım.

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();
    }
  };
}

İlk satır, birbirine bağlı birden fazla işlem olduğu için biraz karmaşık. Bu yüzden satırı parçalara ayıralım:

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

Bu işlem, yeni bir salt okunur işlem oluşturur. IndexedDB ile yapılan tüm veri işlemleri için bir tür işlem gerekir.

objectStore("employee");

Çalışan nesne deposunu alın.

count()

Count API'yi çalıştırın. Bu API, tahmin edebileceğiniz gibi sayım yapar.

onsuccess = function(e) {

İşlem tamamlandığında bu geri çağırma işlevini yürüt. Geri çağırma işlevinde, nesne sayısı olan sonuç değerini alabiliriz. Sayı sıfırsa temel oluşturma sürecimizi başlatırız.

Kullanıcıya veri almaya başlayacağımızı bildirmek için daha önce bahsedilen durum div'ini kullanırız. IndexedDB'nin eşzamansız yapısı nedeniyle, eklemeleri izleyecek basit bir değişken (done) oluşturduk. Döngü oluşturup sahte kişileri ekliyoruz. Bu işlevin kaynağı indirilen dosyada mevcuttur ancak aşağıdaki gibi bir nesne döndürür:

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

Bu, tek başına bir kişiyi tanımlamak için yeterlidir. Ancak verilerimizde arama yapabilmek için özel bir şartımız var. IndexedDB, öğeleri büyük/küçük harfe duyarlı olmayan bir şekilde arama imkanı sunmaz. Bu nedenle, lastname alanının bir kopyasını searchkey adlı yeni bir özelliğe kopyalarız. Hatırlarsanız bu, verilerimiz için dizin olarak oluşturulması gerektiğini söylediğimiz anahtardır.

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

Bu, istemciye özel bir değişiklik olduğundan arka uç sunucusu (veya bizim örneğimizdeki hayali arka uç sunucusu) yerine burada yapılır.

Veritabanı ekleme işlemlerini performanslı bir şekilde gerçekleştirmek için tüm toplu yazma işlemlerinde işlemi yeniden kullanmanız gerekir. Her yazma işlemi için yeni bir işlem oluşturursanız tarayıcı her işlem için bir disk yazma işlemine neden olabilir. Bu da çok sayıda öğe eklerken performansınızı ciddi şekilde düşürür (ör. "1.000 nesne yazmak için 1 dakika").

Başlangıç işlemi tamamlandıktan sonra uygulamamızın bir sonraki bölümü olan setupAutoComplete tetiklenir.

Otomatik tamamlamayı oluşturma

Şimdi eğlenceli kısma, yani jQuery kullanıcı arayüzü Otomatik Tamamlama eklentisiyle bağlantı kurmaya geçelim. jQuery kullanıcı arayüzünün çoğunda olduğu gibi, temel bir HTML öğesiyle başlar ve üzerinde bir kurucu yöntem çağırarak bu öğeyi geliştiririz. Tüm süreci setupAutoComplete adlı bir işleve soyutladık. Şimdi bu koda göz atalım.

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));
    }
  });

}

Bu kodun en karmaşık kısmı, kaynak mülkün oluşturulmasıdır. jQuery kullanıcı arayüzünün Otomatik Tamamlama denetimi, olası her ihtiyacı (IndexedDB verilerimiz dahil) karşılayacak şekilde özelleştirilebilen bir kaynak özelliği tanımlamanıza olanak tanır. API, size isteği (temel olarak form alanına yazılanları) ve bir yanıt geri çağırma işlevini sağlar. Bu geri çağırmaya bir sonuç dizisi göndermek sizin sorumluluğunuzdadır.

İlk olarak displayEmployee div'ini gizleriz. Bu div, tek bir çalışanı göstermek ve daha önce yüklenen bir çalışan varsa onu temizlemek için kullanılır. Artık aramaya başlayabiliriz.

Salt okunur bir işlem, sonuç adında bir dizi ve sonucu otomatik tamamlama kontrolüne ileten bir oncomplete işleyici oluşturarak başlıyoruz.

Girişimizle eşleşen öğeleri bulmak için StackOverflow kullanıcısı Fong-Wan Chau'nun bir ipucundan yararlanalım: Girişe dayalı bir dizin aralığını alt sınır, girişi ve z harfini üst sınır olarak kullanırız. Girdiğimiz küçük harfli verilerle eşleşmesi için terimi küçük harfle yazdığımıza da dikkat edin.

Bu işlem tamamlandıktan sonra bir imleç açabilir (bunu bir veritabanı sorgusu çalıştırmak gibi düşünebilirsiniz) ve sonuçlar üzerinde iterasyon yapabiliriz. jQuery UI'nin otomatik tamamlama kontrolü, istediğiniz her tür veriyi döndürmenize olanak tanır ancak en az bir değer anahtarı gerektirir. Değeri, adın düzgün biçimlendirilmiş bir sürümü olarak ayarladık. Ayrıca tüm kişiyi de döndürüyoruz. Nedenini birazdan göreceksiniz. İlk olarak, otomatik tamamlamanın işleyişini gösteren bir ekran görüntüsü. jQuery kullanıcı arayüzü için Vader temasını kullanıyoruz.

Bu, IndexedDB eşleşmelerimizin sonuçlarını otomatik tamamlamaya döndürmek için tek başına yeterlidir. Ancak bir maç seçildiğinde maçın ayrıntılı görünümünü de göstermek istiyoruz. Otomatik tamamlama özelliğini oluştururken daha önceki Handlebars şablonunu kullanan bir seçim işleyici belirttik.