ربط عناصر واجهة المستخدم باستخدام IndexedDB

ريموند كامدن
ريموند كامدن

مقدمة

تعتبر IndexedDB طريقة فعالة لتخزين البيانات من جهة العميل. أنصحك بقراءة أدلة MDN التعليمية حول هذا الموضوع إذا لم يسبق لك الاطّلاع عليها. تفترض هذه المقالة بعض المعرفة الأساسية بواجهات برمجة التطبيقات والميزات. حتى إذا لم تكن قد اطلعت على IndexedDB من قبل، نأمل أن يمنحك العرض التوضيحي في هذه المقالة فكرة عما يمكن فعله باستخدام هذه القاعدة.

العرض التوضيحي الذي نقدمه هو إثبات بسيط لمفهوم تطبيق الشبكة الداخلية لإحدى الشركات. سيسمح التطبيق للموظفين بالبحث عن موظفين آخرين. من أجل توفير تجربة أسرع وأكثر سرعة، يتم نسخ قاعدة بيانات الموظفين إلى جهاز العميل وتخزينها باستخدام IndexedDB. يوفر العرض التوضيحي ببساطة بحثًا بنمط الإكمال التلقائي وعرضًا لسجل موظف واحد، ولكن الجميل هو أنه بمجرد توفر هذه البيانات للعميل، يمكننا استخدامها بعدة طرق أخرى أيضًا. وفيما يلي مخطط أساسي لما يجب أن يفعله تطبيقنا.

  1. علينا إعداد وتهيئة مثيل لقاعدة البيانات المفهرسة. يُعد ذلك في الغالب أمرًا سهلاً، ولكن جعله يعمل على كل من Chrome وFirefox ثبت أنه أمر معقد بعض الشيء.
  2. نحتاج إلى معرفة ما إذا كانت لدينا أي بيانات، وإذا لم يكن الأمر كذلك، عليك تنزيلها. عادةً ما يتم ذلك عبر استدعاءات AJAX. في عرضنا التوضيحي، أنشأنا فئة بسيطة من الخدمات لإنشاء بيانات مزيفة بسرعة. سيحتاج التطبيق إلى التعرّف على وقت إنشاء هذه البيانات ومنع المستخدم من استخدام البيانات حتى ذلك الحين. ولا يتم تنفيذ هذه العملية إلّا لمرّة واحدة. في المرة التالية التي يشغِّل فيها المستخدم التطبيق، لن يحتاج إلى تنفيذ هذه العملية. سيتعامل الإصدار التجريبي الأكثر تقدمًا مع عمليات المزامنة بين العميل والخادم، لكن هذا العرض التوضيحي يركز بشكل أكبر على جوانب واجهة المستخدم.
  3. عندما يكون التطبيق جاهزًا، يمكننا استخدام عنصر تحكم الإكمال التلقائي في واجهة مستخدم jQuery للمزامنة مع قاعدة البيانات المفهرسة. بينما يسمح عنصر التحكم في الإكمال التلقائي بإنشاء قوائم ومصفوفات أساسية من البيانات، فإنه يحتوي على واجهة برمجة تطبيقات للسماح بأي مصدر بيانات. سوف نوضح كيف يمكننا استخدام هذا للاتصال ببيانات IndexedDB.

البدء

لدينا عدة أجزاء في هذا العرض التوضيحي، لذا لنبدأ الأمر ببساطة، لنلقِ نظرة على جزء HTML.

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

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

ليس كثيرًا، أليس كذلك؟ هناك ثلاثة جوانب رئيسية لواجهة المستخدم هذه تهمّنا. الأول هو الحقل "name" الذي سيتم استخدامه للإكمال التلقائي. يتم تحميل الصفحة بشكل معطل، وسيتم تفعيلها لاحقًا من خلال JavaScript. ويتم استخدام النطاق المجاور له أثناء إنشاء المحتوى الأساسي لتقديم تحديثات للمستخدم. أخيرًا، سيتم استخدام القسم div الذي يحتوي على id displayEmployee عند اختيار موظف من الاقتراح التلقائي.

لنلقِ الآن نظرة على لغة JavaScript. هناك الكثير من الأمور التي يجب استيعابها هنا، لذا سنأخذها خطوة بخطوة. سيكون الرمز الكامل متاحًا في النهاية بحيث يمكنك مشاهدته بالكامل.

أولاً، هناك بعض المشاكل المتعلقة بالبادئات والتي يجب أن نتعامل معها من بين المتصفحات التي تتيح استخدام 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 لعرض تفاصيل الموظف. لا يتم استخدام ذلك حتى وقت لاحق، ولكن يمكننا المضي قدمًا وتجميع القالب الآن وإفساحه الطريق. تم إعداد كتلة نص برمجي كنوع معروف من خلال "أشرطة المقبض". إنها ليست مهمة جدًا، ولكنها تسهّل عرض HTML الديناميكي.

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

ثم يتم تجميع ذلك مرة أخرى في JavaScript على النحو التالي:

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

لنبدأ الآن العمل باستخدام قاعدة البيانات المفهرسة. أولاً - نفتحه.

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

يتيح لنا فتح اتصال بقاعدة بيانات IndexedDB إمكانية الوصول لقراءة البيانات وكتابتها، ولكن قبل أن نفعل ذلك، يجب أن نتأكد من أن لدينا objectStore. يشبه objectStore جدول قاعدة البيانات. قد تحتوي إحدى قواعد البيانات المفهرسة على العديد من متاجر الكائنات، حيث يحتوي كل منها على مجموعة من العناصر ذات الصلة. العرض التوضيحي الخاص بنا بسيط ويحتاج إلى objectStore واحد فقط وهو ما نسميه "employee". عند فتح قاعدة البيانات المفهرسة لأول مرة، أو عند تغيير الإصدار في الرمز، يتم تشغيل حدث 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، وهو مصفوفة من مخازن العناصر، لمعرفة ما إذا كانت تحتوي على موظف. وإذا لم يكُن الأمر كذلك، يمكننا إجراء ذلك. ويُعدّ استدعاء createIndex مهمًا. يجب أن نخبر IndexedDB بالطرق، خارج المفاتيح، التي سنستخدمها لاسترداد البيانات. سنستخدم مفتاحًا يسمى مفتاح البحث. يتم شرح هذا الأمر قليلاً.

وسيتمّ تشغيل الحدث onungradeneeded تلقائيًا في المرة الأولى التي نشغِّل فيها النص البرمجي. يتم تشغيل معالج "onsuccess" بعد تنفيذه أو تخطّيه في عمليات التشغيل المستقبلية. لقد تم تحديد معالج أخطاء بسيط (وقبيح)، ثم نسميه handleSeed.

قبل أن ننتقل، دعنا نراجع سريعًا ما يجري هنا. نفتح قاعدة البيانات. نتحقّق مما إذا كان متجر العناصر موجودًا لدينا. وإذا لم يكن كذلك، فإننا ننشئه. أخيرًا، نستدعي دالة تسمىhandSeed. لننتقل الآن إلى جزء بذرة البيانات في العرض التوضيحي.

إعطائي بعض البيانات!

كما هو مذكور في مقدمة هذه المقالة، يعيد هذا العرض التوضيحي إنشاء تطبيق بنمط شبكة إنترانت ويحتاج إلى تخزين نسخة من جميع الموظفين المعروفين. وعادةً ما يتضمن ذلك إنشاء واجهة برمجة تطبيقات تستند إلى الخادم، والتي يمكنها عرض عدد الموظفين وتوفير طريقة لنا لاسترداد دفعات السجلات. يمكنك أن تتخيل خدمة بسيطة تدعم عدد بدايات وتعرض 100 شخص في المرة الواحدة. قد يتم تنفيذ هذا الإجراء بشكل غير متزامن في الخلفية أثناء إيقاف المستخدم لتنفيذ إجراءات أخرى.

بالنسبة إلى العرض التوضيحي، نقوم بشيء بسيط. يتضح لنا عدد العناصر، إن وجدت، في قاعدة البيانات المفهرسة. إذا كان يقلّ عن رقم معيّن، سننشئ مستخدمين مزيّفين. وإلا سنعتبر انتهينا من الجزء الأساسي ويمكننا تمكين جزء الإكمال التلقائي من العرض التوضيحي. لنلقي نظرة علىhandSeed.

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

احصل على متجر كائنات الموظف.

count()

شغِّل واجهة برمجة تطبيقات العد (يمكنك تخمينها) ويُجري عملية عدّ.

onsuccess = function(e) {

وعند الانتهاء - نفِّذ معاودة الاتصال هذه. داخل رد الاتصال، يمكننا الحصول على قيمة النتيجة وهي عدد الكائنات. وإذا كان العدد صفرًا، نبدأ عملية المحتوى الأساسي.

نستخدم قسم الحالة المذكور سابقًا لمنح المستخدم رسالة تفيد بأنّنا سنبدأ في الحصول على البيانات. نظرًا للطبيعة غير المتزامنة لقاعدة البيانات المفهرسة، فقد أعددنا متغيرًا بسيطًا تم إجراؤه من أجل تتبع الإضافات. نحن ندور فوق الأشخاص المزيفين ونضيفهم. يتوفر مصدر هذه الدالة في عملية التنزيل، ولكنها تعرض كائنًا يبدو كالتالي:

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

في حد ذاته، هذا يكفي لتعريف الشخص. غير أن لدينا متطلبات خاصة لكي نتمكن من البحث في البيانات. لا توفر IndexedDB طريقة للبحث عن العناصر بطريقة غير حساسة لحالة الأحرف. لذلك، ننشئ نسخة من حقل اسم العائلة إلى خاصية جديدة، وهي مفتاح بحث. إذا كنت تتذكر، فهذا هو المفتاح الذي قلناه يجب إنشاؤه كفهرس لبياناتنا.

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

ونظرًا لأن هذا تعديل خاص بالعميل، يتم تنفيذه هنا على عكس خادم الخلفية (أو في حالتنا، الخادم الخلفي التخيّلي).

لإجراء إضافات قاعدة البيانات بطريقة فاعلة، يجب عليك إعادة استخدام العملية لجميع عمليات الكتابة المجمّعة. إذا أنشأت معاملة جديدة لكل عملية كتابة، قد يتسبب المتصفح في حدوث عملية كتابة على القرص لكل عملية، مما سيضعف أدائك عند إضافة العديد من العناصر (فكر في "دقيقة واحدة لكتابة 1000 عنصر" سيئًا).

بعد الانتهاء من إنشاء المحتوى الأساسي، يتم تنشيط الجزء التالي من تطبيقنا - setupAutoComplete.

إنشاء الإكمال التلقائي

والآن، لننتقل إلى الجزء الممتع: إنشاء المكوّن الإضافي Autocomplete لواجهة المستخدم في jQuery. كما هو الحال مع معظم واجهة مستخدم jQuery، نبدأ بعنصر 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 تحديد موقع مصدر يمكن تخصيصه لتلبية أي احتياجات محتملة، بما في ذلك بيانات قاعدة البيانات المفهرسة. تزوّدك واجهة برمجة التطبيقات بالطلب (ما تتم كتابته في حقل النموذج بشكل أساسي) والرد على معاودة الاتصال. أنت مسؤول عن إرسال مجموعة من النتائج مرة أخرى إلى عملية معاودة الاتصال هذه.

أول شيء نفعله هو إخفاء DisplayEmployee div. ويُستخدم هذا لعرض موظف فردي وإذا تم تحميله في السابق، لمحوه. الآن يمكننا بدء البحث.

نبدأ بإنشاء معاملة للقراءة فقط، ومصفوفة تسمى النتيجة، ومعالج oncomplete الذي يمرر النتيجة إلى عنصر تحكم الإكمال التلقائي.

للعثور على العناصر التي تطابق البيانات التي ذكرناها، لنستفيد من نصيحة قدّمها مستخدم StackOverflow "فونغ وان تشاو": نستخدم نطاق فهرس يعتمد على الإدخال كحدّ أدنى للحقل والإدخال بالإضافة إلى الحرف z كحدود النطاق الأعلى. لاحظ أيضًا أننا نستخدم أحرف لاتينية صغيرة لمطابقة البيانات الصغيرة التي أدخلناها.

بمجرد الانتهاء - يمكننا فتح مؤشر (فكر في الأمر مثل تشغيل استعلام قاعدة بيانات) والتكرار التحسيني فوق النتائج. يسمح لك عنصر تحكم الإكمال التلقائي في واجهة مستخدم jQuery بإرجاع أي نوع من البيانات تريده، ولكنه يتطلب مفتاح قيمة على الأقل. نُعيِّن القيمة على نسخة منسقة بشكل جيد من الاسم. نُعيد أيضًا الشخص بأكمله. ستعرف السبب خلال ثوانٍ. أولاً، إليك لقطة شاشة للإكمال التلقائي وهو قيد التنفيذ. نحن نستخدم مظهر Vader لواجهة مستخدم jQuery.

ويكفي هذا وحده لعرض نتائج مطابقات IndexedDB إلى الإكمال التلقائي. لكننا نريد أيضًا إتاحة إظهار عرض تفصيلي للمطابقة عند اختيار أحدها. حددنا معالج تحديد عند إنشاء الإكمال التلقائي الذي يستخدم نموذج "الأشرطة الجانبية" في وقت سابق.