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

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

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

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

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

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

الحفاظ على إمكانية توقّع تطبيقك

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

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

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

لحسن الحظّ، ليس من الصعب تحويل Blob إلى ArrayBuffer، والعكس صحيح. وتخزين ArrayBuffers في 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 بشكل صحيح إلى تحسين تجربة المستخدم إلى حد كبير، إلا أن استخدامه بشكل غير صحيح أو عدم التعامل مع حالات الأخطاء يمكن أن يؤدي إلى تعطّل تطبيقات وإلى شعور المستخدمين بعدم الرضا.

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