أشرنا في المقال السابق إلى إمكانية استخدام قاعدة البيانات المدمجة في المتصفح IndexedDB في تخزين ما هو أعقد من النصوص واﻷرقام، بل يتعداها إلى إمكانية تخزين أي شيئ تريده بما في ذلك الكائنات ذات البنى المعقدة مثل بيانات الفيديوهات والصور الخام. مع ذلك، ليس من الصعب تخزين واسترجاع هذه البيانات مقارنة بغيرها من البيانات التي تعاملنا معها.
ولتوضيح هذا اﻷمر، سنطور مثالًا تطبيقيًا باسم IndexedDB video store بإمكانك الاطلاع على عمله مباشرة. ينزّل هذا التطبيق عند تشغيله جميع مقاطع الفيديو من الشبكة ويخزنها في قاعدة البيانات IndexedDB، ويعرض بعدها هذه المقاطع في واجهة المستخدم ضمن العنصر <video>
، وعندما تشغّل التطبيق في المرات القادمة، سيجد التطبيق المقاطع ضمن قاعدة البيانات ويعرضها بدلًا من تنزيلها مجددًا مما يجعل العملية أسرع، ويوفّر استهلاك حزمة البيانات المتاحة للاتصال باﻹنترنت.
تطبيق عملي: تخزين فيديو في قاعدة البيانات IndexedDB
سنعرض تاليًا اﻷجزاء اﻷكثر أهمية في تطبيقنا، ولن نستعرض كل التفاصيل طبعًا، فالكثير منها مشابه تمامًا لما غطيناه في المقال السابق، إضافة إلى وجود تعليقات كافية ضمن الشيفرة توضح خطوات العمل.
نخزن بداية أسماء مقاطع الفيديو التي نريد إحضارها ضمن مصفوفة كائنات كما يلي:
const videos = [ { name: "crystal" }, { name: "elf" }, { name: "frog" }, { name: "monster" }, { name: "pig" }, { name: "rabbit" }, ];
ننفذ الدالة ()init
عند نجاح الاتصال بقاعدة البيانات. ووظيفة هذه الدالة التنقل بين أسماء مقاطع الفيديو السابقة ومحاولة إيجاد سجل يوافق اسم المقطع ضمن قاعدة بيانات الفيديو. فإن وجد مقطع الفيديو ستكون نتيجة request.result
هي true
وإلا ستكون undefined
. ستمرر الدالة بعد ذلك اسم المقطع إن وجد إلى الدالة ()displayVideo
لتضعه ضمن واجهة المستخدم وإلا تستدعي الدالة ()fetchVideoFromNetwork
ﻹحضار المقطع من اﻹنترنت.
function init() { //تنقل بين أسماء مقاطع الفيديو واحدًا تلو اﻵخر for (const video of videos) { // افتح الاتصال مع قاعدة البيانات واحصل على مخزن الكائنات وكل فيديو فيه const objectStore = db.transaction("videos_os").objectStore("videos_os"); const request = objectStore.get(video.name); request.addEventListener("success", () => { // إن وجد المقطع ضمن قاعدة البيانات if (request.result) { //displayVideo احضر المقطع واعرضه على الواجهة باستخدام الدالة console.log("taking videos from IDB"); displayVideo( request.result.mp4, request.result.webm, request.result.name, ); } else { // احضر مقطع الفيديو من الشبكة fetchVideoFromNetwork(video); } }); } }
تحضر الدالة ()fetchVideoFromNetwork
مقاطع فيديو من النوعين MP4 و WebM باستخدام الطلب ()fetch
، بعدها سنستخدم التابع ()response.blob
لاستخلاص جسم كل طلب على شكل كائن بيانات ثنائية blob والذي يعطي كائنًا يمثل مقطع الفيديو، ويمكن تخزينه وعرضه لاحقًا. أما المشكلة التي تواجهنا هنا، أن هذين الطلبين غير متزامنين، لكن ما نريده فعلًا هو عرض أو تخزين المقطع فقط عندما يكتمل الوعد promise. لهذا نستخدم التابع ()promise.all
الذي يقبل معاملًا واحدًا وهو مصفوفة مراجع إلى كل الوعود التي تريد التحقق من إكتمالها، ويعيد وعدًا يتحقق عندما تتحقق كل الوعود في المصفوفة.
نستدعي ضمن التابع ()then
المتعلق بهذا الوعد الدالة ()displayVideo
كما فعلنا سابقًا لعرض مقطع الفيديو، ثم نستدعي أيضًا الدالة ()storeVideo
لتخزين المقطع في قاعدة البيانات:
// fetch() إحضار مقاطع الفيديو باستخدام الدالة // blob تحويل أجسام الاستجابات إلى كائن const mp4Blob = fetch(`videos/${video.name}.mp4`).then((response) => response.blob(), ); const webmBlob = fetch(`videos/${video.name}.webm`).then((response) => response.blob(), ); // نفّذ الشيفرة التالية إن تحقق كلا الوعدان Promise.all([mp4Blob, webmBlob]).then((values) => { //displayVideo() اعرض الفيديو الذي أحضرته من الإنترنت باستخدام الدالة displayVideo(values[0], values[1], video.name); //storeVideo() خزن مقطع الفيديو في قاعدة البيانات باستخدام storeVideo(values[0], values[1], video.name); });
-
يشبه عمل الدالة
()storeVideo
ما رأيناه في المقال السابق عندما أضفنا بيانات إلى قاعدة البيانات، إذ نفتح قناة العملياتreadwrite
مع القاعدة ونتخذ مرجعًا إلى مخزن الكائنvideo_os
ثم ننشئ كائنًا يمثل السجل الذي نريد إضافته إلى القاعدة ونستخدم بعدها التابع()IDBObjectStore.add
:
// storeVideo() تعريف الدالة function storeVideo(mp4, webm, name) { // فتح قناة اتصال قراءة وكتابة مع قاعدة البيانات const objectStore = db .transaction(["videos_os"], "readwrite") .objectStore("videos_os"); //Add() إضافة السجل إلى قاعدة البيانات باستخدام const request = objectStore.add({ mp4, webm, name }); request.addEventListener("success", () => console.log("Record addition attempt finished"), ); request.addEventListener("error", () => console.error(request.error)); }
-
تُنشئ الدالة
()displayVideo
عناصر شجرة DOM اللازمة لإدراج مقطع الفيديو في واجهة المستخدم ومن ثم تلحق هذه العناصر بالصفحة. أما النقاط اﻷكثر أهمية، فهي التي نستعرضها تاليًا.
لعرض كائن البيانات الثنائية الذي يضم الفيديو داخل العنصر<video>
، لا بد من إنشاء كائن عنوان URL أي عناوين داخلية تشير إلى كائن البيانات الثنائية المخزن في الذاكرة باستخدام التابع()URL.creatObjectURL
. بعدها يمكننا أن نجعل تلك العناوين قيمًا للسماتsrc
العائدة للعناصر<source>
ويعمل عندها كل شيء كما هو متوقع:
//displayVideo() تعريف الدالة function displayVideo(mp4Blob, webmBlob, title) { //blob يشير إلى الكائن URL إنشاء كائن const mp4URL = URL.createObjectURL(mp4Blob); const webmURL = URL.createObjectURL(webmBlob); //لإدراج الفيديو في الصفحة DOM إنشاء عنصر في شجرة const article = document.createElement("article"); const h2 = document.createElement("h2"); h2.textContent = title; const video = document.createElement("video"); video.controls = true; const source1 = document.createElement("source"); source1.src = mp4URL; source1.type = "video/mp4"; const source2 = document.createElement("source"); source2.src = webmURL; source2.type = "video/webm"; //في الشجرة DOM إدراج عنصر section.appendChild(article); article.appendChild(h2); article.appendChild(video); video.appendChild(source1); video.appendChild(source2); }
تخزين اﻷصول للعمل دون اتصال بالشبكة
عرضنا في المثال السابق طريقة إنشاء تطبيق يُخزّن أصولًا assets في قاعدة البيانات IndexedDB حتى لا نضطر إلى تحميلها مجددًا. ويحسن هذا اﻷمر تجربة المستخدم بشكل ملحوظ. لكن لا تزال بعض الأصول المهمة مفقودة كي يعمل التطبيق وهي ملف HTML الرئيسي وملفات CSS وجافا سكريبت، ولا بد من تنزيلها في كل مرة ندخل فيها إلى الموقع، وبالتالي لن يعمل التطبيق بدون الاتصال باﻹنترنت. وهنا يأتي دور عمّال الخدمة service workers والواجهة البرمجية cache API
يُعرف عامل الخدمة service worker في جافا سكريبت على أنه ملف يُسجّل تحت مصدر محدد مثل موقع ويب أو جزء من موقع ويب في نطاق معين عندما يلج إليه متصفح ويب. ويتمكن هذا الملف لكونه مسجلًا على نطاق ما أن يتحكم بالصفحات التي تنتمي إلى نفس اﻷصل أو النطاق. ويتوضع هذا الملف في مكان وسط بين الصفحة التي اكتمل تحميلها وشبكة اﻹنترنت ويعترض طلبات الشبكة التي تحوّل من وإلى ذلك المصدر أو اﻷصل.
وعندما يعترض العامل الطلب سيكون بمقدوره تنفيذ أي شيئ تريده على هذا الطلب، لكن استخدامه النمطي هو تخزين الاستجابات على طلبات الشبكة والاستجابة لهذا الطلبات في المرات القادمة بدلًا من الاتصال بالشبكة والحصول على الاستجابة. وكنتيجة ستتمكن من بناء صفحة ويب تعمل كليًا دون اتصال بشبكة اﻹنترنت.
أما الواجهة البرمجية Cache API فهي آلية أخرى لتخزين البيانات في طرف العميل، مع اختلاف بسيط هو أنها مخصصة لتخزين الاستجابات على طلبات HTTP، لهذا ستعمل جيدًا مع عمال الخدمة service workers.
مثال عن عمال الخدمة
لنطرح مثالًا يوضح قليلًا الفكرة السابقة. إذ أنشانا نسخة أخرى من مثال تخزين ملفات الفيديو الذي فصلناه في الفقرة السابقة. وتعمل هذه النسخة بنفس اﻷسلوب ما عدا أنها تخزّن أيضًا ملفات HTML و CSS وجافا سكريبت ضمن Cache API من خلال عامل خدمة وبالتالي سيعمل المثال دون اتصال باﻹنترنت.
بإمكانك تجريب هذه النسخة مباشرة على جيت-هاب والاطلاع على الشيفرة المصدرية أيضًا.
تسجيل عامل الخدمة
أول ما تلاحظه هو وجود شيفرة إضافية في ملف جافا سكريبت. تختبر هذه الشيفرة بداية وجود العضو serviceWorker
ضمن الكائن Navigator
. فإن كان موجودًا (أعادت الشيفرة القيمة true
)، نعلم حينها وجود دعم أساسي لعمال الخدمة في المتصفح. نستخدم التابع ()ServiceWorkerContainer.register
لتسجيل عامل الخدمة الموجود في الملف sw.js
على المصدر الذي يتواجد فيه، وبالتالي سيكون قادرًا على التحكم بالصفحات الموجودة في نفس المجلد أو المجلدات الفرعية. وعندما يتحقق الوعد سيكون عامل الخدمة قد سُجِّل:
// تسجيل عامل الخدمة لتتمكن من تشغيل المثال دون اتصال بالشبكة if ("serviceWorker" in navigator) { navigator.serviceWorker .register( "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js", ) .then(() => console.log("Service Worker Registered")); }
ملاحظة: يُعطى مسار الملف sw.js
بالنسبة إلى أصل الموقع، وليس بالنسبة إلى ملف جافا سكريبت الذي يضم الشيفرة. فالعامل موجود على العنوان:
https://mdn.github.io/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js
بينما عنوان اﻷصل هو
https://mdn.github.io
لذا لابد أن يكون العنوان المعطى عند التسجيل هو:
/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js
وإذا أردت استضافة هذا المثال على حاسوبك محليًا، لا بد من تبديل هذه القيم بما يناسب. وعلى الرغم من أنه أمر مربك، لكنه ضروري لأسباب أمنية.
تثبيت عامل الخدمة
في كل مرة ندخل فيها إلى أحد الصفحات التي تقع تحت سيطرة عامل الخدمة، يُثبَّت عامل الخدمة على هذه الصفحة أي يبدأ العامل بالتحكم فيها. وعندما يحدث ذلك، يقع الحدث install
على عامل الخدمة، وستتمكن من كتابة الشيفرة ضمن عامل الخدمة نفسه لتستجيب إلى حدث التثبيت.
وإذا ألقينا نظرة على الملف sw.js
سنجد أن مترصد حدث التثبيت مسجّل وفق القيمة self
أي على العامل نفسه. والتعليمة self
طريقة لتشير إلى الطبيعة العامة global scope لعامل الخدمة من داخل ملف عامل الخدمة.
نستخدم ضمن دالة المعالج install
التابع ()ExtendableEvent.waitUntil
العائد إلى كائن الحدث لكي يبلغ المتصفح بعدم تثبيت العامل قبل أن يُنجز الوعد بنجاح.
وهنا نجد طريقة عمل الواجهة Cache API الخاصة بعملية التخزين، إذ نستخدم التابع ()CacheStorage.open
لفتح كائن تخزين مؤقت جديد cache object لنخزّن ضمنه الاستجابات. وعندما يتحقق الوعد، سيعيد كائن cache
يمثل المخزن المؤقت للفيديو video-store
. نستخدم بعد ذلك التابع ()Cache.addAll
ﻹحضار سلسلة اﻷصول واستجاباتها إلى المخزن المؤقت:
self.addEventListener("install", (e) => { e.waitUntil( caches .open("video-store") .then((cache) => cache.addAll([ "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/", "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.html", "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.js", "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/style.css", ]), ), ); });
وهكذا تنتهي عملية التثبيت.
الاستجابة إلى طلبات أخرى
مع تسجيل عامل الخدمة ليتحكم بصفحة HTML وإضافة كل اﻷصول إلى المخزن المؤقت، سيكون التطبيق جاهزًا للعمل. لكن بقي هناك شيء واحد وهو كتابة شيفرة تستجيب إلى طلبات HTML اﻷخرى التي ترد، وهذا ما يفعله القسم الثاني من الشيفرة في الملف sw.js
.
نضيف مترصدًا آخر عامًا إلى عامل الخدمة، يعمل على تنفيذ معالج الحدث عندما يقع حدث اﻹحضار fetch
. ويقع هذا الحدث في كل مرة يحاول فيها المتصفح إجراء طلب لأحد اﻷصول الموجودة في نفس المجلد الذي يضم عامل الخدمة.
نسجّل بداية ضمن دالة المعالج عنوان URL للأصل المطلوب، ثم نهيئ استجابة مخصصة لهذا الطلب باستخدام التابع ()FetchEvent.respondWith
. وضمن كتلة التابع السابق نستخدم التابع ()CacheStorage.match
للتحقق من وجود طلب موافق لهذا اﻷصل ضمن المخزن المؤقت. ويتحقق الوعد الموافق لهذا الطلب في حال وجود عنوان في المخزن يطابق عنوان URL للطلب وإلا سيعيد الوعد القيمة undefined
. نعيد بعد ذلك الطلب على شكل استجابة مخصصة في حال كان العنوان موجودًا وإلا نستخدم الواجهة ()fetch
ﻹحضاره من الشبكة كونه غير موجود في المخزن المؤقت:
self.addEventListener("fetch", (e) => { console.log(e.request.url); e.respondWith( caches.match(e.request).then((response) => response || fetch(e.request)), ); });
وهكذا نستخدم عامل الخدمة، علمًا أن استخداماته أوسع من ذلك ولا يسعنا تغطيها في هذا المقال.
اختبار المثال دون اتصال بالشبكة
لاختبار عمل تطبيقنا، لا بد من تحميله عدة مرات للتأكد من تثبيت عامل الخدمة، وبعدها يمكنك:
- قطع الاتصال باﻹنترنت.
- اختيار العمل دون اتصال (اختر ملف ثم العمل دون اتصال إن كنت تستخدم فايرفوكس).
- الانتقال إلى أدوات مطوري ويب واختر تطبيقات ثم عمال الخدمة Service workers، ولا بد من تفعيل الخيار offline إن كنت تستخدم متصفح كروم.
لو حاولت اﻵن تحديث الصفحة سترى أنها ستعيد التحميل دون أية مشكلات لأن كل أصول الصفحة قد خُزّنت في المخزن المؤقت cache، كما أن كل مقاطع الفيديو مخزنة ضمن قاعدة البيانات IndexedDB.
الخلاصة
تعرفنا في هذا المقال على طريقة لتشغيل صفحة ويب دون اتصال باﻹنترنت عن طريقة استخدام عامل خدمة service worker مع الواجهة البرمجية Cache
التي تخزّن الأصول ضمن مخازن مؤقتة في حاسوبك.
ترجمة -وبتصرف- للجزء الثالث من مقال: Client-side storage
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.