مقدمة
IndexedDB هي طريقة فعّالة لتخزين البيانات من جهة العميل. إذا لم تكن قد اطّلعت عليه بعد، أنصحك بقراءة البرامج التعليمية المفيدة على شبكة مطوّري البرامج من Mozilla حول هذا الموضوع. تفترض هذه المقالة بعض المعرفة الأساسية بواجهات برمجة التطبيقات والميزات. حتى إذا لم يسبق لك استخدام IndexedDB، نأمل أن يمنحك العرض التوضيحي في هذه المقالة فكرة عمّا يمكن تنفيذه من خلاله.
العرض التوضيحي هو تطبيق بسيط لإثبات جدوى الفكرة لشبكة إنترانت لشركة. سيسمح التطبيق للموظفين بالبحث عن موظفين آخرين. لتقديم تجربة أسرع وأكثر سلاسة، يتم نسخ قاعدة بيانات الموظفين إلى جهاز العميل وتخزينها باستخدام IndexedDB. يوفّر العرض التوضيحي ببساطة بحثًا بأسلوب الإكمال التلقائي وعرضًا لسجلّ موظف واحد، ولكن الميزة الرائعة هي أنّه بمجرد توفّر هذه البيانات على العميل، يمكننا استخدامها بطرق أخرى متعددة أيضًا. في ما يلي مخطّط أساسي لما يحتاج تطبيقنا إلى تنفيذه.
- علينا إعداد مثيل IndexedDB وتهيئته. هذا الأمر بسيط في الغالب، ولكن جعلها تعمل في كل من Chrome وFirefox يثبت أنه صعب بعض الشيء.
- نحتاج إلى معرفة ما إذا كانت لدينا أي بيانات، وإذا لم تكن كذلك، فقم بتنزيلها. عادةً ما يتم ذلك من خلال طلبات AJAX. في هذا العرض التوضيحي، أنشأنا فئة خدمات بسيطة لإنشاء بيانات مزيّفة بسرعة. سيحتاج التطبيق إلى التعرّف على وقت إنشاء هذه البيانات ومنع المستخدم من استخدامها حتى ذلك الحين. ولن يتم إجراء هذه العملية إلّا لمرّة واحدة. في المرة التالية التي يشغّل فيها المستخدم التطبيق، لن يحتاج إلى تنفيذ هذه العملية. يمكن أن يتعامل الإصدار التجريبي الأكثر تقدّمًا مع عمليات المزامنة بين العميل والخادم، ولكن هذا الإصدار التجريبي يركّز أكثر على جوانب واجهة المستخدم.
- عندما يكون التطبيق جاهزًا، يمكننا استخدام عنصر التحكّم في الإكمال التلقائي في 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>
ليس كثيرًا، أليس كذلك؟ هناك ثلاثة جوانب رئيسية لواجهة المستخدم هذه تهمّنا. أولاً، الحقل "name" الذي سيتم استخدامه للإكمال التلقائي. يتم تحميله في وضع الإيقاف وسيتم تفعيله لاحقًا من خلال JavaScript. يتم استخدام العنصر span بجانبه أثناء عملية التعبئة الأولية لتقديم تحديثات للمستخدم. أخيرًا، سيتم استخدام علامة 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:
$(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 واحدة على العديد من مخازن العناصر، ويحتوي كل منها على مجموعة من العناصر ذات الصلة. العرض التوضيحي بسيط ولا يحتاج إلا إلى مخزن عناصر واحد نسميه "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. لننتقل الآن إلى الجزء الخاص بتزويد البيانات في العرض التوضيحي.
أريد بعض البيانات.
كما ذكرنا في مقدمة هذه المقالة، يعيد هذا العرض التوضيحي إنشاء تطبيق على نمط الشبكة الداخلية يحتاج إلى تخزين نسخة من جميع الموظفين المعروفين. عادةً ما يتضمن ذلك إنشاء واجهة برمجة تطبيقات مستندة إلى الخادم يمكنها عرض عدد الموظفين وتوفير طريقة لنا لاسترداد دفعات من السجلات. يمكنك تخيُّل خدمة بسيطة تتيح عددًا أوليًا وتعرض 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 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();
بما أنّ هذا التعديل خاص بالعميل، يتم إجراؤه هنا بدلاً من إجرائه على خادم الواجهة الخلفية (أو في حالتنا، خادم الواجهة الخلفية الوهمي).
لإجراء الإضافات إلى قاعدة البيانات بطريقة فعّالة، يجب إعادة استخدام المعاملة لجميع عمليات الكتابة المجمّعة. إذا أنشأت معاملة جديدة لكل عملية كتابة، قد يتسبب المتصفّح في كتابة على القرص لكل معاملة، وسيؤدي ذلك إلى تراجع الأداء بشكل كبير عند إضافة الكثير من العناصر (مثلاً، "دقيقة واحدة لكتابة 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 UI تحديد خاصية مصدر يمكن تخصيصها لتلبية أي احتياجات محتملة، حتى بيانات IndexedDB. توفّر لك واجهة برمجة التطبيقات الطلب (ما تم كتابته في حقل النموذج) ودالة رد نداء للاستجابة. أنت مسؤول عن إرسال مصفوفة من النتائج إلى دالة الاستدعاء هذه.
أول شيء نفعله هو إخفاء div displayEmployee. يتم استخدام هذا لعرض موظف فردي وإذا تم تحميل موظف مسبقًا، لمسحه. يمكننا الآن بدء البحث.
نبدأ بإنشاء معاملة للقراءة فقط، ومصفوفة تسمى النتيجة، ومعالج oncomplete الذي يمرر النتيجة ببساطة إلى عنصر التحكم في الإكمال التلقائي.
للعثور على العناصر التي تتطابق مع الإدخال، لنستفِد من نصيحة قدّمها مستخدم StackOverflow "فونغ وان تشاو": نستخدم نطاق فهرس استنادًا إلى الإدخال كحدّ أدنى للنطاق والإدخال بالإضافة إلى الحرف z كحدّ أقصى للنطاق. لاحظ أيضًا أننا نستخدم الأحرف الصغيرة في المصطلح لمطابقة بيانات الأحرف الصغيرة التي أدخلناها.
بعد الانتهاء، يمكننا فتح مؤشر (مثل تشغيل طلب بحث في قاعدة البيانات) وتكرار النتائج. يتيح لك عنصر التحكّم في الإكمال التلقائي في jQuery UI عرض أي نوع من البيانات التي تريدها، ولكن يتطلب مفتاح قيمة كحد أدنى. نضبط القيمة على نسخة منسَّقة بشكل جيد من الاسم. ونعيد أيضًا الشخص بالكامل. ستعرف السبب بعد قليل. أولاً، إليك لقطة شاشة للإكمال التلقائي أثناء العمل. نحن نستخدم مظهر Vader لواجهة مستخدم jQuery.
هذا وحده يكفي لإرجاع نتائج مطابقات IndexedDB إلى الإكمال التلقائي. لكننا نريد أيضًا إتاحة عرض تفصيلي للمباراة عند اختيارها. لقد حدّدنا معالج اختيار عند إنشاء ميزة الإكمال التلقائي التي تستخدم نموذج Handlebars من قبل.