تُعد الواجه البرمجية IndexedDB أو IDB اختصارًا منظومة قواعد بيانات كاملة مضمنة في المتصفح تساعدك على تخزين بيانات مترابطة معقدة لا تقتصر فيها أنواع البيانات على قيم بسيطة مثل النصوص واﻷعداد. إذ تستطيع تخزين مقاطع الفيديو والصور وتقريبًا أي شيء في نسخ منفصلة من القاعدة IndexedDB.
وتتيح لك هذه الواجهة البرمجية إنشاء قاعدة بيانات ومن ثم إنشاء مخازن كائنات object stores ضمن القاعدة. وتُعد مخازن الكائنات بمثابة بنى شبيهة بالجداول الموجودة في قواعد البيانات العلاقية relational databases، ويمكن لأي مخزن أن يضم مخازن كائنات أخرى.
وبالطبع تأتي هذه الميزات مع آثار لا بد منها، فالواجهة IndexedDB أكثر تعقيدًا من واجهة مخازن ويب Web Stores من ناحية الاستخدام. لهذا سنحاول في هذا المقال تقديم مثال يعرض جزءًا ضئيًلا جدًا من إمكانات هذه الواجهة، لكنه سيزوّدك باﻷساسيات التي تساعدك على الانطلاق.
تطبيق عملي: تخزين ملاحظات
سنبني تطبيق بسيط يسمح لك بتخزين ملاحظات في المتصفح ومراجعة هذه الملاحظات وحذفها متى شئت. وسيكون البناء خطوة خطوة نشرح فيها اﻷجزاء اﻷكثر أهمية من قاعدة البيانات IndexedDB. سيبدو شكل التطبيق عند انتهائه كالتالي:
تتكون كل ملاحظة من عنوان ومحتوى نصي يمكن تحرير أي منهما بشكل مستقل عن اﻵخر. وستجد ضمن شيفرة جافا سكريبت الخاصة بالتطبيق تعليقات كافية لشرح كل خطوة بالتفصيل.
نقطة الانطلاق
-
انسخ بداية الملفات
index.html
وstyle.css
وindex-start.js
إلى مجلد جديد تُنشئه على حاسوبك. -
الق نظرة في البداية على تلك الملفات، وسترى أن ملف HTML يُعرّف موقع ويب له ترويسة وتذييل ومنطقة محتوى رئيسي تُعرض فيه الملاحظات، إضافة إلى نموذج ﻹدخالها في قاعدة البيانات. يقدّم ملف CSS مجموعة من قواعد التنسيق لتوضيح ما يجري، بينما يضم ملف جافا سكريبت تصريحًا عن خمسة ثوابت تضم مراجع إلى العنصر
<ul>
وستُعرض الملاحظات على شكل عنوان ونص ضمن عنصري إدخال<input>
كما ننشئ مرجعًا إلى النموذج<form>
بحد ذاته، ومرجعًا إلى زر<button>
. -
غير اسم ملف جافا سكريبت إلى
index.js
.
تهيئة قاعدة البيانات
لنلق نظرة اﻵن على ما يتوجب علبنا فعله بداية لإعداد قاعدة البيانات:
- أضف السطر التالي تحت التصريحات عن الثوابت:
// إنشاء نسخة عن كائن قاعدة البيانات let db;
نصرّح في هذا السطر عن متغير يُدعى db
لنستخدمه لاحقًا في تخزين الكائن الذي يمثل قاعدة البيانات. وطالما أننا نستخدمه في عدة أماكن لذلك صرحنا عنه كمتغير عام global لتسهيل اﻷمر.
- أضف تاليًا الشيفرة التالية:
// فتح قاعدة البيانات مما يؤدي إلى إنشائها إن لم تكن موجودة const openRequest = window.indexedDB.open("notes_db", 1);
يُنشئ هذا السطر طلبًا لفتح النسخة 1 من قاعدة بيانات تُدعى notes_db
، فإن لم تكن موجودة سوف ينشئها السطر الذي يليه. سترى هذا الشكل من الطلبات كثيرًا عند استخدام IndexedDB. وطالما أن العمليات على قواعد البيانات تحتاج وقتًا، فلا ينبغي ايقاف المتصفح ريثما ننتظر نتيجة الطلب، لهذا عمليات قواعد البيانات هي عمليات غير متزامنة لن تُنفّذ مباشرة، بل في فترة ما مستقبلًا وستُبلَّغ بتنفيذها. ولمعالجة اﻷمر في IndexedDB، ننشئ كائن طلب request object ندعوه مثلًا openRequest
، ويمكنك حينها استخدام معالج حدث لتنفيذ شيفرة مخصصة عند اكتمال هذا الطلب أو فشله، وهذا ما ستراه بعد قليل.
ملاحظة: إن رقم النسخة أمر مهم. فلو أردت تحديث قاعدة بياناتك (مثل تغيير هيكلية الجدول)، عليك تنفيذ شيفرتك مجددًا بعد زيادة رقم النسخة، وتحديد تخطيط مختلف ضمن معالج الحدث upgradeneeded
. لكننا لن نغطي تحديث قاعدة البيانات في هذا المقال.
- أضف اﻵن معالج الحدث التالي تحت الشيفرة السابقة:
// معالج خطأ يحدد حالة فشل الاتصال بقاعدة البيانات openRequest.addEventListener("error", () => console.error("Database failed to open"), ); // معالج نجاح يحدد نجاح فتح قاعدة البيانات openRequest.addEventListener("success", () => { console.log("Database opened successfully"); //db خزن قاعدة البيانات المفتوحة ضمن المتغير db = openRequest.result; //التي تعرض الملاحظات الموجودة في قاعدة البيانات displayData() نفّذ الدالة displayData(); });
يُنفَّذ معالج الحدث error
عندما يُبلغك النظام أن طلبك قد أخفق، مما يتيح لك التعامل مع المشكلة. وما فعلناه في مثالنا هو عرض رسالة خطأ في طرفية جافا سكريبت. ويُنفَّذ معالج الحدث success
عندما ينجح الطلب، بمعنى أن الاتصال إلى قاعدة البيانات تحقق، وأصبح الكائن الذي يمثّل قاعدة البيانات متاحًا ضمن الخاصية openRequest.result
وبالتالي إمكانية التعامل من خلاله مع قاعدة البيانات. نخزّن النتيجة ضمن المتغيّر db
وننفذ دالة تُدعى ()displayData
وظيفتها عرض البيانات الموجودة في قاعدة البيانات داخل العنصر <ul>
بمجرد انتهاء تحميل الصفحة، وسترى تعريف هذه الدالة لاحقًا.
-
في نهاية هذا القسم سنضيف معالج الحدث
upgradeneeded
الذي يُنفَّذ إن لم تكن قاعدة البيانات قد هُيئت مسبقًا أو عندما تكون قاعدة البيانات مفتوحة. لهذا أضف اﻷسطر التالية في نهاية شيفرتك منتبهًا إلى ضرورة استخدام رقم نسخة أعلى من رقم النسخة المخزنة في قاعدة البيانات عندما تريد تحديث القاعدة:
// هيئ جداول قاعدة البيانات إن لم تكن مهيأة مسبقًا openRequest.addEventListener("upgradeneeded", (e) => { // احصل على مرجع إلى قاعدة البيانات المفتوحة db = e.target.result; // أنشئ مخزن كائنات في قاعدة البيانات لتخزين الملاحظة مع مفتاح يزداد تلقائيًا ومخزن الكائنات مشابه للجدول في قاعدة البيانات العلاقية const objectStore = db.createObjectStore("notes_os", { keyPath: "id", autoIncrement: true, }); // حدد ما ستضمه عناصر البيانات ومخازن الكائنات objectStore.createIndex("title", "title", { unique: false }); objectStore.createIndex("body", "body", { unique: false }); console.log("Database setup complete"); });
نعرّف هنا التخطيط الذي نعتمده لقاعدة البيانات، أي نحدد مجموعة الأعمدة أو الحقول التي تضمها. وما فعلناه أننا حددنا مرجعًا في البداية إلى قاعدة البيانات الموجودة عبر الخاصية result
لمعالج الحدث e.target.result
الذي يمثّل كائن الطلب request
. وهذا الأمر مكافئ للسطر:
db = openRequest.result;
داخل معالج الحدث succes
، لكن لا بد هنا من تنفيذه بشكل مستقل لأن معالج الحدث upgradeneeded
سيُنفذ إن احتجنا إليه قبل المعالج success
، ولن يكون المتغير db
متاحًا ما لم نفعل ذلك.
نستخدم بعد ذلك التابع IDBDatabase.createObjectStore
ﻹنشاء مخزن كائن جديد ضمن قاعدة البيانات المفتوحة يُدعى notes_os
ويكافئ هذا المخزن جدولًا مفردًا في قواعد البيانات النمطية. كما خصصنا أيضًا حقلًا مفتاحيًا autoIncrement
في هذا الكائن وسميناه id
تزداد قيمة هذا الحقل كلما أضفنا ملاحظة جديدة ولا حاجة أن يفعل المطوّر هذا صراحة بل هي عملية آلية. يعمل هذا الحقل كمعرّف فريد لكل سجل وذلك عندما نريد تعديل أو حذف هذا السجل.
وأنشأنا أيضًا حقلين مفهرسين آخرين باستخدام التابع ()IDBObjectStore.createIndex
وهما title
و body
كي يضما عنوان الملاحظة ونصها. وسيمثَّل كل منهما على شكل كائن له التخطيط التالي، وذلك عندما نبدأ بإضافة الملاحظات إلى قاعدة البيانات وفق التخطيط الذي وضعناه:
{ "title": "Buy milk", "body": "Need both cows milk and soy.", "id": 8 }
إضافة بيانات إلى قاعدة البيانات
لنرى اﻵن كيف يمكننا إضافة سجلات إلى قاعدة البيانات باستخدام النموذج الذي صممناه في صفحتنا. لهذا أضف الأسطر التالية تحت معالج الحدث السابق. تضبط هذه اﻷسطر معالج الحدث submit
الذي يستدعي الدالة ()addData
عند تسليم النموذج (النقر على زر اﻹرسال):
//addData() إنشاء معالج حدث لعملية تسليم النموذج يعمل عند تنفيذ الدالة form.addEventListener("submit", addData);
لنعرّف اﻵن الدالة ()addData
كالتالي:
// التصريح عن الدالة function addData(e) { //نمنع السلوك الافتراضي، فلا نريد تسليم النموذج بالطريقة النمطية e.preventDefault(); // الحصول على القيم التي نريد تخزينها ووضعها ضمن عنصر تخزين const newItem = { title: titleInput.value, body: bodyInput.value }; // فتح قناة كتابة وقراءة إلى قاعدة البيانات const transaction = db.transaction(["notes_os"], "readwrite"); // استدعاء مخزن كائنات موجود بالفعل في قاعدة البيانات const objectStore = transaction.objectStore("notes_os"); //إلى مخزن الكائنات newItem إنشاء طلب ﻹضافة الكائن const addRequest = objectStore.add(newItem); addRequest.addEventListener("success", () => { // مسح النموذج استعدادًا ﻹضافة سجل آخر titleInput.value = ""; bodyInput.value = ""; }); // اﻹبلاغ عن نجاح العملية على قاعدة البيانات عند اكتمالها transaction.addEventListener("complete", () => { console.log("Transaction completed: database modification finished."); //مجددًا displayData تحديث عرض البيانات بعد إضافة السجل الجديد باستدعاء الدالة displayData(); }); transaction.addEventListener("error", () => console.log("Transaction not opened due to error"), ); }
لنحاول تفسير هذه الشيفرة كونها معقدة نوعًا ما:
-
تنفيذ الدالة
()Event.preventDefault
على كائن الحدث لإيقاف إرسال بيانات النموذج بالطريقة النمطية (لأنها تدفع إلى إعادة تحديث الصفحة وإفساد التأثير المطلوب). -
إنشاء كائن يمثل السجل الذي نريد إدخاله إلى قاعدة البيانات ونشره بالقيم التي نحصل عليها من عناصر اﻹدخال. ولاحظ أنه لا حاجة لتزويد السجل بقيمة للمعرّف
id
كما ذكرنا سابقًا بل سيضاف تلقائيًا. -
فتح عملية القراءة والكتابة
readwrite
إلى مخزن الكائناتnotes_os
باستخدام التابع()IDBDatabase.transaction
. يساعد كائن عمليات قاعدة البيانات في الوصول إلى مخزن الكائن لتنفيذ العملية المطلوبة عليه مثل إضافة سجل جديد. -
الوصول إلى مخزن الكائن باستخدام التابع
()IDBTransaction.objectStore
وتخزين النتيجة في المتغيرobjectStore
. -
إضافة السجل الجديد إلى قاعدة البيانات باستخدام التابع
()IDBObjectStore.add
الذي يُنشئ كائن طلب بنفس اﻹسلوب الذي رأيناه سابقًا. -
إضافة مجموعة من معالجات اﻷحداث إلى الكائن
request
والكائنtransaction
لتنفيذ الشيفرة المطلوبة عند النقاط المطلوبة خلال دورة حياة التطبيق. وبمجرد نجاح الطلب، نمسح حقول اﻹدخال في النموذج لتحضيرها لعملية إدخال أخرى. وعند اكتمال العملية، ننفذ الدالة()displayData
مجددًا لتحديث ما يُعرض من ملاحظات في الصفحة.
عرض البيانات
أشرنا إلى الدالة ()displayData
مرتين في تطبيقنا، لهذا من اﻷفضل اﻵن تعريف هذه الدالة. أضف اﻷسطر البرمجية التالية بعد تعريف الدالة السابقة:
// displayData() تعريف الدالة function displayData() { // نمحي محتوى عناصر القائمة في كل مرة نحدّث فيها ما يُعرض // وإلا ستتكرر العناصر في كل مرة نجري فيها تحديثًا while (list.firstChild) { list.removeChild(list.firstChild); } // افتح مخزن الكائنات واحصل على مؤشر يتنقل بين عناصره المختلفة const objectStore = db.transaction("notes_os").objectStore("notes_os"); objectStore.openCursor().addEventListener("success", (e) => { // احصل على مرجع إلى هذا المؤشر const cursor = e.target.result; // استمر في تنفيذ الشيفرة طالما هناك عناصر يمكن التنقل بينها if (cursor) { //p وفقرة نصية h3 وعنوان itemlist أنشئ عنصر قائمة //كي تضع ضمنها كل عنصر بيانات سيُعرض // ألحق الفقرة والعنوان بعنصر القائمة ثم ألحقه بالقائمة const listItem = document.createElement("li"); const h3 = document.createElement("h3"); const para = document.createElement("p"); listItem.appendChild(h3); listItem.appendChild(para); list.appendChild(listItem); // ضع البيانات الموجودة في المؤشر ضمن الفقرة النصية والعنوان h3.textContent = cursor.value.title; para.textContent = cursor.value.body; // خزن معرّف عنصر البيانات داخل سمة ضمن عنصر القائمة //كي نعرف إلى أي عنصر تنتمي. وسيفيدنا ذلك عند حذف العناصر listItem.setAttribute("data-note-id", cursor.value.id); // أنشئ زرًا وضعه ضمن كل عنصر قائمة const deleteBtn = document.createElement("button"); listItem.appendChild(deleteBtn); deleteBtn.textContent = "Delete"; // اضبط معالج حدث يحذف عنصر القائمة عند النقر على هذا الزر deleteBtn.addEventListener("click", deleteItem); // انقل المؤشر إلى العنصر التالي cursor.continue(); } else { //إن لم تكن هنالك أية عناصر قائمة `No notes stored` اعرض رسالة if (!list.firstChild) { const listItem = document.createElement("li"); listItem.textContent = "No notes stored."; list.appendChild(listItem); } // إن لم تكن هنالك أية عناصر اخرى للتنقل بينها، ابلغ عن ذلك برسالة console.log("Notes all displayed"); } }); }
لنشرح الآن الشيفرة السابقة بشيء من التفصيل:
-
نمحي بداية محتوى العنصر
<ul>
قبل أن نملأه مجددًا بالمحتوى المُحدَّث، وإلا سينتهي بك اﻷمر إلى قائمة مليئة بالعناصر المكررة التي تُضاف عند كل تحديث. -
نتخذ مرجعًا إلى مخزن الكائن
notes_os
باستخدام التابعين()IDBDatabase.transaction
و()IDBTransaction.objectStore
كما فعلنا مع الدالة()addData
، ماعدا أننا نربطهما معًا في سطر واحد هنا. -
نستخدم تاليًا التابع
()IDBObjectStore.openCursor
لطلب للحصول على مؤشر Cursor، وهي بنية تُستخدم للتنقل بين السجلات في مخزن الكائنات. كما نربط معالج حدث النجاحsuccess
في نهاية السطر لنجعل الشيفرة أكثر ترابطًا. وعندما يُعاد المؤشر بنجاح يُنفَّذ المعالج. -
نتخذ مرجعًا إلى المؤشر ذاته (على شكل كائن
IDBCursor
) باستخدام السطرconst cursor= e.target.result
-
نتحقق تاليًا من وجود سجل من مخزن الكائنات ضمن المؤشر (
{}if (cursor)
)، فإن كان اﻷمر كذلك، ننشئ فرعًا في شجرة DOM وننشر ضمنه بيانات السجل ومن ثم نعرضها على الصفحة ضمن العنصر<ul>
. كما نضمّن الفرع زرًا يحذف عنصر القائمة الذي يضم بيانات السجل عند النقر على هذا الزر، وذلك بتنفيذ الدالة()deleteItem
التي نتعرف عليها في الفقرة القادمة. -
في نهاية الكتلة
if
نستخدم التابع()IDBCursor.continue
لنقل المؤشر إلى السجل التالي في المخزن وتنفيذ محتوى الكتلةif
مجددًا إن كان هنالك سجل آخر سيعرض في الصفحة، ومن ثم يتابع المؤشر تفقد وجود سجلات أخرى. -
عند انتهاء السجلات، يعيد المؤشر القيمة
undefined
وبالتالي ستعمل الكتلةelse
هذه المرة. وتتحقق هذه الكتلة من إضافة أية ملاحظات إلى القائمة<ul>
، فإن لم تُضاف أية ملاحظات، تعرض رسالة مفادها عدم وجود أية ملاحظات محفوظة.
حذف ملاحظة
أشرنا سابقا إلى أن النقر على زر الحذف الموجود إلى جوار الملاحظة المعروضة يسبب حذفها. وننفذ ذلك باستخدام الدالة ()deleteitem
:
// deleteItem() تعريف الدالة function deleteItem(e) { // الحصول على الملاحظة التي ينبغي حذفها على شكل عدد قبل //IDB: IDB key محاولة استخدامها عن طريق الزوج // والانتباه إلى أن القيم حساسة لحالة الأحرف const noteId = Number(e.target.parentNode.getAttribute("data-note-id")); // فتح قناة العمليات مع قاعدة البيانات وحذف الملاحظة التي حصلنا //على رقمها سابقًا عن طريق السمة التي خزنا فيها معرف الملاحظة const transaction = db.transaction(["notes_os"], "readwrite"); const objectStore = transaction.objectStore("notes_os"); const deleteRequest = objectStore.delete(noteId); // اﻹبلاغ عن حذف عنصر القائمة transaction.addEventListener("complete", () => { // حذف العنصر اﻷب للزر وهو في حالتنا عنصر القائمة ولن يُعرض بعدها e.target.parentNode.parentNode.removeChild(e.target.parentNode); console.log(`Note ${noteId} deleted.`); //إن لم تكن هنالك عناصر قائمة `No Notes Stored` عرض رسالة if (!list.firstChild) { const listItem = document.createElement("li"); listItem.textContent = "No notes stored."; list.appendChild(listItem); } }); }
-
نحصل على معّرف السجل الذي نحذفه باستخدام التابع
Number(e.target.parentNode.getAttribute('data-note-id'))
، وتذكر أن معرّف السجل قد خُزِّن سابقًا ضمن السمةdata-note-id
لعنصر القائمة<li>
عندما عُرض أول مرة. لكن لا بد من تمرير الدالة التي تعطينا قيمة السمة إلى التابع العام()Number
لأنها من النوع النصي وما نريده هو المكافئ العددي للقيمة وإلا لن تميزها قاعدة البيانات التي تتوقع عددًا. -
نتخذ تاليًا مرجعًا إلى مخزن الكائن مستخدمين اﻷسلوب الذي خبرناه سابقًا ومن ثم التابع
()IDBObjectStore.delete
لحذف السجل من قاعدة البيانات بعد أن نمرر له معرف الملاحظة. -
عند اكتمال العملية على قاعدة البيانات، نحذف العنصر
<li>
من شجرة DOM ونتحقق مجددًا من خلو القائمة<ul>
من العناصر.
إن واجهت صعوبة في تطبيق المثال، قارن بين نسختك والنسخة المكتملة كما يمكنك أيضًا الاطلاع على الشيفرة المصدرية.
الخلاصة
تعرفنا في هذا المقال على قاعدة البيانات المدمجة في المتصفح IndexedDB وكيفية التعامل معها من خلال مثال تطبيقي يعرض أساسيات إضافة وحذف البيانات واسترجاعها.
ترجمة -وبتصرف- للجزء الثاني من مقال: Client-side storage
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.