أفضل الممارسات لاستخدام IndexedDB

تعرف على أفضل الممارسات لمزامنة حالة التطبيق بين IndexedDB ومكتبات إدارة حالة شائعة.

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

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

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

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

توفير إمكانية توقّع تطبيقك

تنشأ الكثير من التعقيدات حول أداة IndexedDB من حقيقة وجود عدد كبير من العوامل التي لا يمكن لك (مطوّر البرامج) التحكم فيها. يستكشف هذا القسم العديد من المشكلات التي يجب أن تضعها في اعتبارك عند العمل باستخدام IndexedDB.

لا يمكن تخزين كل شيء في IndexedDB على جميع الأنظمة الأساسية

في حال تخزين ملفات كبيرة من إنشاء المستخدمين مثل الصور أو الفيديوهات، يمكنك محاولة تخزينها في شكل عناصر File أو Blob. سيعمل ذلك على بعض الأنظمة الأساسية لكنه لا ينجح على أنظمة أخرى. لا يمكن لمتصفِّح Safari على iOS على وجه التحديد تخزين Blob في IndexedDB.

لحسن الحظ ليس من الصعب جدًا تحويل Blob إلى ArrayBuffer، والعكس صحيح. إنّ تخزين ArrayBuffer في IndexedDB مدعوم للغاية.

مع ذلك، تذكَّر أنّ Blob يتضمّن نوع MIME بينما لا يتوفّر للنوع ArrayBuffer. ستحتاج إلى تخزين النوع إلى جانب المورد الاحتياطي لإجراء التحويل بشكل صحيح.

لتحويل ArrayBuffer إلى Blob، يمكنك ببساطة استخدام الدالة الإنشائية Blob.

function arrayBufferToBlob(buffer, type) {
  return new Blob([buffer], { type: type });
}

أما الاتجاه الآخر فهو أكثر تعقيدًا قليلاً، ويُعد عملية غير متزامنة. يمكنك استخدام كائن FileReader لقراءة الكائن الثنائي الكبير كـ ArrayBuffer. عند الانتهاء من القراءة، يتمّ تشغيل حدث loadend على القارئ. يمكنك إكمال هذه العملية باستخدام Promise على النحو التالي:

function blobToArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('loadend', () => {
      resolve(reader.result);
    });
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(blob);
  });
}

قد تتعذّر الكتابة في وحدة التخزين

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

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

يمكنك رصد الأخطاء في عمليات IndexedDB عن طريق إضافة معالج أحداث للحدث error عند إنشاء كائن IDBDatabase أو IDBTransaction أو IDBRequest.

const request = db.open('example-db', 1);
request.addEventListener('error', (event) => {
  console.log('Request error:', request.error);
};

يُحتمَل أنّ المستخدم قد عدَّل أو حذف البيانات المخزَّنة.

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

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

قد تكون البيانات المخزنة قديمة

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

تتوافق أداة IndexedDB مع إصدارات المخططات والترقية من خلال طريقة IDBOpenDBRequest.onupgradeneeded()، ولكن لا يزال عليك كتابة رمز الترقية بطريقة تتيح له معالجة المستخدم في الإصدارات السابقة (بما في ذلك الإصدار الذي يحتوي على خطأ).

قد تكون اختبارات الوحدات مفيدة للغاية هنا، لأنّه لا يكون من الممكن غالبًا إجراء اختبار يدوي لجميع مسارات الترقية والحالات المحتملة.

الحفاظ على أداء تطبيقك

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

وكقاعدة عامة، يجب ألا تكون قيمة القراءة والكتابة في IndexedDB أكبر مما هو مطلوب للوصول إلى البيانات.

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

يطرح ذلك بعض التحديات عند التخطيط لكيفية الاحتفاظ بحالة التطبيق في IndexedDB، حيث تعمل معظم مكتبات إدارة الحالات الشائعة (مثل Redux) من خلال إدارة شجرة الحالة بالكامل ككائن JavaScript واحد.

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

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

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

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

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

الاستنتاجات

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

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

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