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

مقدمة

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

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

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

البدء

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

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

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

ليس كثيرًا، أليس كذلك؟ هناك ثلاثة جوانب رئيسية لهذه الواجهة التي نهتم بها. الحقل الأول هو "الاسم" الذي سيتم استخدامه للإكمال التلقائي. يتم تحميله بشكل غير مفعّل وسيتم تفعيله لاحقًا من خلال JavaScript. ويتم استخدام النطاق بجانبه أثناء عملية الإعداد الأولية لتقديم آخر المعلومات للمستخدم. أخيرًا، سيتم استخدام div الذي يحمل المعرّف 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 لعرض تفاصيل الموظف. لا يتم استخدام هذا الإجراء إلا لاحقًا، ولكن يمكننا المتابعة وتجميع النموذج الآن وإزالته من الطريق. لدينا كتلة نص برمجي تم إعدادها كنوع معرَّف في Handlebars. هذه الطريقة ليست مُبهرة جدًا، ولكنها تسهِّل عرض HTML الديناميكي.

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

ويتم بعد ذلك تجميع هذا الرمز في JavaScript على النحو التالي:

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

لنبدأ الآن العمل مع IndexedDB. أولاً، نفتح الملف.

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

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

Gimme Some Data!

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

في العرض التوضيحي، سننفّذ عملية بسيطة. نرى عدد العناصر، إن توفّرت، في 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");

احصل على مساحة تخزين العناصر للموظف.

count()

يمكنك تشغيل واجهة برمجة التطبيقات count التي تُجري عملية احتساب.

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

وبما أنّ هذا تعديل خاص بالعميل، يتم إجراؤه هنا بدلاً من الخادم في الخلفية (أو في حالتنا، الخادم الافتراضي في الخلفية).

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

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

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

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

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

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

للعثور على العناصر التي تتطابق مع الإدخال، لنستخدِم نصيحة من مستخدم StackOverflow، وهو Fong-Wan Chau: نستخدم نطاقًا للفهرسة يستند إلى الإدخال كحدّ أدنى وإلى الإدخال بالإضافة إلى الحرف z كحدّ أقصى للنطاق. يُرجى العلم أيضًا أنّنا نكتب العبارة بأحرف صغيرة لمطابقة البيانات التي أدخلناها بأحرف صغيرة.

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

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