اذهب إلى المحتوى

البحث في الموقع

المحتوى عن 'indexeddb'.

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • مقالات برمجة عامة
  • مقالات برمجة متقدمة
  • PHP
    • Laravel
    • ووردبريس
  • جافاسكربت
    • لغة TypeScript
    • Node.js
    • React
    • Vue.js
    • Angular
    • jQuery
    • Cordova
  • HTML
  • CSS
    • Sass
    • إطار عمل Bootstrap
  • SQL
  • لغة C#‎
    • ‎.NET
    • منصة Xamarin
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • لغة Rust
  • برمجة أندرويد
  • لغة R
  • الذكاء الاصطناعي
  • صناعة الألعاب
  • سير العمل
    • Git
  • الأنظمة والأنظمة المدمجة

التصنيفات

  • تصميم تجربة المستخدم UX
  • تصميم واجهة المستخدم UI
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب GIMP
    • كريتا Krita
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • مقالات DevOps عامة
  • خوادم
    • الويب HTTP
    • البريد الإلكتروني
    • قواعد البيانات
    • DNS
    • Samba
  • الحوسبة السحابية
    • Docker
  • إدارة الإعدادات والنشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • مقالات عمل حر عامة
  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • العمل الحر المهني
    • العمل بالترجمة
    • العمل كمساعد افتراضي
    • العمل بكتابة المحتوى

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
    • بريستاشوب
    • أوبن كارت
    • دروبال
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
    • Trados
    • Memsource
  • برامج تخطيط موارد المؤسسات ERP
    • تطبيقات أودو odoo
  • أنظمة تشغيل الحواسيب والهواتف
    • ويندوز
    • لينكس
  • مقالات عامة

التصنيفات

  • آخر التحديثات

أسئلة وأجوبة

  • الأقسام
    • أسئلة البرمجة
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات

التصنيفات

  • كتب ريادة الأعمال
  • كتب العمل الحر
  • كتب تسويق ومبيعات
  • كتب برمجة
  • كتب تصميم
  • كتب DevOps

ابحث في

ابحث عن


تاريخ الإنشاء

  • بداية

    نهاية


آخر تحديث

  • بداية

    نهاية


رشح النتائج حسب

تاريخ الانضمام

  • بداية

    نهاية


المجموعة


النبذة الشخصية

تم العثور على 2 نتائج

  1. IndexedDB هي قاعدة بيانات مدمجة مع المتصفح، ولها ميزات أقوى بكثير من الكائن localStorage، أهمها: تخزن أي نوع من القيم تقريبًا من خلال المفاتيح ذات الأنواع المختلفة. تدعم الإجرائيات المترابطة transactions لوثوقية أعلى. تدعم الاستعلامات عن المفاتيح ضمن مجالات، كما تدعم الوصول إلى المفتاح بالفهرس index. يمكن أن تخزّن بيانات أحجام أكبر بكثير مما يخزنه الكائن localStorage. إنّ القدرة التي تؤمّنها قاعدة البيانات هذه تفوق المطلوب في تطبيقات (خادم-عميل) التقليدية، فهي مصممة للتطبيقات التي تعمل دون اتصال offline، وذلك لتشترك مع تقنية عمال الخدمات ServiceWorkers وغيرها من التقنيات. تشرح توصيفات قاعدة البيانات IndexedDB الواجهة الأصلية للتعامل مع القاعدة، وتتميز بأنها مقادة بالأحداث، كما يمكن أيضًا استخدام آلية async/await بمساعدة مُغلّف wrapper يعتمد على الوعود promise مثل المُغلّف idb، وعلى الرغم من أن هذه آلية مريحة، إلا أنها ليست مثاليةً، إذ لن يتمكن المغلف من استبدال الأحداث في كل الحالات، لذلك سنبدأ أولًا بالتعرف على الأحداث، ثم نتفهم قاعدة البيانات IndexedDb، ثم سنعود لاستخدام المُغلِّف. الاتصال مع قاعدة البيانات نحتاج أولًا إلى تأسيس اتصال مع قاعدة البيانات IndexedDB باستعمال التابع open قبل البدء بالعمل معها، ولهذا التابع الصيغة التالية: let openRequest = indexedDB.open(name, version); name: قيمة نصية تشير إلى اسم قاعدة البيانات. version: قيمة صحيحة موجبة لنسخة قاعدة البيانات، وهي 1 افتراضيًا. قد توجد قواعد بيانات عديدة بأسماء مختلفة، لكنها تعود جميعها إلى نفس الأصل (نطاق/بروتوكول/منفذ)، ولا يمكن لمواقع الويب المختلفة الوصول إلى قواعد بيانات المواقع الأخرى. يعيد الاستدعاء الكائن openRequest، وينبغي علينا الاستماع إلى الأحداث المتعلقة به، وهي: success: يقع عندما تكون قاعدة البيانات جاهزة، أو لما يتواجد كائن قاعدة بيانات ضمن openRequest.result، والذي علينا استخدامه في الاستدعاءات اللاحقة. error: يقع عند الإخفاق في إنشاء الاتصال مع قاعدة البيانات upgradeneeded: قاعدة البيانات جاهزة، لكن نسختها قديمة. تتميز IndexedDB بوجود آلية مدمجة فيها لتحديد نسخة تخطيطها schema versioning، والتي لا نراها في قواعد البيانات الموجودة في جهة الخادم، فهي قاعدة بيانات تعمل من جهة العميل وتخزّن بياناتها ضمن المتصفح، كما لا يستطيع المطورون الوصول إليها في أي وقت، لذا عندما يزور المستخدم موقع الويب بعد إطلاق نسخة جديدة من التطبيق، فلا بدّ من تحديث قاعدة البيانات، فإذا كانت نسخة قاعدة البيانات أقل من تلك التي يحملها الأمر open، فسيقع الحدث upgradeneeded الذي يوازن بين النسختين ويُحدِّث هياكل البيانات بما يناسب، كما يقع هذا الحدث أيضًا عندما تكون قاعدة البيانات غير موجودة (أي تكون نسختها -تقنيًا- "0")، وهذا ما يجعلنا قادرين على إجراء عملية التهيئة. لنفترض أننا قد أصدرنا النسخة الأولى من تطبيقنا، حيث يمكننا عندها تأسيس الاتصال مع قاعدة بيانات نسختها "1"، وتهيئتها بالاستفادة من معالج الحدث upgradeneeded بالشكل التالي: let openRequest = indexedDB.open("store", 1); openRequest.onupgradeneeded = function() { // يقع عندما لا يمتلك العميل قاعدة بيانات // ...إنجاز التهيئة... }; openRequest.onerror = function() { console.error("Error", openRequest.error); }; openRequest.onsuccess = function() { let db = openRequest.result; // متابعة العمل مع قاعدة البيانات }; ثم أصدرنا لاحقًا النسخة الثانية، عندها سنتمكن من تأسيس الاتصال مع النسخة "2" والتحديث بالشكل التالي: let openRequest = indexedDB.open("store", 2); openRequest.onupgradeneeded = function(event) { // قاعدة البيانات أقل من النسخة 2 أو غير موجودة let db = openRequest.result; switch(event.oldVersion) { // النسخة الموجودة من القاعدة case 0: // لا قاعدة بيانات // تهيئة case 1: // يمتلك المستخدم النسخة 1 من القاعدة // تحديث } }; لاحظ أنه عندما تكون نسختنا الحالية هي "2"، فسيتضمن معالج الحدث onupgradeneeded شيفرةً تعالج حالة النسخة "0"، وهذا الأمر ملائم للمستخدمين الذين يزورون الصفحة للمرة الأولى ولا يمتلكون قاعدة بيانات، كما يتضمن شيفرةً تعالج وجود النسخة "1" لتحديثها. سيقع الحدث openRequest.onsuccess عندما ينتهي معالج الحدث onupgradeneeded بنجاح، وينجح تأسيس الاتصال مع قاعدة البيانات. لحذف قاعدة البيانات: let deleteRequest = indexedDB.deleteDatabase(name) //العملية deleteRequest.onsuccess/onerror يتتبع الحدثان لا يمكن تأسيس اتصال مع قاعدة بيانات بنسخة أقدم إذا كانت النسخة الحالية لقاعدة بيانات المستخدم أعلى من النسخة التي نمررها للاستدعاء، أي نسخة القاعدة "3" ونحاول تأسيس اتصال مع النسخة "2" مثلًا، فسيتولد خطأ وسيقع الحدث openRequest.onerror. وعلى الرغم من ندرة حدوث هذا الأمر، إلا أنه قد يحصل عندما يحاول الزائر تحميل شيفرة JavaScript قديمة، مثل أن تكون من الذاكرة المؤقتة لخادم وكيل مثلًا، حيث ستكون الشيفرة قديمةً والقاعدة حديثة. ولا بدّ من التحقق من نسخة قاعدة البيانات db.version، واقتراح إعادة تحميل الصفحة إذا أردنا الحماية من الأخطاء، كما نستخدم ترويسة HTTP ملائمةً للتعامل مع الذاكرة المؤقتة لتفادي تحميل شيفرة قديمة، وبالتالي لن نواجه مشاكل. مشكلة التحديث المتوازي Parallel update ما دمنا نتكلم عن تحديد نسخة التطبيق، فسنتطرق إلى مشكلة صغيرة مرتبطة بذلك، لنتأمل الحالة التالية: فتح مستخدم موقعنا في نافذة متصفح، وكانت نسخة قاعدة البيانات هي "1". ثم حدّثنا الصفحة وأصبحت الشيفرة أحدث. ثم فتح المستخدم نفسه موقعنا في نافذة أخرى. أي ستكون هناك نافذة متصلة بقاعدة بيانات نسختها "1"، بينما تحاول النافذة الأخرى تحديثها إلى النسخة "2" عبر معالج الحدث upgradeneeded. تتلخص المشكلة بأن قاعدة البيانات مشتركة بين نافذتين، لأنهما تعودان لنفس الموقع ولهما الأصل ذاته، ولا يمكن أن تكونا من النسختين "1" و"2" في نفس الوقت، ولتنفيذ عملية الانتقال إلى النسخة "2"، ينبغي إغلاق كل قنوات الاتصال مع النسخة "1" بما فيها قناة اتصال النافذة الأولى، ولتنظيم ذلك سيقع الحدث versionchange ضمن كائن قاعدة البيانات "المنتهية الصلاحية"، لذا يفترض الاستماع لهذا الحدث، وإغلاق اتصال قاعدة البيانات القديمة. يمكن اقتراح إعادة تحميل الصفحة للحصول على الشيفرة الأحدث، فإذا لم نستمع إلى الحدث versionchange ولم نغلق قناة الاتصال، فلن يُنفَّذ الاتصال الثاني، وسيعطي الكائن openRequest الحدث blocked بدلًا من success ولن تعمل النافذة الثانية. إليك الشيفرة التي تتعامل بشكل صحيح مع التحديث المتوازي، إذ تثبّت معالج الحدث onversionchange الذي يقع عندما يصبح الاتصال الحالي مع قاعدة البيانات منتهي الصلاحية، أي عندما تُحدَّث نسخة قاعدة البيانات في مكان آخر، ويغلق الاتصال. let openRequest = indexedDB.open("store", 2); openRequest.onupgradeneeded = ...; openRequest.onerror = ...; openRequest.onsuccess = function() { let db = openRequest.result; db.onversionchange = function() { db.close(); alert("Database is outdated, please reload the page.") }; // ...قاعدة البيانات جاهزة! استخدمها... }; openRequest.onblocked = function() { }; إن ما نفعله هنا بعبارة أخرى هو: يُعلمنا المستمِع إلى الحدث db.onversionchange عن محاولة التحديث المتوازي، عندما تصبح النسخة الحالية لقاعدة البيانات منتهية الصلاحية. يُعلمنا المستمِع إلى الحدث openRequest.onblocked عن الحالة المعاكسة، وهي وجود اتصال لم يُغلَق بعد مع نسخة منتهية الصلاحية في نافذة ما، وبالتالي لن يعمل الاتصال الجديد. يمكن أن نتعامل مع الموضوع بطريقة ألطف عند استخدام الحدث db.onversionchange الذي يبلغ المستخدم بوجوب حفظ بياناته قبل قطع الاتصال، يمكن أيضًا اعتماد مقاربة أخرى لا تتعلق بإغلاق الاتصال مع قاعدة البيانات في معالج الحدث db.onversionchange، بل باستخدام معالج الحدث onblocked -في نافذة متصفح أخرى- لتنبيه المستخدم بأن النسخة الجديدة لن تُحمَّل قبل إغلاق النافذة الأخرى. لا يحدث التعارض في التحديث إلا نادرًا، ومع ذلك لا بدّ من توقعه والتعامل معه، على الأقل باستخدام معالج الحدث onblocked لمنع انهيار السكربت. مخزن الكائن نحتاج إلى مخزن الكائنات Object Store لتخزين أي شيء في قاعدة البيانات IndexedDB، وهو مفهوم جوهري فيها، ويقابل مفهوم "الجداول tables" أو "المجموعات collections" في قواعد البيانات الأخرى، كما تُخزّن فيه البيانات. وقد تتكون القاعدة من عدة مخازن، يكون الأول فيها للمستخدمين، والثاني للبضائع مثلًا وغيرها. على الرغم من اسم "مخزن الكائن" إلا أنه يمكن تخزين القيم الأولية فيه بالإضافة إلى الكائنات، إذ يمكن تخزين أي قيمة بما فيها الكائنات المعقدة، وتستخدم قاعدة البيانات خوارزمية التفكيك المعيارية standard serialization algorithm لنسخ وتخزين الكائن، ويشابه ذلك استخدام الأمر JSON.stringify لكن مع إمكانيات أكثر، وقدرة على تخزين أنواع أكثر من البيانات. ومن الكائنات التي لا يمكن تخزينها في هذه المخازن، نجد الكائن ذو المراجع الحلقية التي تشكل حلقةً يدل آخرها على أولها، والتي لا يمكن أن تُفكك، وسيفشل الأمر JSON.stringify معها. يجب أن يكون لكل قيمة ستُخزّن في قاعدة البيانات مفتاح فريد key، كما يجب أن تكون هذه المفاتيح من أحد الأنواع التالية: رقم أو تاريخ أو نص أو قيمة ثنائية أو مصفوفة، وسنتمكن من البحث عن القيم أو إزالتها أو تحديثها بواسطة هذه المفاتيح الفريدة. سنرى قريبًا كيف سنحصل على مفتاح عند إضافة قيمة جديدة إلى المخزن، كما يحدث عند استخدام الكائن localStorage، لكن ستسمح لنا قاعدة البيانات IndexedDB عند تخزين كائن؛ بضبط وإعداد خصائصه مثل مفتاح، وهذه مقاربة أفضل بكثير، كما يمكننا توليد المفاتيح تلقائيًا، لكننا سنحتاج إلى إنشاء مخزن للكائن أولًا. إليك الصيغة المستخدمة في إنشاء مخزن لكائن: db.createObjectStore(name[, keyOptions]); name: وتمثل اسم المخزن. keyOptions: وتمثل كائنًا اختياريًا له خاصيتان، هما: keyPath: المسار إلى خاصية الكائن التي سنستخدمها مفتاحًا، مثل id. autoIncrement: إذا كانت قيمته true فسيتولد تلقائيًا مفتاح للكائن الجديد، مثل رقم يتزايد باستمرار. إذا لم نستخدم keyOptions، فلا بدّ حينها من التصريح عن المفتاح لاحقًا عند تخزين الكائن. يستخدم مخزن الكائن التالي الخاصية id مفتاحًا، وبشكل صريح: db.createObjectStore('books', {keyPath: 'id'}); لا يمكن إنشاء أو تعديل مخزن كائن عند تحديث نسخة قاعدة البيانات إلا ضمن معالج الحدث upgradneeded، ويشكل هذا الأمر محدوديةً تقنيةً، إذ يمكن إضافة أو إزالة أو تحديث البيانات خارج المعالج، بينما لا يمكننا إنشاء أو حذف أو تغيير مخزن الكائن إلا خلال تحديث نسخة قاعدة البيانات. وتوجد مقاربتان لتنفيذ عملية تحديث نسخة قاعدة البيانات: يمكن كتابة دوال خاصة لتحديث كل نسخة ممكنة إلى النسخة الجديدة، من "1" إلى "2"، ومن "2" إلى "3" وهكذا، ثم يمكن موازنة النسخ ضمن جسم دالة معالج الحدث upgradeneeded من القديمة 2 إلى الحديثة 4 مثلًا، وتنفيذ الدالة المناسبة لتنفيذ التحديث خطوةً بخطوة، أي من 2 إلى 3، ثم من 3 إلى 4. فحص قاعدة البيانات والحصول على قائمة بمخازن الكائنات الموجودة باستخدام التابع db.objectStoreNames، ويمثل الكائن المُعاد قائمةً من النوع DOMStringList، والذي يزودنا بالتابع (contains(name الذي يتحقق من وجود مخزن باسم محدد، ثم سنتمكن من إجراء التحديث اعتمادًا على ما هو موجود وما هو غير موجود، وهذه المقاربة أبسط لقواعد البيانات الصغيرة، وإليك مثالًا عن استخدامها: let openRequest = indexedDB.open("db", 2); // تحديث وإضافة قواعد البيانات دون تحقق openRequest.onupgradeneeded = function() { let db = openRequest.result; if (!db.objectStoreNames.contains('books')) { // "books" إن لم يكن هناك مخزن باسم db.createObjectStore('books', {keyPath: 'id'}); // أنشئه } }; ولحذف مخزن كائن: db.deleteObjectStore('books') الإجرائيات المترابطة Transactions يُعَد مصطلح "إجرائيات مترابطة" مصطلحًا عامًا، ويُستخدم في قواعد بيانات عديدة، كما يشير إلى مجموعة من العمليات التي ينبغي أن تُنفَّذ مترابطةً، بحيث تنجح معًا أو تخفق معًا، فعندما يشتري شخص شيئًا ما مثلًا، فعلينا: سحب المبلغ من حساب المشتري. إضافة المشتريات إلى سلة مشترياته. سيبدو الأمر سيئًا بالطبع لو أُنجزت الخطوة الأولى بنجاح وفشلت الخطوة الثانية لسبب أو لآخر، فإما أن تنجح الخطوتان -أي يكتمل الشراء- أو تخفق الخطوتان، فعندها لن يخسر المشتري ماله ويستطيع إعادة المحاولة مجددًا. ينبغي أن تُنفَّذ جميع العمليات على البيانات ضمن إجرائيات مترابطة في القاعدة IndexedDB، وللبدء بإجرائية مترابطة يجب تنفيذ الأمر: db.transaction(store[, type]); store: اسم المخزن الذي ستُنفَّّذ عليه إجرائية مترابطة، المخزن "books" مثلًا، ويمكن أن يكون مصفوفةً من أسماء المخازن إذا كنا نريد الوصول إلى عدة مخازن. type: نوع الإجرائية المترابطة، وقد يكون: readonly: يُنفِّذ عمليات قراءة فقط، وهذا هو الخيار الافتراضي. readwrite: يُنفِّذ عمليات قراءة وكتابة فقط، ولا يستطيع إنشاء أو حذف أو تبديل الكائن. كما يمكن تنفيذ إجرائيات مترابطة من النوع versionchange، ويستطيع هذا النوع تنفيذ أي شيء، لكن لا يمكن إنشاؤه يدويًا، إذ تنشئ قاعدة البيانات الإجرائيات المترابطة من النوع versionchange عند تأسيس الاتصال مع قاعدة البيانات لتنفيذ معالج الحدث updateneeded، لذا فهو المكان الوحيد الذي نستطيع فيه تحديث هيكلية قاعدة البيانات أو إنشاء وحذف كائن المخزن. يمكن إضافة العناصر إلى المخزن بعد إنشاء الإجرائية المترابطة بالشكل التالي: let transaction = db.transaction("books", "readwrite"); // (1) // الحصول على كائن المخزن للتعامل معه let books = transaction.objectStore("books"); // (2) let book = { id: 'js', price: 10, created: new Date() }; let request = books.add(book); // (3) request.onsuccess = function() { // (4) console.log("Book added to the store", request.result); }; request.onerror = function() { console.log("Error", request.error); }; تُنفَّذ العملية مبدئيًا وفق خطوات أربع هي: إنشاء إجرائية مترابطة تشير إلى كل المخازن الذي سنصل إليها. الحصول على كائن المخزن باستخدام الأمر (transaction.objectStore(name. تنفيذ العمليات على كائن المخزن (books.add(book. التعامل مع حالة نجاح أو فشل الإجرائية المترابطة على كائن المخزن (books.add(book. تدعم كائنات المخازن تابعين لتخزين القيم، هما: ([put(value,[key: حيث يضيف القيمة value إلى المخزن، ويزوَّد التابع بالمعامل key فقط في الحالة التي لا يمتلك فيها المخزن أحد الخيارين keyPath أو autoIncrement، فإن وجدت قيمة لها نفس المفتاح فستُستبدَل. ([add(value,[key: يشابه التابع السابق، لكن إن وجدت قيمة لها نفس المفتاح فسيخفق الطلب، وسيولِّد خطأً باسم "ConstraintError". يمكن إرسال الطلب (books.add(book مثلًا بما يتناسب مع تأسيس اتصال مع قاعدة بيانات، ومن ثم ننتظر وقوع أحد الحدثين success/error. سيكون مفتاح الكائن الجديد هو نتيجة الطلب request.result للتابع add. فإن وقع خطأ ما فسنجده في الكائن request.error. اكتمال الإجراءات المترابطة بدأنا الإجرائية المترابطة في المثال السابق بتنفيذ الطلب add، لكن -وكما أشرنا سابقًا- قد تتألف الإجرائية المترابطة من عدة طلبات ينبغي أن تنجح معًا أو تخفق معًا، فكيف سنميِّز إذًا انتهاء الإجرائية ولا توجد طلبات أخرى قيد التنفيذ؟ الجواب باختصار هو أننا لا نستطيع، ولا بدّ من وجود طريقة يدوية لإنهاء الإجرائيات المترابطة في النسخة التالية 3.0 من التوصيفات، لكن حاليًا في النسخة 2.0 لا يوجد شيء مشابه لهذا. وستكتمل الإجرائية تلقائيًا عندما تنتهي جميع الطلبات المتعلقة بإجرائية مترابطة، وسيصبح صف المهام المتناهية الصغر microtasks queue فارغًا. توصف الإجرائية عادةً بأنها مكتملة عندما تكتمل كل طلباتها، وينتهي تنفيذ الشيفرة المتعلقة بها، لذا فلا حاجة في الشيفرة السابقة مثلًا إلى استدعاء خاص لإنهاء الإجرائية المترابطة. ويترافق مبدأ الاكتمال التلقائي للإجرائيات المترابطة بتأثير جانبي مهم، فلا يمكن تنفيذ عملية غير متزامنة مثل fetch أو setTimeout أثناء تنفيذ الإجرائية، كما لن تبقي قاعدة البيانات IndexedDB الإجرائية المترابطة في حالة انتظار حتى تُنجز هذه العملية. سيُخفق الطلب request2 في السطر(*) من الشيفرة التالية لأن الإجرائية قد اكتملت بالفعل، ولن نتمكن من تنفيذ طلبات أخرى: let request1 = books.add(book); request1.onsuccess = function() { fetch('/').then(response => { let request2 = books.add(anotherBook); // (*) request2.onerror = function() { console.log(request2.error.name); // TransactionInactiveError }; }); }; لأنّ fetch عملية غير متزامنة وتمثل مهمةً مستقلةً macrotask، وستُغلَق الإجرائية المترابطة قبل أن يبدأ المتصفح بتنفيذ مهمات مستقلة، كما سيرى محررو توصيفات قاعدة البيانات IndexedDB أنّ مدة إنجاز الإجرائيات المترابطة لا بدّ أن تكون قصيرة، وذلك لأسباب تتعلق بالأداء في الغالب. تقفِل الإجرائيات المترابطة من النوع readwrite المخزن عند الكتابة فيه، لذا إذا حاول جزء آخر من التطبيق تنفيذ عملية مشابهة على نفس المخزن، فعليه الانتظار. ستعلَّق الإجرائية المترابطة الجديدة حتى تنتهي الأولى، وهذا ما يسبب تأخيرًا غريبًا إذا استغرقت إجرائية مترابطة ما وقتًا طويلًا، فما العمل إذًا؟ بإمكاننا في المثال السابق تنفيذ إجرائية مترابطة جديدة db.transaction تمامًا قبل الطلب الجديد في السطر "(*)"، لكن يفضَّل -إن أردنا إبقاء العمليات معًا في إجرائية مترابطة واحدة- أن نفصل الإجرائية المترابطة في قاعدة البيانات IndexedDB عن الأمور الأخرى غير المتزامنة. نفِّذ العملية fetch أولًا، ثم حضر البيانات إن تطلب الأمر ذلك، ثم أنشئ إجرائيةً مترابطةً، ونفّذ كل الطلبات وسينجح الأمر. استمع إلى الحدث transaction.oncomplete لمعرفة اللحظة التي تكتمل فيها الإجرائية المترابطة بنجاح. let transaction = db.transaction("books", "readwrite"); // ...perform operations... transaction.oncomplete = function() { console.log("Transaction is complete"); }; تضمن الخاصية complete فقط اكتمال وحفظ الإجرائية بالكامل، وقد تنجح طلبات بمفردها، لكن قد تفشل بالمقابل عملية الكتابة النهائية، بسبب خطأ في منظومة الدخل/خرج مثلًا. استدعي التابع التالي لإيقاف الإجرائية المترابطة يدويًا: transaction.abort(); سيلغي هذا الاستدعاء كل التغييرات التي نفذتها الطلبات، ويتسبب بوقوع الحدث transaction.onabort. معالجة الأخطاء قد تُخفق طلبات الكتابة، ولا بدّ من توقع هذا الأمر -لا نتيجةً للأخطاء المحتملة التي قد نرتكبها فقط- بل لأسباب تتعلق بالإجرائيات المترابطة بحد ذاتها، فقد يحدث تجاوز حجم المخزن المحدد على سبيل المثال، لذا لا بدّ أن نكون مستعدين للتعامل مع حالات كهذه. يوقف إخفاق الطلب الإجرائية المترابطة تلقائيًا ويلغي كل التغييرات التي حدثت، لكننا قد نحتاج إلى التعامل مع حالة إخفاق الطلب، لتجريب طلب آخر مثلًا، دون إلغاء التغييرات التي حدثت، ومن ثم متابعة الإجرائية المترابطة، وهذا أمر ممكن، إذ يمكن لمعالج الحدث request.onerror أن يمنع إلغاء الإجرائية المترابطة عن طريق استدعاء التابع ()event.preventDefault. سنرى في المثال التالي كيف يُضاف كتاب جديد بمفتاح id موجود مسبقًا، وعندها سيولِّد التابع store.add الخطأ "ConstraintError"، الذي نتعامل معه دون إلغاء الإجرائية المترابطة: let transaction = db.transaction("books", "readwrite"); let book = { id: 'js', price: 10 }; let request = transaction.objectStore("books").add(book); request.onerror = function(event) { //عند إضافة قيمة بمفتاح موجود مسبقًا ConstraintError يقع الخطأ if (request.error.name == "ConstraintError") { console.log("Book with such id already exists"); // التعامل مع الخطأ event.preventDefault(); // لا تلغ الإجرائية المترابطة // استخدم مفتاح جديد للكتاب؟ } else { // خطأ غير متوقع لايمكن التعامل معه // ستلغى الإجرائية المترابطة } }; transaction.onabort = function() { console.log("Error", transaction.error); }; تفويض الأحداث هل نحتاج إلى الحدثين onsuccess/onerror عند كل طلب؟ والجواب هو لا، ليس في كل مرة، إذ يمكننا أن نستعمل تفويضًا للحدث event delegation بدلًا من ذلك. تجري عملية انسياب Bubbling الحدث في قاعدة بيانات بالشكل التالي: request → transaction → database والتي تقتضي التقاط العنصر الداخلي ضمن شجرة DOM للحدث، ثم تستمع إليه الأحداث الخارجية بالتتالي. قد تنساب الأحداث في الشجرة DOM للخارج bubbling أو للداخل capturing، لكن يُستخدم عادةً الانسياب نحو الخارج، ويمكن حينها التقاط الأخطاء عن طريق معالج الحدث db.onerror لإظهارها أو لأي أسباب أخرى. db.onerror = function(event) { let request = event.target; // الطلب الذي ولّد الخطأ console.log("Error", request.error); }; لكن ماذا لو تمكنا من التعامل مع الخطأ كاملًا؟ عندها لن نحتاج لإظهار أي شيء، يمكننا إيقاف الانسياب الخارجي للأحداث، وبالتالي إيقاف الحدث db.onerror، باستخدام الأمر ()event.stopPropagation ضمن دالة معالجة الحدث request.onerror. request.onerror = function(event) { if (request.error.name == "ConstraintError") { console.log("Book with such id already exists"); // معالجة الخطأ event.preventDefault(); // لا توقف الإجرائية المترابطة event.stopPropagation(); // لا تجعل الأحداث تنساب للخارج } else { // لا تفعل شيئًا // إيقاف الإجرائية المترابطة // transaction.onabort يمكن التعامل مع الخطأ ضمن } }; عمليات البحث يوجد نوعان أساسيان للبحث في مخزن الكائن: بقيمة المفتاح أو مجال المفتاح، ففي مثالنا عن المخزن "books"، سنتمكن من البحث عن قيمة أو مجال من القيم للمفتاح book.id. باستخدام حقل آخر من حقول الكائن، مثل البحث في مثالنا السابق اعتمادًا على الحقل book.price، ويتطلب هذا البحث هيكليةً إضافيةً للبيانات تُدعى الفهرس index. البحث بالمفتاح لنتعرف أولًا على النوع الأول، وهو البحث بالمفتاح، وتدعم طرق البحث حالة القيمة الدقيقة للمفتاح أو ما يسمى "مجالًا من القيم"، ويمثلها الكائن IDBKeyRange، وهي كائنات تحدد مجالًا مقبولًا من قيم المفاتيح، وتتولد الكائنات IDBKeyRange نتيجةً لاستخدام الاستدعاءات التالية: ([IDBKeyRange.lowerBound(lower, [open: وتعني أن تكون القيم أكبر أو تساوي الحد الأدنى lower، أو أكبر تمامًا إذا كانت قيمة open هي "true". ([IDBKeyRange.upperBound(upper, [open: تعني أن تكون القيم أصغر أو تساوي الحد الأعلى upper، أو أصغر تمامًا إذا كانت قيمة open هي "true". ([IDBKeyRange.bound(lower, upper, [lowerOpen], [upperOpen: تعني أن تكون القيمة محصورةً بين الحد الأدنى lower والأعلى upper، ولن يتضمن المجال قيمتي الحدين الأعلى والأدنى إذا ضبطنا open على القيمة "true". (IDBKeyRange.only(key: وتمثل مجالًا يتكون من مفتاح واحد، وهو نادر الاستخدام. سنرى التطبيق العملي لهذه الاستدعاءات السابقة قريبًا. ولتنفيذ بحث حقيقي ستجد التوابع التالية التي تقبل الوسيط query، وقد يكون هذا الوسيط قيمةً دقيقةً للمفتاح أو مجالًا: (store.get(query: يبحث عن أول قيمة من خلال مفتاح أو مجال. ([store.getAll([query], [count: يبحث عن جميع القيم، ويكون عدد القيم محدودًا إذا أعطينا قيمةً للوسيط count. (store.getKey(query: يبحث عن أول مفتاح يحقق الاستعلام، وعادةً يكون مجالًا. ([store.getAllKeys([query], [count: يبحث عن كل المفاتيح التي تحقق الاستعلام، ويكون عدد النتائج محدودًا إذا أعطينا count قيمةً. [(store.count([query: يعيد العدد الكلي للمفاتيح التي تحقق الاستعلام، وعادةً يكون مجالًا. قد يكون لدينا على سبيل المثال الكثير من الكتب في مخزننا، وما دام الحقل id هو المفتاح، فستتمكن تلك التوابع من البحث باستعماله، مثل: // الحصول على كتاب واحد books.get('js') // 'css' <= id <= 'html' الحصول على كتاب يكون books.getAll(IDBKeyRange.bound('css', 'html')) // id < 'html' الحصول على كتاب بحيث books.getAll(IDBKeyRange.upperBound('html', true)) // الحصول على كل الكتب books.getAll() // id > 'js' الحصول على كل الكتب التي تحقق books.getAllKeys(IDBKeyRange.lowerBound('js', true)) البحث بالحقول نحتاج إلى إنشاء هيكلية إضافية للبيانات تُدعى الفهرس index، لتنفيذ استعلام باستخدام حقل آخر من حقول الكائن، ويمثِّل الفهرس إضافةً add-on إلى المخزن لتتبع حقل محدد من كائن، ويُخزّن الفهرس -ومن أجل كل قيمة للحقل المحدد- قائمةً من مفاتيح الكائنات التي تمتلك تلك القيمة، وسنوضح ذلك لاحقًا بالتفصيل. إليك صيغة إنشاء الفهرس: objectStore.createIndex(name, keyPath, [options]); name: اسم الفهرس. keyPath: المسار إلى حقل الكائن الذي سيتتبعه الفهرس، وسنبحث اعتمادًا على ذلك الحقل. option: كائن اختياري له الخصائص التالية: unique: إذا كانت قيمته "true"، فيجب أن يوجد كائن واحد في المخزن له القيمة المعطاة في المسار المحدد، وسيجبر الفهرس تنفيذ هذا الأمر بتوليد خطأ إذا حاولنا إضافة نسخة مكررة. multiEntry: ويستخدَم فقط إذا كانت القيمة في المسار المحدد keyPath مصفوفةً، وسيعامل الفهرس في هذه الحالة المصفوفة بأكملها مثل مفتاح افتراضيًا، لكن إذا كانت قيمة multiEntry هي "true"، فسيحتفظ الفهرس بقائمة من كائنات المخزن لكل قيمة في تلك المصفوفة، وهكذا ستصبح عناصر المصفوفة مفاتيح فهرسة. نخزّن في المثال التالي الكتب باستعمال المفتاح id، ولنقل أننا نريد البحث باستعمال الحقل price، سنحتاج أولًا إلى إنشاء فهرس، ويجب أن ننجز ذلك ضمن معالج الحدث upgradeneeded تمامًا مثل مخزن الكائن: openRequest.onupgradeneeded = function() { //لابد من إنشاء الفهرس هنا، ضمن إجرائية تغيير النسخة let books = db.createObjectStore('books', {keyPath: 'id'}); let index = books.createIndex('price_idx', 'price'); }; سيتعقب الفهرس الحقل price. لن يكون السعر حقلًا بقيم فريدة، إذ يمكن وجود عدة كتب لها السعر نفسه، وبالتالي لن نضبط الخيار unique. السعر قيمة مفردة وليس مصفوفةً، لذا لن نطبّق الخيار multiEntry. لنتخيل الآن وجود 4 كتب في مخزننا inventory، إليك الصورة التي تظهر طبيعة الفهرس index: كما قلنا سابقًا، سيحفَظ الفهرس العائد لكل قيمة من قيم price -الوسيط الثاني- قائمةً بالمفاتيح التي ترتبط بهذا السعر، ويُحدَّث الفهرس تلقائيًا، فلا حاجة للاهتمام بهذا الأمر. سنطبِّق ببساطة التوابع السابقة نفسها على الفهرس عندما نريد أن نبحث عن سعر محدد: let transaction = db.transaction("books"); // قراءة فقط let books = transaction.objectStore("books"); let priceIndex = books.index("price_idx"); let request = priceIndex.getAll(10); request.onsuccess = function() { if (request.result !== undefined) { console.log("Books", request.result); // مصفوفة من الكتبالتي سعرها=10 } else { console.log("No such books"); } }; يمكن أن نستخدم IDBKeyRange أيضًا لإنشاء مجال محدد، والبحث عن الكتب الرخيصة أو باهظة الثمن: // جد الكتب التي سعرها يساوي أو أقل من 5 let request = priceIndex.getAll(IDBKeyRange.upperBound(5)); تُصنَّف الفهارس داخليًا وفق الحقل الذي تتعقبه، وهو السعر price في حالتنا، لذا سترتَّب النتائج حسب السعر عندما ننفذ البحث وفق حقل السعر. الحذف من مخزن يبحث التابع delete عن القيم التي يُطلب حذفها عبر استعلام، وله صيغة شبيهة بالتابع getAll. (delete(query: يحذف القيم المطابقة للاستعلام، إليك مثالًا: // id='js' احذف الكتاب الذي يحقق books.delete('js'); إذا أردنا حذف الكتب بناءً على سعرها أو بناءً على أي حقل آخر، فعلينا أولًا إيجاد المفتاح ضمن الفهرس، ثم استدعاء التابع delete: // ايجاد المفتاح في حالة السعر=5 let request = priceIndex.getKey(5); request.onsuccess = function() { let id = request.result; let deleteRequest = books.delete(id); }; ولحذف كل شيء: books.clear(); // افرغ المخزن المؤشرات Cursors تعيد التوابع -مثل getAll/getAllKeys- مصفوفةً من المفاتيح والقيم، وقد يكون كائن التخزين ضخمًا وأكبر من أن تتسع له الذاكرة المتاحة، وبالتالي سيخفق التابع getAll في الحصول على كل سجلات المصفوفة، فما العمل في حالة مثل هذه؟ ستساعدنا المؤشرات على الالتفاف على هذه المشكلة. المؤشرات هي كائنات خاصة تتجاوز كائن التخزين عند تنفيذ استعلام، وتعيد زوجًا واحدًا (مفتاح/قيمة) في كل مرة، وبالتالي ستساعدنا في توفير الذاكرة. ما دام مخزن الكائن سيُصنَّف وفقًا للمفتاح، فسينتقل المؤشر عبر المخزن وفق ترتيب المفتاح تصاعديًا (افتراضيًا). إليك صيغة استخدام المؤشر: // لكن مع مؤشر getAll مثل let request = store.openCursor(query, [direction]); // store.openKeyCursor للحصول على المفاتيح لا القيم query: مفتاح أو مجال لمفتاح، ويشابه في عمله getAll. direction: وهو وسيط اختياري، ويفيد في ترتيب تنقل المؤشر بين السجلات: next: وهي القيمة الافتراضية، حيث يتحرك المؤشر من المفتاح ذي القيمة الأدنى إلى الأعلى. prev: يتحرك المؤشر من القيمة العليا للمفتاح إلى الدنيا. nextunique وprevunique: تشابهان الخيارين السابقين، لكنهما تتجاوزان المفتاح المكرر، وتعملان فقط مع المؤشرات المبنية على فهارس، فعند وجود عدة قيم لن تُعاد إلا قيمة أول سعر يحقق معيار البحث. إن الاختلاف الرئيسي في عمل المؤشرات هو أنها تسبب وقوع الحدث request.onsuccess عدة مرات، مرةً عند كل نتيجة. إليك مثالًا عن استخدام المؤشر: let transaction = db.transaction("books"); let books = transaction.objectStore("books"); let request = books.openCursor(); // يُستدعى من أجل كل كتاب وجده المؤشر request.onsuccess = function() { let cursor = request.result; if (cursor) { let key = cursor.key; // (id) مفتاح الكتاب let value = cursor.value; // كائن الكتاب console.log(key, value); cursor.continue(); } else { console.log("No more books"); } }; للمؤشر التوابع التالية: (advance(count: يدفع المؤشر إلى الأمام بمقدار الوسيط count ويتجاوز القيم. ([continue([key]): يدفع المؤشر إلى القيمة التالية في المجال المطابق للبحث، أو إلى ما بعد مفتاح معين مباشرةً إذا حددنا القيمة key. يُستدعى معالج الحدث onsuccess، سواءً تعددت القيم التي تطابق معيار البحث أو لا، ثم سنتمكن من الحصول على المؤشر الذي يدل على السجل التالي ضمن النتيجة result التي حصلنا عليها، أو أن لا يؤشر إلى شيء unidefined. تدريب أنشئ المؤشر في المثال السابق لمخزن الكائن. يمكن أيضًا أن ننشئ المؤشر اعتمادًا على الفهارس، إذ تسمح الفهارس كما نتذكر بالبحث وفق أي حقل من حقول الكائن، وتتشابه المؤشرات المبنية على الفهارس مع تلك المبنية على أساس مخازن الكائنات، فهي توفر الذاكرة بإعادة قيمة واحدة في كل مرة. سيكون cursor.key مفتاح الفهرسة بالنسبة للمؤشرات المبنية على الفهارس، وينبغي أن نستخدم الخاصية cursor.primaryKey إذا أردنا الحصول على مفتاح الكائن. let request = priceIdx.openCursor(IDBKeyRange.upperBound(5)); // يُستدعى لكل سجل request.onsuccess = function() { let cursor = request.result; if (cursor) { let primaryKey = cursor.primaryKey; // مفتاح مخزن الكائن التالي (id field) let value = cursor.value; // مفتاح مخزن الكائن التالي (book object) let key = cursor.key; //مفتاح الفهرسة التالي (price) console.log(key, value); cursor.continue(); } else { console.log("No more books"); } }; مغلف الوعود Promise wrapper إنّ إضافة الحدثين onsuccess/onerror إلى كل طلب أمر مرهق، ويمكن أحيانًا تحسين الوضع باستخدام التفويض delegation، مثل إعداد معالجات أحداث للإجرائية المترابطة بأكملها، لكن الصيغة async/await أكثر ملائمةً. لنستخدم مُغلَّف الحدث البسيط idb في هذا المقال، حيث ننشئ كائن idb عامًا مزوّدًا بتوابع مبنية على الوعود promisified لقاعدة البيانات IndexedDB، وسنكتب الشيفرة التالية بدلًا من استخدام onsuccess/onerror: let db = await idb.openDB('store', 1, db => { if (db.oldVersion == 0) { // نفذ عملية التهيئة db.createObjectStore('books', {keyPath: 'id'}); } }); let transaction = db.transaction('books', 'readwrite'); let books = transaction.objectStore('books'); try { await books.add(...); await books.add(...); await transaction.complete; console.log('jsbook saved'); } catch(err) { console.log('error', err.message); } وهكذا سنرى الشيفرة المحببة للجميع، "شيفرة غير متزامنة " وكتلة "try…catch". معالجة الأخطاء إذا لم نلتقط الأخطاء، فستقع إلى أن تعترضها أول كتلة try..catch. ستتحول الأخطاء التي لا نعترضها إلى حدث "عملية رفض وعد غير معالجة" ضمن الكائن window، لكن يمكن التعامل مع هذا الخطأ بالشكل التالي: window.addEventListener('unhandledrejection', event => { let request = event.target; // IndexedDB كائن طلب أصلي للقاعدة let error = event.reason; //request.error خطأ غير مُعتَرض للكائن ...report about the error... }); إجرائية مترابطة غير فعالة تكتمل الإجرائية المترابطة -كما رأينا سابقًا- عندما ينتهي المتصفح من تنفيذ شيفرتها ومهامها المتناهية الصغر، فلو وضعنا مهمةً مستقلةً مثل fetch وسط إجرائية مترابطة، فلن تنتظر عندها الإجرائية انتهاء هذه المهمة المستقلة، بل ستكتمل الإجرائية ببساطة وسيخفق الطلب التالي. يبقى الأمر نفسه بالنسبة إلى مُغلَّف الوعود والصيغة async/await، إليك مثالًا عن العملية fetch داخل إجرائية مترابطة: let transaction = db.transaction("inventory", "readwrite"); let inventory = transaction.objectStore("inventory"); await inventory.add({ id: 'js', price: 10, created: new Date() }); await fetch(...); // (*) await inventory.add({ id: 'js', price: 10, created: new Date() }); // Error سيخفق الأمر inventory.add بعد العملية fetch في السطر "(*)" وسيقع الخطأ “inactive transaction” أي "إجرائية مترابطة غير فعالة"، لأنّ الإجرائية المترابطة قد اكتملت بالفعل وأغلقت، وسنلتف على هذه المشكلة كما فعلنا سابقًا، فإما أن ننشئ إجرائيةً مرتبطةً جديدةً، أو أن نفصل الأمور عن بعضها. حضّر البيانات ثم أحضر كل ما يلزم عبر fetch أولًا. احفظ البيانات داخل قاعدة البيانات. الحصول على كائنات أصيلة يُنفِّذ المُغلَّف طلب IndexedDB أصلي داخليًا، ويضيف معالجي الحدثين onerror/onsuccess إليه، ثم يعيد وعدًا يُرفَض أو يُنفَّذ مع نتيجة الطلب. يعمل هذا الأمر جيّدًا في معظم الأوقات، وستجد مثالًا في مستودع GitHub الخاص بمُغلًّف الحدث idb. وفي حالات قليلة نادرة، وعندما نحتاج إلى الكائن request الأصلي، يمكن الوصول إليه باستخدام الخاصية promise.request العائدة للوعد. let promise = books.add(book); // الحصول على الوعد دون انتظار النتيجة let request = promise.request; // كائن الطلب الأصلي let transaction = request.transaction; // كائن الإجرائية المترابطة الأصلي // ...do some native IndexedDB voodoo... let result = await promise; // إن كنا بحاجة لذلك الخلاصة يمكن تشبيه قاعدة البيانات بالكائن "localStorage" لكنها مدعَّمة، وهي قاعدة بيانات (مفتاح-قيمة) لها إمكانيات كبيرة كافية للتطبيقات التي تعمل دون اتصال، كما أنها سهلة الاستخدام. تُمثِّل التوصيفات الخاصة بقاعدة البيانات أفضل دليل لاستخدامها، ونسختها الحالية هي 2.0، كما ستجد بعض التوابع من النسخة 3.0 التي لن تختلف كثيرًا، وهي مدعومة جزئيًا. يمكن تلخيص طريقة استخدامها الأساسية بالشكل التالي: أولًا، الحصول على مُغلِّف وعود مثل idb. ثانيًا، فتح قاعدة البيانات. idb.openDb(name, version, onupgradeneeded) إنشاء مخزن للكائن وفهارس ضمن معالج الحدث onupgradeneeded، أو تنفيذ تحديث للنسخة عند الحاجة. ثالثًا، لتنفيذ الطلبات عليك باتباع الآتي: أنشئ إجرائيةً مترابطةً ('db.transaction('books، مع إمكانية القراءة والكتابة عند الحاجة. احصل على مخزن كائن بالشكل التالي ('transaction.objectStore('books. رابعًا، يمكنك البحث بالمفتاح أو استدعاء التوابع المتعلقة بكائن المخزن مباشرةً. أنشئ فهرسًا للبحث باستخدام حقل آخر من حقول الكائن. خامسًا، إذا لم تتسع الذاكرة للبيانات، فاستخدم مؤشرًا cursor. إليك هذا المثال النموذجي: <!doctype html> <script src="https://cdn.jsdelivr.net/npm/idb@3.0.2/build/idb.min.js"></script> <button onclick="addBook()">Add a book</button> <button onclick="clearBooks()">Clear books</button> <p>Books list:</p> <ul id="listElem"></ul> <script> let db; init(); async function init() { db = await idb.openDb('booksDb', 1, db => { db.createObjectStore('books', {keyPath: 'name'}); }); list(); } async function list() { let tx = db.transaction('books'); let bookStore = tx.objectStore('books'); let books = await bookStore.getAll(); if (books.length) { listElem.innerHTML = books.map(book => `<li> name: ${book.name}, price: ${book.price} </li>`).join(''); } else { listElem.innerHTML = '<li>No books yet. Please add books.</li>' } } async function clearBooks() { let tx = db.transaction('books', 'readwrite'); await tx.objectStore('books').clear(); await list(); } async function addBook() { let name = prompt("Book name?"); let price = +prompt("Book price?"); let tx = db.transaction('books', 'readwrite'); try { await tx.objectStore('books').add({name, price}); await list(); } catch(err) { if (err.name == 'ConstraintError') { alert("Such book exists already"); await addBook(); } else { throw err; } } } window.addEventListener('unhandledrejection', event => { alert("Error: " + event.reason.message); }); </script> وستكون النتيجة كالتالي: ترجمة -وبتصرف- للفصل indexeddb من سلسلة The Modern JavaScript Tutorial. اقرأ أيضًا المقال السابق: ملفات تعريف الارتباط وضبطها في JavaScript تعرّف على IndexedDB مفهوم Service Worker وتأثيره في أداء وبنية مواقع وتطبيقات الويب
  2. تُقدم IndexedDB أو "قاعدة البيانات المُفَهرَسَة" طريقة حيوية وفعالّة لتخزين وجلب البيانات باستخدام المتصفح. و لكونها تتعامل مع قواعد البيانات في الخادم فإن IndexedDB تسمح بتوليد مفاتيح وبحث البيانات أو ترتيبها في حقل معين. سنُبحر في هذه المقالة في عالم IndexedDB حيث سنشرح واجهة التطبيقات البرمجية الخاصة بقواعد البيانات المفهرسة IndexedDB API وذلك من خلال بناء مدير مهام من نوع خاص. لكن أولاً دعونا نأخذ أولاً فكرة عن قواعد البيانات وIndexedDB. ماهي IndexedDB ؟تعتبر IndexedDB مخزن للكائنات key-value كما أنها غير متزامنة ومرحلية. تحتوي IndexedDB على الكثير من الأفكار والتي يمكن شرحها بجملة واحدة، لكنيّ سأقوم بشرح كل واحدةٍ منها على حدى. غير متزامنة: تعني بأن IndexedDB لاتحجب واجهة المستخدم. حيث أن العمليات تجري على نحوٍ لاحق، أي أنها ليست فورية. وهذا بالطبع يسمح لواجهة المستخدم بالإستجابة للأوامر الأخرى. وكمثال للمقارنة: يعتبر التخزين المحلي localStorage متزامنًا، أي أن العمليات تجري بشكل فوري وﻻ يحدث أي شيء حتى تكتمل العملية. وفي حال كان لدينا الكثير من أوامر القراءة والكتابة سيحدث بطؤُ ملحوظ في البرنامج.مرحلية: وتعني بأن جميع العمليات في IndexedDB يجب أن تتم جميعها أو لا تتم كليةً. يُمكن أن تفشل العملية للعديد من الأسباب عندها ستقوم قاعدة البيانات بالرجوع إلى حالتها السابقة. لأنه من غير الممكن أن يكون هناك أمر جزئي في قاعدة البيانات المُفَهرسَة IndexedDB.تخزين كائن key-value: تعني بأن كل أمر في قاعدة البيانات هو كائن بحد ذاته، أي أنه يُعارض مفهوم الصفوف (rows) في قاعدة البيانات. تُستخدم قواعد البيانات التقليدية نموذجًا منطقيًا، حيث أن البيانات يتم التعرّف عليها في جدول كما ترتبط بصلات بين قيمة جدول معين وأيضاً المفاتيح لجدول آخر. كما في الصورة1. من خلال عملية تخزين الكائن key-value كمفتاح وقيمة، يحتوي كل أمر على كائن مستقل خاص به. حيث لا يمكن الربط بين أي كائن وآخر، كما يمكن أن يكون في نفس مكان التخزين أكثر من كائن و كل كائن مختلفٌ نوعه عن الآخر. الصورة 1: كائنات في نفس مكان التخزين ولا يتوجب أن تكون تلك الكائنات بنفس الخاصيات. يُعتبر هذا الاختلاف الأكبر من نوعه بين قواعد البيانات المفهرسة IndexedDB وبين قواعد البيانات الأخرى مثل SQL أو MySQL. فمثلاً يجب أن يحتوي كل حقل قيمة معينة في قواعد البيانات SQL حتى وإن كانت خالية NULL ولكن باستخدام IndexedDB فإن بنية قاعدة البيانات يمكن أن تكون مرنة حسب احتياجك واستخدامك لها. و أيضاً تحتوي IndexedDB على كمّية هائلة من تخزين البيانات تفوق التخزين المحلي localStorage والذي يفرض مساحة لاتقل عن 250 ميجابايت على معظم المتصفحات. حيث أن هذه المساحة أساسية مع المتصفح Internet Explorer ولكن تستخدم Google Opera وOpera نسبة مئوية للتخزين بينما غير معروف عن كيمة ومحدودية التخزين الذي يستخدمها فَيرفُكس. بينما مع IndexedDB يمكن إنشاء تخزين ثنائي للبيانات وبكلمات أخرى تشبه هذه العملية تركيبة Javascript مع التعامل مع البيانات والقدرة على استيعاب قواعد بيانات الموقع. المتصفحات الداعمة الحاليةللأسف تُعتبر IndexedDB غير مدعومة من جميع المتصفحات بعد. فقط مع متصفح Opera الاصدار 15 ومافوق وGoogle Chrome 24 ومافوق وفَيرفُكس15 ومافوق، أيضاُ Internet Explorer 10 ومافوق. بينما الاصدارات الأقدم من Chrome وفَيرفُكس يتم دعمها من خلال التجارب فقط التي تدعم واجهة التطبيقات البرمجية API. ولن نتكلم عن هذا. لا يدعم متصفح سفاري IndexedDB مطلقاً ولا نسخة Presto-based المدعومة من متصفح Opera اصدرات ماقبل نسخة 12. و لكن عوضاً عن ذلك فهو يدعم قاعدة بيانات الموقع القديم. كما يدعم في معظم الأحيان IndexedDBShim. طبعاً و بالنسبة للمتصفحات التي لاتدعم أيًّا منهم فمن الأفضل البقاء على قواعد البيانات التي تعمل على الخادم. كيفية اختبار دعم IndexedDBيمكنك كتابة الشِفرة التالية لاختبار فيما إذا كان استخدام IndexedDB ممكناً: var hasIDB = typeof window.indexedDB != 'undefined';أو يمكنك استخدام Modernizr: var hasIDB = Modernizr.indexeddb;كن متأكداً بأن هذه الاختبار فقط للمتصفحات الأخيرة الداعمة لـِ IndexedDB. إنشاء مدير مهامهذه الفقرة فقط لخبراء المشروع التجريبي. لكن مع ذلك يمكنك تحميل المصدر وذلك للحصول على فهم أفضل حول عمل هذه الدوال في الكود. باستخدام مدير المهام ستحصل على مايلي: حفظ المهمة.ضبط بداية و نهاية التواريخ.ضبط أولوية المهمة.إضافة ملاحظة لكل مهمة.ميزة البحث عن المهام و الملاحظات السابقة.أولاً لنبدأ بإنشاء نموذج والذي سنستخدمه لإضافة المهام الجديدة في قاعدة البيانات: <form id="addnew"> <div> <!-- Used for updates --> <input type="hidden" name="key" id="key" value=""> <label for="task">What do you need to do? (required)</label> <input type="text" name="task" id="task" value="" required> </div> <div class="txtright"> <input type="checkbox" name="status" id="status"><label for="status">Completed?</label> </div> <div> <label for="start">Start date:</label> <input type="date" id="start" name="start" value=""> </div> <div> <label for="due">Due date:</label> <input type="date" id="due" name="due" value=""> </div> <div> <label for="priority">Priority:</label> <select id="priority" name="priority"> <option value="0">None</option> <option value="1">1 - High</option> <option value="2">2</option> <option value="3">3 - Medium</option> <option value="4">4</option> <option value="5">5 - Low</option> </select> </div> <div> <label for="tasknotes">Task notes</label> <textarea id="tasknotes" name="tasknotes" cols="30" rows="3"></textarea> <button type="submit" id="submit">Save entry</button> <button type="button" id="delete" class="hidden" >Delete entry</button> </div> </form>تشرح الشِفرة البرمجية السابقة المؤلفة من HTML وبعضٍ من CSS إنشاء نموذج مشابه نوعاً ما للصورة2. الصورة 2 : نموذج مدير المهام ماذا تحتاج أن تفعل؟ كل ما تحتاجه يجب أن يكون ضمن حقول النموذج حيث ستستخدم النموذج أيضاً لاحقاً للإضافة والتحديث على المهام. وبما أننا شرحنا ما يجب علينا فعله سنقوم الآن ببناء قاعدة البيانات. إنشاء قاعدة البياناتلإنشاء قاعدة بيانات IndexedDB يجب استخدام الطريقة ()open في كائن indexedDB. var idb = indexedDB.open('IDBTaskManager', 1);في البداية يجب وضع اسم مناسب لقاعدة البيانات، وهذا إجباري، يجب أن يكون الاسم نصًا عاديًا. يمكن تسمية قاعدة البيانات لأي شيء تريده لكن فقط نص عادي. وبكل الأحوال يجب أن يكون كل اسم لقاعدة البيانات فريدًا و أصليًا. أصلي نعني به أن يكون مكونًا من البروتوكول:اسم المستضيف:المنفذ مثل https://dev.opera.com أو http://www.example.com:80. ثانياً و اختيارياً يمكن وضع رقم اصدار قاعدة البيانات، بما أن هذا المشروع هو تجربتنا الأولى سنضع الرقم1. استخدام رقم الاصدار مهم جداً لأن الدالة open تقوم بإنشاء قاعدة اذا لم تكن موجودة من الأصل ووضع رقم الاصدار هو 1 وإذا لم تكن قاعدة البيانات موجودة سيتم إنشاء اتصال إليها. يجب أن يكون رقم الاصدار integer أي عبارة عن رقم فقط وأن يكون أكبر من الصفر. بينما الأرقام العشرية أو التي تحتوي على كسور سيتم حذف تلك الكسور لإرجاع الرقم الى رقم integer. أي أن الرقم 2.5 سيصبح 2 فقط وأيضاً 0.8 سيصبح 0 (حيث يسبب ذلك خطأ برمجي). الحد الأعلى لرقم الاصدار هو 253 أو 9,007,199,254,740,992. و هذا الحد الأعلى أيضاً يمكن تطبيقه على موّلد المفاتيح. ملاحظة: يمكن من خلال متصفح الاوبرا استخدام فحص تخزين كائنات IndexedDB وأيضاً المفاتيح والقيم من خلال الخيار المصادر الموجود في خيارات المطورون في المتصفح. وبذلك سيتم إنجاز العملية والتي ستعيد الكائن IDBOpenDBRequest كما سيتم نجاح الحدث. الآن دعونا نعرّف الأمر onsuccess لهذا الحدث. سنقوم بتعيين الكائن IDBDatabase مع event.target.result والذي سيتم تحويله إلى متغير والذي يقوم بنشر هدف إلى الدوال الأخرى. // Define a global variable to hold our database object var dbobject; idb.onsuccess = function(evt){ dbobject = evt.target.result; }مع استخدام IndexedDB فإن كل خطوة أو عملية في قاعدة البيانات يجب أن تكون ضمن دالّة لأمر مرجعي. ولفعل ذلك يحتاج كائن قاعدة البيانات لأن يكون متوفرًا على كل دالة والتي تقوم بالعملية كاملة. نسخة قاعدة البياناتعندما تكون نسخة قاعدة البيانات أعلى من التي تم تخزينها مسبقاً من قبل المستخدم فإن المتصفح عندها سيطرد الحدث upgradeneeded. وهذا يتضمن أول تشغيل للبرنامج عندما تكون النسخة الأولية هي صفر. يعتبر إنشاء الحدث upgradeneeded الطريقة الوحيدة لتغيير البنية الأصلية لقاعدة البيانات. تتضمن التغيرات الجذرية إنشاء وحذف تخزين الكائن أو اضافة فهرسات. يمكننا صنع هذه التغيرات ضمن الحدث المرجعي onupgradeneeded كما في المثال التالي: idb.onupgradeneeded = function (evt) { if (evt.oldVersion < 1) { // Create our object store and define indexes. } } لو افترضنا إنشاء تخزين أو فهرسة كائن موجود مسبقاً عندها سيحدث خطأ. لكن يمكننا استخدام الخاصية oldVersion التابعة للحدث upgradeneeded للتحكم بهذه التغيرات كما سنشرح ذلك في هذه المقالة. لكن عند طرده سيظهر الحدث upgradeneeded قبل حدث نجاح الاتصال success. و أيضاً لن يتم تعريف المتغير dbobject عند استدعاء idb.onupgradeneeded. فقط تذكر ذلك عند برمجة و تطوير التطبيقات. إنشاء تخزين الكائنلاقيمة لإنشاء قاعدة البيانات بمفردها. لحفظ و تعديل الأوامر ستحتاج أيضاً إلى إنشاء تخزين الكائن. تخزين الكائنات مشابهة لعملية الجداول في SQL وهي الوحدة التي تحمل جميع البيانات والصفوف. تتغير البنية عند إنشاء تخزين الكائن ولهذا سنقوم بهذه الخطوة ضمن الأمر المرجعي onupgradeneeded. الآن سنضيف تخزين الكائن وليكن اسمه tasks باستخدام الطريقة createObjectStore: idb.onupgradeneeded = function(evt){ var dbobject = evt.target.result; // Check our version number if (evt.oldVersion < 1) { dbobject.createObjectStore('tasks',{autoIncrement: true}); } };الوسيطة الأولى للكائن createObjectStore مطلوبة لأنها اسم تخزين الكائن بينما الوسيطة الثانية اختيارية. و مع ذلك يجب أن يكون هناك قاموس والذي يعرف خيارات المفتاح للمخزن. تشبه القواميس كائنات Javascript حرفياً. و لكنهم بصراحة تم تخصيصهم للمصفوفات مع المفاتيح والقيم الخاصة بهم. تسمح لنا القواميس بتخطي الوسائط بدون قلق حول ترتيبهم. ومع createObjectStore يُمكن أن يحتوي القاموس على الخاصيات والقيم التالية: keyPath: يحدد أيّ خاصية للكائن ينبغي استخدامها كمفتاح لكل صف . القيمة الافتراضية هي null.autoIncrement: هي قيمة منطقية و التي تحدد فيما اذا يمكن توليد مفاتيح تلقائي في الصفوف أو لا . القيمة الافتراضية خطأ false.يسمح لنا keyPath بتحويل خاصية معينة إلى اجبارية. فعلى سبيل المثال استخدام: {keyPath: 'task'} تعني بأن كل كائن يتم إضافته للمخزن يجب أن يحتوي على الخاصية task. ستلاحظ من خلال المشروع التجريبي بأننا نستحدم autoIncrement. و بإستخدام كلاً من autoIncrement أو keyPath لن نتمكن من تحديد الواسطة في الطرق add أو put. ملاحظة: يمكنك استخدام autoIncrement وkeyPath سوياً حيث سيصبح المفاتيح رقمية كما أن الكائنات ستحصل على حقول إجبارية. العمل على التسجيلاتيتألف العمل مع التسجيلات من أربع مراحل وهي الإضافة، التحديث، الحذف أو الاستعادة. إنشاء طريقة اتصال لكائن أو أكثر باستخدام readwrite أو readonly.تحديد اي كائن سيقوم بالأمر مع طريقة الاستجابة.إنشاء طلب باستخدام واحداً من طرق الطلبات أو تمرير كائن.افعل شيئاً ما مع النتيجة باستخدام الأمر المرجعي onsuccess.يعتبر صعباً العمل على صف واحد من قاعدة البيانات من العمل مع مجموعة من الصفوف. وفي هذا القسم سنعمل على صف واحد باستخدام المؤشر لاسترجاع صفوف متعددة. إضافة صف/ تسجيللإضافة صفٍ جديد لديك خياران: إما باستخدام ()add أو ()put. الفرق بينهما أن ()add يمكن استخدامها في حال اضافة صف جديد فقط بينما ()put تستخدم لإضافة أو تحديث صف معين. كلا الطريقتين تقبل نقاشين كحد أعلى. Value (مطلوب): حفظ الكائن.Key (اختياري): مفتاح الكائن وهو ضروري فقط في حال كانت الحالة خاطئة للطريقة autoIncrement ولم يتم تعريف keyPath.الآن دعونا نحفظ المهمة في قاعدة البيانات عندما يقوم المستخدم بالضغط على ارسال النموذج: var addnewhandler, addnew; addnew = document.getElementById('addnew'); addnewhandler = function (evt) { 'use strict'; evt.preventDefault(); var entry = {}, transaction, objectstore, request, fields = evt.target, o; // Build our task object. for (o in fields) { if ( fields.hasOwnProperty(o)) { entry[o] = fields[o].value; } } // Open a transaction for writing transaction = dbobject.transaction(['tasks'], 'readwrite'); objectstore = transaction.objectStore('tasks'); // Save the entry object request = objectstore.add(entry); transaction.oncomplete = function (evt) { displaytasks(dbobject); }; transaction.onerror = errorhandler; }; addnew.addEventListener('submit', addnewhandler);لانحتاج فعلاً لحذف تلك القيم بما أن المدخلات الضارة محدودة للمستخدم. يمكنك إدراج الأقواس < و > عند اظهار النتيجة. في حال أردت مزامنة بيانات IndexedDB مع قواعد بيانات السيرفر عندها كن متأكداً بفلترة وادراج مدخلات المستخدم كأولوية لقاعدة البيانات. الخطوة التالية هي: فتح طريق اتصال لكائن قاعدة البيانات باستخدام الطريقة transaction. حيث تقبل الطريقة transaction نقاشين هما: تسلسل أسماء تخزين الكائن وأيضاً صيغة هذا الاتصال. التسلسل هو قائمة من مخزن كائن واحد أو أكثر حيث ينتج اتصال وهنا سنقوم فقط بفتح مخزن الكائن tasks. دعونا نفترض بأن التطبيق يسمح لنا بتعيين مهام للآخرين. يتم تخزين أولئك الأشخاص ووظائفهم في الكائن assignees. إذا أردنا أيضاً استخدام وظائف القراءة والكتابة في assignees عندها يجب استخدامهم معاً. transaction = dbobject.transaction(['tasks', 'assignees'], 'readwrite');ملاحظة: تستخدم الأقواس [ وَ ] بشكل اختياري في معظم اصدارات المتصفحات في حال أردت إنشاء اتصال لكائن واحد. لكن بعض الاصدارات القديمة للمتصفحات لاتزال تطلبهم. ولأفضل توافقية استخدم تلك الأقواس. لدينا خياران مع صيغة نقاش وهما: readwrite وreadonly. يسمح لنا readwrite باسترجاع وإضافة وتحديث أو حذف الصفوف. وبكل الأحوال للحفاظ على سلامة البيانات يمكن استخدام readwrite مرة واحدة. لاسترجاع الصفوف لعرضها يمكنك فقط استخدام readonly. يمكن تشغيل أكثر من readonly مرة واحدة في نفس تخزين الكائن والذي بذلك يساعد على تحسين الأداء وبما أننا نضيف صفوف يجب أن نستخدم الصيغة readwrite. الخطوة الثالثة وهي اختيار أي كائن سنستخدمه في طلب الأمر: transaction.objectStore('tasks');أخيراً يكتب الأمر (request = objectstore.add(entry طريقة ادخال الكائن إلى قاعدة البيانات. سيتم استدعاء الدالة displaytasks عند اكتمال العملية. لإضافة صفوف متعددة يمكنك استدعاء الطريقة ()add مرات عديدة باستخدام نفس طلب الكائن. request = objectstore.add({object1:'Test object 1'}); request = objectstore.add({object2:'Test object 2'}); request = objectstore.add({object3:'Test object 3'});في هذه الحالة سيتم طرد الأحداث success وcomplete فور اكتمال جميع عمليات add وput عوضاً عن طردهم واحداً في كل مرة. تحديث الصف / التسجيليجب استخدام الطريقة put لجميع عمليات التحديث متضمنة مفتاح النقاش. ترك تلك الطريقة سيقوم بإنشاء صف جديد ولكن هذا ما لا نريده هنا. يستخدم تطبيقنا نفس المظهر في كلاً من عمليات الإضافة والتعديل على المهام. سنقوم باستخدام الدالّة addnewhandler للتعامل مع عمليات الإضافة والتعديل. فقط نحتاج تعديلها للتحرير بإضافة كود شرطي. يكون حقل key خالياً في النموذج لهذا سنقوم بإضافة صف جديد. و إذا كان يحمل أي قيمة سنقوم بالتعديل باستخدام put. addnewhandler = function (evt) { evt.preventDefault(); var entry = {}, transaction, objectstore, request, fields = evt.target, o; for (o in fields) { if ( fields.hasOwnProperty(o)) { entry[o] = fields[o].value; } } transaction = dbobject.transaction(['tasks'], 'readwrite'); objectstore = transaction.objectStore('tasks'); // Save the entry object with a key if one is available. if(fields.key.value){ // +fields.key.value converts our key to a number request = objectstore.put(entry, +fields.key.value); } else { request = objectstore.add(entry); } transaction.oncomplete = function (evt) { displaytasks(dbobject); }; transaction.onerror = errorhandler; };استرجاع الصفوفللتعديل على الصفوف تحتاج أولاً لاسترجاعها. وهنا يمكننا استخدام الطريقة get. سيتم إدراج الحدث hashchange عند الضغط على قائمة المهام. سنقوم الآن بتعريف الدالّة hashchangehandler لإستعادة العناصر المطابقة. hashchangehandler = function (evt) { var transaction, objectstore, request, key; if (window.location.hash) { // Extract digit characters from the hash, and convert to a number. // Generated IndexedDB keys are numbers. String values won’t work. key = +window.location.hash.match(/\d/g).join(''); // Run a read-only transaction on this object store. transaction = dbobject.transaction(['tasks'], 'readonly'); objectstore = transaction.objectStore('tasks'); // Retrieve the record by its key request = objectstore.get(key); // If it’s successful, update our form fields. request.onsuccess = function (successevent) { var o, data = successevent.target.result; for(o in data) { if( o == 'status') { addnew.status.checked = !!data.status; } addnew[o] = data[o]; } }; transaction.oncomplete = function (evt) { hide('#tasklist'); show('#addnew'); } };تسترجع الطريقة get الصفوف. كما أنها تقبل المُعامل (parameter) المفرد، أي سيتم استرجاع المفتاح كما أننا قمنا باستعادة الصف من خلال صيغة العمليات من خلال استخدام readonly. ملاحظة: تعتبر مفاتيح IndexedDB محددة من خلال النوع كما أن مولد المفاتيح هو عيارة عن أرقام. في حال قمت بتخطي get حتى ولو كان رقمي فإنه لن يعمل. ستحتاج إلى تحويل المُعامل إلى رقم كما فعلنا سابقاً. إذا كان الطلب ناجحاً سنقوم بتزويد النموذج addnew# بالنتيجة لعملية get. يجب تعريف الحدث onsuccess في أي وقت عند ظهور نتيجة الطلب. لقد قمنا أيضاً بتعريف oncomplete والذي سيتم إدراجه في حال اكتمال العملية. تنتهي العملية دوماً في حال فشل الطلب. حذف الصفسنقوم باستخدام طريقة مألوفة في حذف الصف كما فعلنا سابقاً مع استرجاع البيانات. ولكن هذه المرة سنقوم بحذف البيانات باستخدام الطريقة delete. تتطلب الطريقة delete مُعاملا واحدًا، ومفتاح الكائن سيقوم بحذف التخزين. أيضاً نحتاج لفتح عملية جديدة من خلال الصيغة readwrite. deletehandler = function (evt) { var transaction, objectstore, request, key; if (window.location.hash) { // Retrieve the key from the hash and convert it to a number key = +window.location.hash.match(/\d/g).join(''); // Perform the transaction transaction = dbobject.transaction(['tasks'], 'readwrite'); objectstore = transaction.objectStore('tasks'); request = objectstore.delete(key); // Recreate the task list display transaction.oncomplete = function (evt) { tbody.innerHTML = ''; displaytasks(dbobject); }; transaction.onerror = errorhandler; } };لقد عرفنا هنا المعالج oncomplete للكائن transaction وبما أن delete لن يقدم نتيجة بعد. حذف الصفوف و مولّد المفاتيحكما في قواعد البيانات التقليدية فإن حذف الصف لا يعيد القيمة إلى مولد المفاتيح . يمكن أن ترى في الصورة 3 أن لدينا 16 صفًا في قاعدة البيانات. كما أن أعلى قيمة للمفتاح هي 30. الصورة 3: لم يتم إعادة ضبط المفتاح بعد حذف الصف من قاعدة البيانات من الممكن إعادة استخدام المفتاح للصف المحذوف. حيث يمكنك تخطي المفتاح المطلوب كما في المناقشة الثانية للعمليات add أو put. استخدام المؤشر لاستعادة صفوف متعددةتعتبر عملية استرجاع مجموعة من الصفوف مختلفة نوعاً ما. لهذا نحتاج إلى استخدام المؤشر. المؤشرات هي تقنيات عابرة تستخدم لإعادة مجموعة صفوف في قاعدة البيانات (المؤشرات معرفة تلقائياً من IndexedDB). يُبقي المؤشر امتداد العمليات المتزامنة حيث يتحرك بترتيب تصاعدي أو تنازلي معتمداً على وجهة فتح المؤشر. تستخدم المؤشرات عملية الدوران while. دعونا نلقي نظرة على كيفية استعادة مجموعة من النتائج مع المؤشر: var displaytasks = function (database) { var transaction, objectstore, request; transaction = dbobject.transaction(['tasks'], 'readonly'); objectstore = transaction.objectStore('tasks'); request = objectstore.openCursor(IDBKeyRange.lowerBound(0), 'next'); request.onsuccess = function (successevent) { var task, tbody = document.querySelector('#list tbody'); if (request.result) { task = buildtask(request.result); tbody.appendChild(task); cursor.continue(); // advance to the next result } } }مجدداً، قمنا بإنشاء عمليات للكائن واختيار تخزين الكائن. مافعلناه مختلفًا نوعاً ما لكن لفتح كائن المؤشر يمكن استخدام الطريقة openCursor. تقبل الطريقة openCursor مُعاملين، كلاهما اختياري: المدى range: يجب أن يكون إما مفتاح أو مفتاح مدى.الاتجاه direction: يجب أن يكون واحداً من الطرق 'next' أو 'prev' أو 'nextunique' أو 'prevunique'.إنشاء مدىلإنشاء مفتاح مدى نحتاج إلى استخدام الواجهة IDBKeyRange وجميع الطرق التي فيها ثابتة. IDBKeyRange.lowerBound: يحدد مفتاح منخفض فقط.IDBKeyRange.upperbound: يحدد مفتاح مرتفع فقط.IDBKeyRange.bound: يحدد مفتاح منخفض و مرتفع.IDBKeyRange.only: يوافق على قيمة واحدة مشابهة تماماً لعملية get.لقد قمنا بتعيين أصغر قيمة مقيدة وهي الصفر باستخدام IDBKeyRange.lowerBound ولم نقم بتعيين القيمة العليا. كل صف في قيمة المفتاح والتي هي اكبر من صفر سيتم إرجاعها وذلك في كل صف في مخزن الكائن tasks ويتم ارجاع الأقدم أولاً. يعيد IDBKeyRange.upperBound جميع الكائنات مع قيمها والتي هي أقل مما هي عليه في المُعامل. على سبيل المثال يقوم (IDBKeyRange.upperBound(20 بإعادة جميع الكائنات التي تحتوي على قيمة 20 أو أقل. يعيد IDBKeyRange.bound جميع قيم الكائنات المحددة المدى من أصغر تحديد الى الأعلى تحديداً. ولإعادة الصفوف بين 11 و20 على سبيل المثال نستخدم (IDBKeyRange.bound(11,20. لاتوجد أي طريقة تعيد عدد النتائج. و عوضاً عن ذلك تعيد المفاتيج ضمن المدى المحدد. دعونا نقول على سبيل المثال بأن مفاتيح تخزين الكائن هي 1, 2, 4, 8, 9, 11, 15, 16, 20, 21, 22,23. قد قمنا بحذف بعض الصفوف لذلك هناك فجوة في تسلسل المفاتيح. يعيد (IDBKeyRange.lowerBound(0 جميع الكائنات.يعيد (IDBKeyRange.lowerBound(10 جميع الكائنات للمفاتيح 11, 15, 16, 20, 21, 22 , 23.لا يعيد (IDBKeyRange.upperBound(0 أي كائن.يعيد (IDBKeyRange.bound(0,20 جميع الكائنات ماعدا 21, 22, 23.بما أن مدى المفاتيح مقيد ضمن النتيجة فإنه من الممكن إظهارهم من خلال مناقشة جديدة باستخدام open. يجب أن تكون القيمة منطقية كما أن القيمة الافتراضية هي false. لتخطي أول صف من النتيجة التي قمنا بها على سبيل المثال يمكن استخدام (IDBKeyRange.lowerBound(0, true. يختلف الأمر IDBKeyRange.bound عن الآخرين إذ أنه يقبل مُعاملين إضافيين هُما: lowerOpen وupperOpen. إذا أردنا عرض أول وآخر نتيجة من IDBKeyRange.bound يجب علينا جعل القيمة صحيحة true مرتين: (IDBKeyRange.bound(0, 10, true, true. اختيار وجهة المؤشريُشير المُعامل الثاني openCursor إلى وجهة المؤشر وأين يجب أن يتجه. باستخدام next يعني بأن الصفوف سيتم ترتيبها حسب المفتاح بترتيب تصاعدي. أيضاً يمكننا إنشاء مفتاح مزدوج باستخدام nextunique وprevunique وتعتبر هذه الطريقة مفيدة جداً عند العمل مع الخاصيات المفهرسة. إضافة علامةقمنا سابقاً باستعادة المُدخلات من خلال المفتاح ومدى المفتاح. ولكن لأجل عملية قائمة المهام يجب استعادة وترتيب الصفوف من خلال اسم المهمة والأولوية والتاريخ المستحق والحالة. ملاحظة: لاتدعم IndexedDBShim فتح أو فهرسة المؤشرات. ستحتاج إلى وسلية أخرى لفعل ذلك. فكر بالعلامات المفهرسة بطريقة سريعة ورتّب صفوفك في قاعدة البيانات. تسمح لك العلامات بمشاهدة الكائنات من خلال خصائصهم فضلاً عن مفاتيحهم. لإضافة علامة لمخزن الكائن سنتحتاج الى استخدام طريقة createIndex. ولأن إضافة العلامة تقوم بتغيير البنية سنحتاج إلى فعل ذلك يدوياً من خلال الحدث versionchange واستخدام onupgradeneeded أيضا. دعنا نقوم بتحديث الدالّة onupgradeneeded: idb.onupgradeneeded = function (evt) { var tasks, transaction; dbobject = evt.target.result; if (evt.oldVersion < 1) { tasks = dbobject.createObjectStore('tasks', {autoIncrement: true}); // Create indexes on the object store. transaction = evt.target.transaction.objectStore('tasks'); transaction.createIndex('by_task', 'task'); transaction.createIndex('priority', 'priority'); transaction.createIndex('status', 'status'); transaction.createIndex('due', 'due'); transaction.createIndex('start', 'start'); } };تقبل الطريقة createIndex ثلاث مُعامِلات: Name (مطلوب): اسم العلامة التي تريد اضافتها.keyPath (مطلوب): خاصية الكائن.optionalParameters: وهو قاموس يحتوي على الضبط يتكون من unique و/أو multiEntry للعلامة.أضفنا علامة مفهرسة للحقول task, priority, status, start, due. يمكن أن تشارك هذه العلامات نفس الاسم و الخاصية التي تحملها. يتم إدخال مخزن العلامات فقط للكائنات التي تحتوي خاصيات الفهرسة. الصورة 4: انظر إلى علامة by_task في الجدول في متصفح Opera عند تغيير خاصية الصف عندها ستصبح تلك الخاصيات أكثر مرونة في الجدول، دعنا نلقي نظرة على الصفوف الأساسية في الفهرس. سنقوم بتحديث الدالّة displaytasks من الأعلى. استعادة الصفوف من خلال علاماتهمسنقوم بفتح المؤشر لمخزن الكائن من خلال النسخة السابقة من الدالّة displaytasks. وهنا سنحتاج لإضافة سطر جديد والذي سيعيد الفهرس by_task. ثم سنقوم باستدعاء openCursor كما في الشِفرة البرمجية التالية: var displaytasks = function (database) { var transaction, objectstore, request, index; transaction = dbobject.transaction(['tasks'], 'readonly'); objectstore = transaction.objectStore('tasks'); // New line to select the index index = objectstore.index('by_task'); // Our request opens a cursor on the index, // rather than the object store. request = index.openCursor(IDBKeyRange.lowerBound(0), 'next'); request.onsuccess = function (successevent) { var task, tbody = document.querySelector('#list tbody'); if (request.result) { task = buildtask(request.result); tbody.appendChild(task); cursor.continue(); } }ترتّب أيضاً العلامات القيم لكل خاصية. ستعيد الدالّة displaytasks قائمة المهام مُرتبة أبجدياً. محدودية العلاماتللأسف تفتقر IndexedDB لنوع البحث الذي يدعم جميع النصوص والذي يمكن أن تجده في قواعد البيانات SQL مثل MySQL وPostgreSQL. وعوضاً عن ذلك سنقوم بفلترة جميع النتائج لدينا باستخدام تعابير النمطية. دعنا نشاهد المثال الذي يستخدم نموذج بحث. سنقوم بانتزاع قيمة النموذج واستخدام تعبير نظامي عند الضغط على ارسال أمر البحث. ثم سنقوم باختبار المهمة للمطابقة. var searchhandler, search = document.getElementById('search'); searchhandler = function (evt) { evt.preventDefault(); var transaction, objectstore, index, request; transaction = dbobject.transaction(['tasks'], 'readwrite'); objectstore = transaction.objectStore('tasks'); index = objectstore.index('by_task'); request = index.openCursor(IDBKeyRange.lowerBound(0), 'next'); request.onsuccess = function (successevent) { var reg, cursor, task; reg = new RegExp(evt.target.find.value, "i"); cursor = request.result; if (cursor !== null) { if (reg.test(cursor.value.task)) { task = buildtask(cursor); } cursor.continue(); } } } search.addEventListener('submit', searchhandler); الصورة 5 : فلترة المهام من خلال استخدام تعبير بسيط للبحث تحتوي التعابير التي تقوم بالبحث على محدوديات. وبكل الأحوال البحث عن كلمة cafe لن يُطابق الكلمة café لأن الحرفان الأخيران في كلا الكلمتين غير متشابهين. و هنا يمكنك استخدام تقنية بسيطة لتخطي هذه المشكلة باستخدام تعبير نمطي ويقوم بالبحث تلقائياً عن caf فقط. الخاتمةتقدم IndexedDB قدرة قاعدة البيانات الأساسية للمتصفح، كما تجعل بناء تطبيقات ويب تعمل في حالة الإتصال (online) وفي حالة الانفصال (offline) مُمكنا. يمكنك الاطلاع أكثر على هذا النوع من قواعد البيانات لأنه سيحسن كثيراً لديك فكرة عملهم. في حال أردت تعلم أكثر حول المُدخلات والمخرجات لقاعدة البيانات IndexedDB. يمكنك قراءة تخصيصات IndexedDB. و هذا غير كافي للقراءة لأنه فقط مرجع تعريفي. يقدم أيضًا مطوروا شركة Mozilla بعض الشروحات حول الفكرة من وراء IndexedDB في حال كانت الفكرة من ورائها لا تزال مُبهمة لديك. ترجمة -وبتصرّف- للمقال: An Introduction to IndexedDB لصاحبته: Tiffany Brown.
×
×
  • أضف...