عناصر میانای کاربر Databinding با IndexedDB

مقدمه

IndexedDB روشی قدرتمند برای ذخیره داده‌ها در سمت کارخواه است. اگر هنوز آن را بررسی نکرده‌اید، توصیه می‌کنم آموزش‌های مفید MDN را درباره این موضوع بخوانید. این مقاله فرض می‌کند که شما با برخی‌از اصول اولیه میاناهای برنامه‌سازی کاربردی و ویژگی‌ها آشنا هستید. حتی اگر قبلاً IndexedDB را ندیده باشید، امیدواریم نسخه نمایشی این مقاله ایده‌ای از آنچه می‌توان با آن انجام داد به شما بدهد.

نسخه نمایشی ما یک برنامه اثبات مفهوم ساده اینترانت برای یک شرکت است. این برنامه به کارمندان اجازه می‌دهد کارمندان دیگر را جستجو کنند. برای ارائه تجربه‌ای سریع‌تر و پویاتر، پایگاه داده کارمندان در دستگاه مشتری کپی می‌شود و بااستفاده از IndexedDB ذخیره می‌شود. این نسخه نمایشی به‌سادگی یک جستجوی سبک تکمیل خودکار و نمایش یک سابقه کارمند را ارائه می‌دهد، اما نکته خوب این است که وقتی این داده‌ها در سرویس‌گیرنده دردسترس قرار گرفت، می‌توانیم از آن به روش‌های متعدد دیگری نیز استفاده کنیم. در اینجا طرح کلی پایه‌ای از آنچه برنامه ما باید انجام دهد آورده شده است.

  1. باید یک نمونه از IndexedDB را راه‌اندازی و مقداردهی اولیه کنیم. این کار در بیشتر موارد ساده است، اما اجرای آن در Chrome و Firefox کمی دشوار است.
  2. باید ببینیم داده‌ای داریم یا نه، و اگر نداریم، آن را بارگیری کنیم. معمولاً این کار ازطریق تماس‌های AJAX انجام می‌شود. برای نسخه نمایشی‌مان، کلاس کاربردی ساده‌ای ایجاد کرده‌ایم تا داده‌های جعلی را به‌سرعت تولید کنیم. برنامه باید تشخیص دهد که چه زمانی این داده‌ها را ایجاد می‌کند و تا آن زمان مانع استفاده کاربر از داده‌ها شود. این عملیات یک‌باره است. دفعه بعد که کاربر برنامه را اجرا می‌کند، نیازی به طی کردن این فرایند نخواهد بود. یک نسخه نمایشی پیشرفته‌تر عملیات همگام‌سازی بین کارخواه و سرور را مدیریت می‌کند، اما این نسخه نمایشی بیشتر بر جنبه‌های میانای کاربر تمرکز دارد.
  3. وقتی برنامه آماده شد، می‌توانیم از کنترل «تکمیل خودکار» jQuery UI برای همگام‌سازی با IndexedDB استفاده کنیم. درحالی‌که کنترل «تکمیل خودکار» امکان استفاده از فهرست‌های پایه و آرایه‌های داده را فراهم می‌کند، دارای یک میانای برنامه‌سازی کاربردی (API) است که امکان استفاده از هر منبع داده‌ای را فراهم می‌کند. نشان خواهیم داد که چگونه می‌توانیم از این برای اتصال به داده‌های IndexedDB خود استفاده کنیم.

درحال شروع کردن

این نسخه نمایشی چند بخش دارد، بنابراین برای شروع ساده، بیایید به بخش HTML نگاه کنیم.

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

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

زیاد نیست، درست است؟ این میانای کاربری سه جنبه اصلی دارد که برای ما مهم است. اولین فیلد «نام» است که برای تکمیل خودکار استفاده خواهد شد. غیرفعال بار می‌شود و بعداً ازطریق جاوا اسکریپت فعال خواهد شد. از بازه زمانی کنار آن درطول بذر اولیه برای ارائه به‌روزرسانی‌ها به کاربر استفاده می‌شود. درنهایت، وقتی کارمندی را از پیشنهاد خودکار انتخاب می‌کنید، از div با شناسه displayEmployee استفاده خواهد شد.

اکنون بیایید نگاهی به جاوا اسکریپت بیندازیم. مطالب زیادی برای هضم کردن وجود دارد، بنابراین قدم‌به‌قدم پیش می‌رویم. کد کامل در پایان دردسترس خواهد بود تا بتوانید آن را به‌طور کامل ببینید.

اول از همه - برخی مشکلات پیشوند وجود دارد که باید در میان مرورگرهایی که از IndexedDB پشتیبانی می‌کنند نگران آن‌ها باشیم. در اینجا کدی از اسناد Mozilla وجود دارد که برای ارائه نام‌های مستعار ساده برای مؤلفه‌های اصلی IndexedDB که برنامه ما نیاز دارد، اصلاح شده است.

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

سپس، چند متغیر سراسری که در سراسر نسخه نمایشی استفاده خواهیم کرد:

var db;
var template;

اکنون با بلوک آماده سند jQuery شروع می‌کنیم:

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

نسخه نمایشی ما از Handlebars.js برای نمایش جزئیات کارمند استفاده می‌کند. این مورد تا بعداً استفاده نمی‌شود، اما می‌توانیم پیش برویم و الگوی خود را اکنون کامپایل کنیم و آن را از سر راه برداریم. یک بلوک اسکریپت داریم که به‌عنوان نوعی شناخته‌شده برای Handlebars راه‌اندازی شده است. خیلی جالب نیست، اما نمایش HTML پویا را آسان‌تر می‌کند.

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

سپس این کد دوباره در جاوا اسکریپت ما به این صورت کامپایل می‌شود:

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

اکنون بیایید کار با IndexedDB را شروع کنیم. اول - ما آن را باز می‌کنیم.

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

باز کردن اتصال به IndexedDB به ما امکان دسترسی به خواندن و نوشتن داده‌ها را می‌دهد، اما قبل‌از انجام این کار، باید مطمئن شویم که objectStore داریم. objectStore مانند جدول پایگاه داده است. یک IndexedDB ممکن است چندین objectStore داشته باشد که هرکدام مجموعه‌ای از اشیاء مرتبط را در خود نگه می‌دارد. نسخه نمایشی ما ساده است و فقط به یک objectStore نیاز دارد که آن را «employee» می‌نامیم. وقتی indexedDB برای اولین‌بار باز می‌شود یا وقتی نسخه را در کد تغییر می‌دهید، رویداد onupgradeneeded اجرا می‌شود. می‌توانیم از این برای راه‌اندازی 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();
};

در بلوک مدیر رویداد onupgradeneeded، objectStoreNames (آرایه‌ای از فروشگاه‌های شیء) را بررسی می‌کنیم تا ببینیم آیا حاوی employee است یا نه. اگر این‌طور نباشد، به‌سادگی آن را وادار به انجام این کار می‌کنیم. فراخوانی createIndex مهم است. باید به IndexedDB بگوییم که از چه روش‌هایی، به‌جز کلیدها، برای بازیابی داده‌ها استفاده خواهیم کرد. از کلیدی به‌نام searchkey استفاده خواهیم کرد. این موضوع کمی بعد توضیح داده می‌شود.

رویداد onungradeneeded اولین باری که دستورات را اجرا می‌کنیم به‌طور خودکار اجرا خواهد شد. پس‌از اجرا شدن یا رد شدن در اجراهای بعدی، گرداننده onsuccess اجرا می‌شود. یک کنترل‌کننده خطای ساده (و زشت) تعریف کرده‌ایم و سپس handleSeed را فراخوانی می‌کنیم.

پس قبل‌از اینکه ادامه دهیم، بیایید سریعاً آنچه را که در اینجا اتفاق می‌افتد مرور کنیم. پایگاه داده را باز می‌کنیم. بررسی می‌کنیم که آیا فروشگاه شیء ما وجود دارد یا نه. اگر وجود نداشته باشد، آن را ایجاد می‌کنیم. درنهایت، تابعی به‌نام handleSeed را فراخوانی می‌کنیم. اکنون بیایید به بخش کاشت داده در نسخه نمایشی‌مان توجه کنیم.

کمی داده به من بده!

همان‌طور که در مقدمه این مقاله ذکر شد، این نسخه نمایشی درحال بازسازی یک برنامه به سبک اینترانت است که باید نسخه‌ای از همه کارمندان شناخته‌شده را ذخیره کند. معمولاً این کار شامل ایجاد یک میانای برنامه‌سازی کاربردی مبتنی بر سرور است که می‌تواند تعداد کارمندان را برگرداند و راهی برای بازیابی دسته‌های سوابق برای ما فراهم کند. می‌توانید سرویس ساده‌ای را تصور کنید که از شمارش شروع پشتیبانی می‌کند و هربار ۱۰۰ نفر را برمی‌گرداند. این کار می‌تواند به‌صورت ناهمگام در پس‌زمینه اجرا شود، درحالی‌که کاربر مشغول انجام کارهای دیگر است.

برای نمایش، کار ساده‌ای انجام می‌دهیم. می‌بینیم که چند شیء، درصورت وجود، در IndexedDB خود داریم. اگر کمتر از تعداد مشخصی باشد، به‌سادگی کاربران جعلی ایجاد می‌کنیم. درغیراین‌صورت، فرض می‌کنیم بخش بذر تمام شده است و می‌توانیم بخش تکمیل خودکار نسخه نمایشی را فعال کنیم. بیایید به 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();
    }
  };
}

خط اول کمی پیچیده است زیرا چندین عملیات زنجیره‌ای به یکدیگر داریم، بنابراین بیایید آن را تجزیه کنیم:

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

با این کار تراکنش جدید فقط خواندنی ایجاد می‌شود. همه عملیات داده با IndexedDB به نوعی تراکنش نیاز دارد.

objectStore("employee");

Get the employee object store.

count()

اجرای API شمارش - که همان‌طور که حدس می‌زنید - شمارش را انجام می‌دهد.

onsuccess = function(e) {

و وقتی انجام شد - این فراخوانی برگشتی را اجرا کنید. درون فراخوانی برگشتی می‌توانیم مقدار نتیجه را که تعداد اشیاء است دریافت کنیم. اگر تعداد صفر بود، فرایند بذرپاشی را شروع می‌کنیم.

از آن div وضعیت که قبلاً ذکر شد استفاده می‌کنیم تا به کاربر پیامی بدهیم که می‌خواهیم دریافت داده‌ها را شروع کنیم. به‌دلیل ماهیت ناهمگام IndexedDB، متغیر ساده‌ای به‌نام done تنظیم کرده‌ایم که موارد اضافه‌شده را ردیابی می‌کند. ما افراد جعلی را تکرار و درج می‌کنیم. منبع آن تابع در بارگیری دردسترس است، اما شیئی را برمی‌گرداند که شبیه این است:

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

این به‌تنهایی برای تعریف یک شخص کافی است. اما برای اینکه بتوانیم داده‌هایمان را جستجو کنیم، الزامی خاص داریم. IndexedDB روشی برای جستجوی موارد به‌صورت غیرحساس به حروف بزرگ و کوچک ارائه نمی‌دهد. بنابراین، نسخه‌ای از فیلد lastname را در ویژگی جدیدی به‌نام searchkey ایجاد می‌کنیم. اگر به‌خاطر داشته باشید، این همان کلیدی است که گفتیم باید به‌عنوان نمایه برای داده‌های ما ایجاد شود.

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

ازآنجایی‌که این یک اصلاح مختص کارخواه است، در اینجا انجام می‌شود، نه در سرور زیرینه (یا در مورد ما، سرور زیرینه فرضی).

برای انجام افزودن‌های پایگاه داده به‌صورت کارآمد، باید از تراکنش برای همه نوشتن‌های دسته‌ای مجدداً استفاده کنید. اگر برای هر نوشتن تراکنش جدیدی ایجاد کنید، ممکن است مرورگر برای هر تراکنش یک نوشتن دیسک ایجاد کند و این امر باعث می‌شود عملکرد شما هنگام افزودن موارد زیاد بسیار بد شود (مثلاً «۱ دقیقه برای نوشتن ۱۰۰۰ شیء» که خیلی بد است).

وقتی بذر انجام شد، بخش بعدی برنامه ما فعال می‌شود - setupAutoComplete.

درحال ایجاد تکمیل خودکار

اکنون نوبت بخش جالب است - اتصال به افزونه jQuery UI تکمیل خودکار. مانند اکثر jQuery UI، ما با یک عنصر HTML پایه شروع می‌کنیم و آن را با فراخوانی یک روش سازنده روی آن بهبود می‌بخشیم. ما کل فرایند را در تابعی به‌نام setupAutoComplete خلاصه کرده‌ایم. بیایید اکنون به آن کد نگاه کنیم.

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

}

پیچیده‌ترین بخش این کد ایجاد دارایی منبع است. کنترل «تکمیل خودکار» jQuery UI به شما امکان می‌دهد ویژگی منبعی را تعریف کنید که می‌تواند برای برآورده کردن هر نیاز احتمالی سفارشی‌سازی شود - حتی داده‌های IndexedDB ما. میانای برنامه‌سازی کاربردی درخواست (اساساً آنچه در فیلد فرم تایپ شده است) و فراخوانی پاسخ را به شما ارائه می‌دهد. شما مسئول ارسال آرایه‌ای از نتایج به آن فراخوان هستید.

اولین کاری که انجام می‌دهیم پنهان کردن div مربوط به displayEmployee است. این div برای نمایش یک کارمند به‌صورت جداگانه استفاده می‌شود و اگر قبلاً بارگیری شده باشد، آن را پاک می‌کند. اکنون می‌توانیم جستجو را شروع کنیم.

با ایجاد یک تراکنش فقط خواندنی، آرایه‌ای به نام result، و یک کنترل‌کننده oncomplete که به‌سادگی نتیجه را به کنترل تکمیل خودکار ارسال می‌کند، شروع می‌کنیم.

برای پیدا کردن مواردی که با ورودی ما مطابقت دارند، از نکته‌ای که Fong-Wan Chau، کاربر StackOverflow، ارائه کرده است استفاده می‌کنیم: از محدوده نمایه‌ای براساس ورودی به‌عنوان مرز پایینی و ورودی به‌علاوه حرف z به‌عنوان مرز بالایی استفاده می‌کنیم. همچنین توجه داشته باشید که عبارت را با حروف کوچک می‌نویسیم تا با داده‌های حروف کوچکی که وارد کردیم مطابقت داشته باشد.

پس‌از انجام این کار، می‌توانیم مکان‌نمایی را باز کنیم (آن را مانند اجرای یک پُرسمان پایگاه داده درنظر بگیرید) و نتایج را تکرار کنیم. کنترل تکمیل خودکار jQuery UI به شما امکان می‌دهد هر نوع داده‌ای را که می‌خواهید برگردانید، اما حداقل به یک کلید مقدار نیاز دارد. مقدار را روی نسخه قالب‌بندی‌شده مناسبی از نام تنظیم می‌کنیم. همچنین کل شخص را برمی‌گردانیم. تا چند ثانیه دیگر دلیلش را خواهید دید. ابتدا، این نماگرفتی از تکمیل خودکار درحال اجرا است. ما از طرح زمینه Vader برای jQuery UI استفاده می‌کنیم.

این به‌تنهایی برای برگرداندن نتایج مطابقت‌های IndexedDB ما به تکمیل خودکار کافی است. اما می‌خواهیم از نمایش نمای جزئیات مسابقه درصورت انتخاب شدن نیز پشتیبانی کنیم. ما هنگام ایجاد تکمیل خودکار، یک کنترل‌کننده انتخاب مشخص کردیم که از الگوی Handlebars قبلی استفاده می‌کند.