كيف يسمح تطبيق Kiwix PWA للمستخدمين بتخزين وحدات غيغابايت من البيانات من الإنترنت لاستخدامها بلا اتصال بالإنترنت

أشخاص يجتمعون حول كمبيوتر محمول واقف على طاولة بسيطة مع كرسي بلاستيكي على اليسار تبدو الخلفية كمدرسة في بلد نامٍ.

تتناول هذه الدراسة الحالة كيفية استخدام Kiwix، وهي مؤسسة غير ربحية، لتكنولوجيا التطبيقات المتوافقة مع الويب المتطورة وFile System Access API لتمكين المستخدمين من تنزيل وحفظ أرشيفات الإنترنت الكبيرة لاستخدامها بلا إنترنت. تعرَّف على التنفيذ الفني لرمز المصدر الخاص بنظام الملفات الخاص (OPFS)، وهو ميزة جديدة للمتصفّح في تطبيق Kiwix المتوافق مع الأجهزة الجوّالة (PWA) تعمل على تحسين إدارة الملفات، وتوفير إمكانية وصول محسّنة إلى المحفوظات بدون طلبات الحصول على أذونات. تتناول المقالة التحديات وتسلّط الضوء على التطورات المستقبلية المحتملة في نظام الملفات الجديد.

لمحة عن Kiwix

بعد مرور أكثر من 30 عامًا على ولادة الويب، لا يزال ثلث سكان العالم في انتظار إمكانية الوصول إلى الإنترنت بشكل موثوق وفقًا لاتحاد الاتصالات الدولي. هل هذا هو مكان انتهاء القصة؟ بالطبع لا. طوّر فريق Kiwix، وهي منظمة غير ربحية مقرّها سويسرا، منظومة متكاملة من التطبيقات والمحتوى المفتوحَين المصدر بهدف إتاحة المعرفة للأشخاص الذين لا يمكنهم الوصول إلى الإنترنت أو يمكنهم الوصول إليه بشكل محدود. وتعتمد هذه الفكرة على أنّه إذا لم يكن بإمكان أنت الوصول إلى الإنترنت بسهولة، يمكن لشخص ما تنزيل الموارد الرئيسية نيابةً عنك في المكان والوقت اللذين يتوفّر فيهما الاتصال بالإنترنت، ثم تخزينها على الجهاز لاستخدامها لاحقًا بلا إنترنت. يمكن الآن تحويل العديد من المواقع الإلكترونية المهمة، مثل Wikipedia أو Project Gutenberg أو Stack Exchange أو حتى محادثات TED، إلى أرشيفات مضغوطة للغاية تُعرف باسم ملفات ZIM، ويمكن قراءتها على الفور باستخدام متصفّح Kiwix.

تستخدم أرشيفات ZIM ضغط Zstandard (ZSTD) الفعّال للغاية (كانت الإصدارات القديمة تستخدم XZ)، ويُستخدَم هذا الضغط في الغالب لتخزين HTML وJavaScript وCSS، في حين يتم عادةً تحويل الصور إلى تنسيق WebP المضغوط. يتضمّن كل ملف ZIM أيضًا عنوان URL وفهرسة العناوين. فالضغط هو المفتاح هنا، حيث يتم ضغط كل محتوى ويكيبيديا باللغة الإنجليزية (6.4 مليون مقالة بالإضافة إلى صورة) إلى 97 غيغابايت بعد التحويل إلى تنسيق ZIM، الذي يبدو كثيرًا إلى أن تدرك أن مجموع كل المعارف البشرية يمكن أن يتناسب الآن مع هاتف Android متوسط المواصفات. وتتوفّر أيضًا العديد من المراجع الأصغر حجمًا، بما في ذلك نُسخ مخصّصة من Wikipedia، مثل الرياضيات والطب وما إلى ذلك.

تقدّم Kiwix مجموعة من التطبيقات المتوافقة مع الأجهزة فقط التي تستهدف استخدام أجهزة الكمبيوتر المكتبي (Windows/Linux/macOS) والأجهزة الجوّالة (iOS/Android). ومع ذلك، ستتمحور دراسة الحالة هذه عن تطبيق الويب التقدّمي (PWA) الذي يهدف إلى أن يكون حلاً شاملاً وبسيطًا لأي جهاز مزوّد بمتصفّح حديث.

سنلقي نظرة على التحديات التي تواجه تطوير تطبيق ويب شامل يحتاج إلى توفير إمكانية الوصول السريع إلى أرشيفات المحتوى الكبيرة بلا إنترنت، وبعض واجهات برمجة تطبيقات JavaScript الحديثة، ولا سيما File System Access API وOrigin Private File System، التي توفّر حلولاً مبتكرة ومثيرة لهذه التحديات.

هل هو تطبيق ويب للاستخدام بلا إنترنت؟

يمثّل مستخدمو Kiwix مجموعة متنوعة لديهم العديد من الاحتياجات المختلفة، ولا تملك Kiwix سوى تحكّم بسيط أو لا تحكّم على الإطلاق في الأجهزة وأنظمة التشغيل التي سيتم من خلالها الوصول إلى المحتوى. قد تكون بعض هذه الأجهزة بطيئة أو قديمة، خاصةً في المناطق ذات الدخل المنخفض في العالم. على الرغم من أنّ Kiwix تحاول تغطية أكبر عدد ممكن من حالات الاستخدام، لاحظت المؤسسة أيضًا أنّه يمكنها الوصول إلى عدد أكبر من المستخدمين باستخدام أداة البرامج الأكثر عالمية على أي جهاز: متصفح الويب. لذلك، استنادًا إلى قانون أتوودف الذي ينص على أنّ أي تطبيق يمكن كتابته بلغة JavaScript سيتم كتابته باستخدامها في نهاية المطاف، بدأ بعض مطوّري Kiwix قبل 10 سنوات تقريبًا في نقل برنامج Kiwix من C++ إلى JavaScript.

كان الإصدار الأول من هذا المتوافق، المُسمى Kiwix HTML5، مخصّصًا لنظام التشغيل Firefox OS الذي تم إيقافه نهائيًا الآن ولإضافات المتصفّح. كان (ولا يزال) في الأساس محرك فك ضغط برمجيًا (XZ وZSTD) مكتوبًا بلغة C++ وتم تجميعه باستخدام لغة JavaScript المُعدّة للاستخدام على الويب (ASM.js) ثم Wasm أو WebAssembly باستخدام مجمّع Emscripten. لا تزال إضافات المتصفّح قيد تطوير فعال، وقد تم تغيير اسمها لاحقًا إلى Kiwix JS.

متصفّح Kiwix JS بلا إنترنت

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

إنّ تطبيقات الويب التقدّمية التي تعمل بلا إنترنت هي تطبيقات خفيفة للغاية، لذا فهي مثالية للسياقات التي يكون فيها الإنترنت الجوّال متقطّعًا أو باهظ التكلفة. التكنولوجيا المستخدَمة في هذا الإجراء هي Service Worker API وCache API ذات الصلة، والتي تستخدمها كل التطبيقات المستندة إلى مكتبة Kiwix JS. تسمح واجهات برمجة التطبيقات هذه للتطبيقات بالعمل كخادم، ويتمثل ذلك في رصد طلبات الجلب من المستند أو المقالة الرئيسيَين المعروضَين وإعادة توجيهها إلى الخلفيّة (JS) لاستخراج استجابة وإنشاءها من أرشيف ZIM.

مساحة تخزين في كل مكان

نظرًا للحجم الكبير لأرشيف ZIM، يشكّل التخزين والوصول إليه، خاصةً على الأجهزة الجوّالة، أكبر مشكلة تواجه مطوّري Kiwix. ينزِّل العديد من مستخدمي Kiwix النهائيين المحتوى داخل التطبيق، عندما يكون الإنترنت متاحًا، لاستخدامه لاحقًا بلا إنترنت. ينزِّل مستخدمون آخرون المحتوى على جهاز كمبيوتر باستخدام برنامج تورنت، ثم ينقلونه إلى جهاز جوّال أو جهاز لوحي، ويتبادل بعض المستخدمين المحتوى على فلاشات USB أو أقراص صلبة محمولة في المناطق التي تتوفر فيها خدمة إنترنت جوّال متقطّعة أو باهظة الثمن. يجب أن تتيح كل طرق الوصول إلى المحتوى من مواقع جغرافية عشوائية يمكن للمستخدم الوصول إليها بواسطة Kiwix JS وKiwix PWA.

إنّ واجهة برمجة التطبيقات File API هي التي سمحت في البداية لتطبيق Kiwix JS بقراءة أرشيفات ضخمة تبلغ مساحتها مئات غيغابايت (يبلغ حجم أحد أرشيفات ZIM 166 غيغابايت)، حتى على الأجهزة ذات الذاكرة المنخفضة. تتوفّر واجهة برمجة التطبيقات هذه بشكل عام في أي متصفّح، حتى في المتصفّحات القديمة جدًا، وبالتالي، تُستخدَم كخيار احتياطي عام في حال عدم توفّر واجهات برمجة تطبيقات أحدث. إنّ عملية تحديد عنصر input في HTML سهلة جدًا، كما هو الحال في Kiwix:

<input
 
type="file"
 
accept="application/octet-stream,.zim,.zimaa,.zimab,.zimac, ..."
 
value="Select folder with ZIM files"
 
id="archiveFilesLegacy"
 
multiple
/>

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

على الرغم من أنّ واجهة برمجة التطبيقات File API عالمية، إلا أنّها تتضمن عيوبًا جعلت تطبيقات Kiwix JS تبدو معقدة وقديمة مقارنةً بالتطبيقات الأصلية: فهي تتطلب من المستخدم اختيار الأرشيفات باستخدام أداة اختيار الملفات، أو السحب والإفلات لملف في التطبيق، في كل مرة يتم فيها تشغيل التطبيق، لأنّه لا تتوفّر من خلال واجهة برمجة التطبيقات هذه طريقة للحفاظ على أذونات الوصول من جلسة إلى أخرى.

للحدّ من هذه التجربة السيئة للمستخدم، اتّبع مطوّرو JavaScript في Kiwix في البداية مسار Electron، مثل العديد من المطوّرين. ElectronJS هو إطار عمل رائع يقدّم ميزات فعّالة، بما في ذلك الوصول الكامل إلى نظام الملفات باستخدام واجهات برمجة تطبيقات Node. ومع ذلك، لهذه الطريقة بعض السلبيات المعروفة:

  • ولا يمكن تشغيله إلا على أنظمة التشغيل المخصّصة للكمبيوتر المكتبي.
  • حجمه كبير (من 70 إلى 100 ميغابايت).

يُرجى العِلم أنّ حجم تطبيقات Electron يتجاوز 5.1 ميغابايت بكثير، وذلك لأنّ كل تطبيق يتضمّن نسخة كاملة من Chromium.

هل كانت هناك طريقة يمكن من خلالها لفريق Kiwix تحسين الوضع لمستخدمي التطبيق المتوافق مع الأجهزة الجوّالة؟

واجهة برمجة التطبيقات File System Access API لإنقاذ الموقف

في عام 2019 تقريبًا، تعرّف فريق Kiwix على واجهة برمجة تطبيقات جديدة كانت تخضع لاختبار أولي في Chrome 78، وكان اسمها آنذاك Native File System API. ووعدنا بأنّه سيكون بإمكاننا الحصول على معرّف ملف لملف أو مجلد وتخزينه في قاعدة بيانات IndexedDB. من المهم أن يظل هذا الاسم المعرِّف محفوظًا بين جلسات التطبيق، حتى لا يضطر المستخدم إلى اختيار الملف أو المجلد مرة أخرى عند إعادة تشغيل التطبيق (على الرغم من أنّه عليهم الإجابة عن طلب إذن سريع). وبحلول مرحلة الطرح، تمت إعادة تسميتها باسم File System Access API، وتمت توحيد الأجزاء الأساسية من قِبل WHATWG باسم File System API (FSA).

إذن، كيف يعمل جزء File System Access من واجهة برمجة التطبيقات؟ في ما يلي بعض النقاط المهمة التي يجب أخذها في الاعتبار:

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

إنّ الرمز البرمجي بسيط نسبيًا، باستثناء الحاجة إلى استخدام واجهة برمجة التطبيقات المعقدة IndexedDB API لتخزين أسماء المعرِّفات الخاصة بالملف والدليل. والخبر السار هو أنّ هناك بضع مكتبات تُنجز الكثير من المهام الصعبة نيابةً عنك، مثل مكتبة browser-fs-access. في Kiwix JS، قرّرنا العمل مباشرةً مع واجهات برمجة التطبيقات التي تم documentingها بشكل جيد جدًا.

فتح أدات اختيار الملفات والأدلة

يظهر فتح أداة اختيار الملفات على النحو التالي (يتم استخدام Promises هنا، ولكن إذا كان يعجبك async/await sugar، يمكنك الاطّلاع على الدليل التعليمي لبرنامج Chrome للمطوّرين):

return window
 
.showOpenFilePicker({ multiple: false })
 
.then(function (fileHandles) {
   
return processFileHandle(fileHandles[0]);
 
})
 
.catch(function (err) {
   
// This is normal if app is launching
    console
.warn(
     
'User cancelled, or cannot access fs without user gesture',
      err
,
   
);
 
});

يُرجى العِلم أنّ هذا الرمز لا يعالج سوى الملف الأول الذي تم اختياره (ويحظر اختيار أكثر من ملف واحد) وذلك بهدف التبسيط. إذا أردت السماح باختيار ملفات متعددة باستخدام { multiple: true }، ما عليك سوى لفّ كل التعهدات التي تعالج كل اسم معرِّف في عبارة Promise.all().then(...)، على سبيل المثال:

let promisesForFiles = fileHandles.map(function (fileHandle) {
   
return processFileHandle(fileHandle);
});
return Promise.all(promisesForFiles).then(function (arrayOfFiles) {
   
// Do something with the files array
    console
.log(arrayOfFiles);
}).catch(function (err) {
   
// Handle any errors that occurred during processing
    console
.error('Error processing file handles!', err);
)};

ومع ذلك، يمكن اختيار ملفات متعددة بشكل أفضل من خلال مطالبة المستخدم بتحديد المجلد الذي يحتوي على هذه الملفات بدلاً من الملفات الفردية فيه، خاصةً أنّ مستخدمي Kiwix يميلون إلى تنظيم جميع ملفات ZIM في المجلد نفسه. رمز تشغيل أداة اختيار الدليل هو نفسه تقريبًا كما هو موضّح أعلاه باستثناء استخدام window.showDirectoryPicker.then(function (dirHandle) { … });.

معالجة معرّف الملف أو الدليل

بعد الحصول على الاسم المعرِّف، عليك معالجته، لذا يمكن أن تبدو الدالة processFileHandle على النحو التالي:

function processFileHandle(fileHandle) {
 
// Serialize fileHandle to indexedDB
  serializeFSHandletoIdxDB
('pickedFSHandle', fileHandle, function (val) {
    console
.debug('IndexedDB responded with ' + val);
 
});
 
return fileHandle.getFile().then(function (file) {
   
// Do something with the file
   
return file;
 
});
}

يُرجى العلم أنّه عليك توفير الوظيفة لتخزين معرّف الملف، ولا تتوفّر طُرق مساعدة لهذا الغرض، ما لم تستخدم مكتبة تجريدية. يمكن الاطّلاع على كيفية تنفيذ Kiwix لهذه الميزة في الملف cache.js، ولكن يمكن تبسيطها بشكل كبير إذا تم استخدامها فقط لتخزين اسم ملف أو مجلد واسترداده.

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

let iterableEntryList = dirHandle.entries();
return iterateAsyncDirEntries(iterableEntryList, []).then(function (entryList) {
 
// Do something with the entry list
 
return entryList;
});

/**
 * Iterates FileSystemDirectoryHandle iterator and adds entries to an array
 * @param {Iterator} entries An asynchronous iterator of entries
 * @param {Array} archives An array to which to add the entries (may be empty)
 * @return {Promise<Array>} A Promise for an array of entries in the directory
 */

function iterateAsyncDirEntries(entries, archives) {
 
return entries
   
.next()
   
.then(function (result) {
     
if (!result.done) {
        let entry
= result.value[1];
       
// Filter for the files you want
       
if (/\.zim(\w\w)?$/i.test(entry.name)) {
          archives
.push(entry);
       
}
       
return iterateAsyncDirEntryArray(entries, archives);
     
} else {
       
// We've processed all the entries
       
if (!archives.length) {
          console
.warn('No archives found in the picked directory!');
       
}
       
return archives;
     
}
   
})
   
.catch(function (err) {
      console
.error('There was an error processing the directory!', err);
   
});
}

يُرجى العلم أنّه لكل إدخال في entryList، ستحتاج لاحقًا إلى الحصول على الملف باستخدام entry.getFile().then(function (file) { … }) عندما تحتاج إلى استخدامه، أو المكافئ باستخدام const file = await entry.getFile() في async function.

هل يمكننا تحسين الأداء أكثر؟

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

ماذا لو لم يكن علينا الانتظار؟ اكتشف مطوّرو Kiwix مؤخرًا أنّه يمكنهم إزالة جميع طلبات الأذونات في الوقت الحالي، وذلك باستخدام ميزة جديدة رائعة لواجهة برمجة التطبيقات File Access API التي تتيحها كل من متصفحات Chromium وFirefox (تتيحها Safari جزئيًا، ولكن لا تزال غير متاحة FileSystemWritableFileStream). وهذه الميزة الجديدة هي Origin Private File System.

استخدام نظام ملفات أصلي بالكامل: نظام الملفات الخاص في Origin

لا يزال نظام Origin Private File System (OPFS) ميزة تجريبية في تطبيق Kiwix PWA، ولكن الفريق متحمّس جدًا لتشجيع المستخدمين على تجربته لأنّه يسدّ الفجوة بين التطبيقات المتوافقة مع الأجهزة الجوّالة والتطبيقات المتوافقة مع الويب. في ما يلي المزايا الرئيسية:

  • يمكن الوصول إلى الأرشيفات في "نظام إدارة الملفات في وضع التشغيل" بدون طلبات الحصول على أذونات، حتى عند التشغيل. يمكن للمستخدمين استئناف قراءة مقالة وتصفُّح أرشيف من حيث توقفوا في جلسة سابقة، بدون أي مشاكل على الإطلاق.
  • ويوفر إمكانية وصول محسّنة للغاية إلى الملفات المخزّنة فيه: على أجهزة Android، نلاحظ تحسينات في السرعة تتراوح بين خمس و10 مرات أسرع.

إنّ الوصول العادي إلى الملفات في Android باستخدام واجهة برمجة التطبيقات File API يكون بطيئًا بشكلٍ مؤلم، خاصةً (كما هو الحال غالبًا لمستخدمي Kiwix) إذا تم تخزين أرشيفات كبيرة على بطاقة microSD بدلاً من مساحة تخزين الجهاز. تغيّر كل ذلك مع واجهة برمجة التطبيقات الجديدة هذه. على الرغم من أنّ معظم المستخدمين لن يتمكّنوا من تخزين ملف بحجم 97 غيغابايت في نظام OPFS (الذي يستهلك مساحة تخزين الجهاز، وليس مساحة تخزين بطاقة microSD)، إلا أنّه مثالي لتخزين أرشيفات صغيرة أو متوسطة الحجم. هل تريد قراءة الموسوعة الطبية الأكثر اكتمالاً من WikiProject Medicine؟ لا مشكلة، يمكن تخزين هذا الملف بسهولة في نظام OPFS بسعة 1.7 غيغابايت. (ملاحظة: ابحث عن othermdwiki_en_all_maxi في المكتبة داخل التطبيق).

آلية عمل ميزة "الملف الشخصي للعمل"

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

لاستخدام نظام OPFS، تكون الخطوة الأولى هي طلب الوصول إليه باستخدام navigator.storage.getDirectory() (مرة أخرى، إذا كنت تفضّل الاطّلاع على الرمز باستخدام await، يُرجى قراءة نظام Origin Private File System):

return navigator.storage
 
.getDirectory()
 
.then(function (handle) {
   
return processDirHandle(handle);
 
})
 
.catch(function (err) {
    console
.warn('Unable to get the OPFS directory entry', err);
 
});

الاسم المعرِّف الذي تحصل عليه من هذا الإجراء هو النوع نفسه من FileSystemDirectoryHandle الذي تحصل عليه من window.showDirectoryPicker() المذكور أعلاه، ما يعني أنّه يمكنك إعادة استخدام الرمز الذي يعالج ذلك (وهناك خبر سارّ وهو أنّه ما مِن حاجة إلى تخزين هذا الرمز في indexedDB، ما عليك سوى الحصول عليه عند احتياجك إليه). لنفترض أنّ لديك بعض الملفات في OPFS وتريد استخدامها، ثم باستخدام الدالة iterateAsyncDirEntries() الموضَّحة سابقًا، يمكنك إجراء ما يلي:

return navigator.storage.getDirectory().then(function (dirHandle) {
  let entries
= dirHandle.entries();
 
return iterateAsyncDirEntries(entries, [])
   
.then(function (archiveList) {
     
return archiveList;
   
})
   
.catch(function (err) {
      console
.error('Unable to iterate OPFS entries', err);
   
});
});

تذكَّر أنّه لا يزال عليك استخدام getFile() على أي إدخال تريد العمل به من صفيف archiveList.

استيراد الملفات إلى OPFS

إذًا، كيف تنقل الملفات إلى "نظام إدارة الملفات في السحابة الإلكترونية" في المقام الأول؟ ليس بهذه السرعة. أولاً، عليك تقدير مساحة التخزين المتاحة لك، والتأكّد مما إذا كان المستخدمون سيحاولون تحميل ملف بحجم 97 غيغابايت إذا لم يكن مناسبًا.

يمكنك الحصول على الحصة المقدَّرة بسهولة: navigator.storage.estimate().then(function (estimate) { … });. أما المسألة الأصعب قليلاً، فهي تحديد كيفية عرض هذه المعلومات للمستخدم. في تطبيق Kiwix، اخترنا استخدام لوحة صغيرة داخل التطبيق تظهر بجانب مربّع الاختيار مباشرةً، ما يتيح للمستخدمين تجربة نظام OPFS:

لوحة تعرض مساحة التخزين المستخدَمة بالنسب المئوية ومساحة التخزين المتاحة المتبقية بالغيغابايت

تتم تعبئة اللوحة باستخدام estimate.quota و estimate.usage، على سبيل المثال:

let OPFSQuota; // Global variable, so we don't have to keep checking it
return navigator.storage.estimate().then(function (estimate) {
 
const percent = ((estimate.usage / estimate.quota) * 100).toFixed(2);
 
OPFSQuota = estimate.quota - estimate.usage;
  document
.getElementById('OPFSQuota').innerHTML =
   
'<b>OPFS storage quota:</b><br />Used:&nbsp;<b>' +
    percent
+
   
'%</b>; ' +
   
'Remaining:&nbsp;<b>' +
   
(OPFSQuota / 1024 / 1024 / 1024).toFixed(2) +
   
'&nbsp;GB</b>';
});

كما ترى، يتوفّر أيضًا زر يتيح للمستخدمين إضافة ملفات إلى OPFS من نظام الملفات الظاهرة للمستخدم. الخبر السارّ هنا هو أنّه يمكنك ببساطة استخدام File API للحصول على كائن الملف (أو الكائنات) المطلوبة التي سيتم استيرادها. في الواقع، من المهم عدم استخدام window.showOpenFilePicker() لأنّ هذه الطريقة غير متوافقة مع Firefox، في حين أنّ OPFS متوافق بالتأكيد.

الزرّ المرئي إضافة ملفات الذي يظهر في لقطة الشاشة أعلاه ليس أداة اختيار ملفات قديمة، ولكنه click()يعرض أداة اختيار قديمة مخفية (عنصر <input type="file" multiple … />) عند النقر عليه. بعد ذلك، يُسجِّل التطبيق حدث change لإدخال الملف المخفي، ويتحقّق من حجم الملفات ويرفضها إذا كانت كبيرة جدًا بالنسبة إلى الحصة. إذا كان كل شيء على ما يرام، اسأل المستخدم عما إذا كان يريد إضافتها:

archiveFilesLegacy.addEventListener('change', function (files) {
 
const filesArray = Array.from(files.target.files);
 
// Abort if user didn't select any files
 
if (filesArray.length === 0) return;
 
// Calculate the size of the picked files
  let filesSize
= 0;
  filesArray
.forEach(function (file) {
    filesSize
+= file.size;
 
});
 
// Check the size of the files does not exceed the quota
 
if (filesSize > OPFSQuota) {
   
// Oh no, files are too big! Tell user...
    console
.log('Files would exceed the OPFS quota!');
 
} else {
   
// Ask user if they're sure... if user said yes...
   
return importOPFSEntries(filesArray)
     
.then(function () {
       
// Tell user we successfully imported the archives
     
})
     
.catch(function (err) {
       
// Tell user there was an error (error catching is important!)
     
});
 
}
});

مربّع حوار يسأل المستخدم عما إذا كان يريد إضافة قائمة بملفات ‎.zim إلى نظام الملفات الخاص الأصلي

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

كيف نفَّذ Kiwix دالة importOPFSEntries()؟ ويشمل ذلك استخدام طريقة fileHandle.createWriteable() التي تسمح ببث كل ملف بفعالية في نظام OPFS. ويتولّى المتصفّح تنفيذ كل الإجراءات اللازمة. (تستخدم Kiwix Promises هنا لأسباب تتعلّق بقاعدة بياناتنا القديمة، ولكن يجب القول إنّ await تُنشئ في هذه الحالة بنية نحوية أبسط وتتجنّب تأثير هرم الهلاك).

function importOPFSEntries(files) {
 
// Get a handle on the OPFS directory
 
return navigator.storage
   
.getDirectory()
   
.then(function (dir) {
     
// Collect the promises for each file that we want to write
      let promises
= files.map(function (file) {
       
// Create the file and get a writeable handle on it
       
return dir
         
.getFileHandle(file.name, { create: true })
         
.then(function (fileHandle) {
           
// Get a writer for the file
           
return fileHandle.createWritable().then(function (writer) {
             
// Show a banner / spinner, then write the file
             
return writer
               
.write(file)
               
.then(function () {
                 
// Finished with this writer
                 
return writer.close();
               
})
               
.catch(function (err) {
                  console
.error('There was an error writing to the OPFS!', err);
               
});
           
});
         
})
         
.catch(function (err) {
            console
.error('Unable to get file handle from OPFS!', err);
         
});
     
});
     
// Return a promise that resolves when all the files have been written
     
return Promise.all(promises);
   
})
   
.catch(function (err) {
      console
.error('Unable to get a handle on the OPFS directory!', err);
   
});
}

تنزيل بث ملفات مباشرةً في OPFS

هناك اختلاف على ذلك وهو إمكانية بث ملف من الإنترنت مباشرةً إلى OPFS أو إلى أي دليل لديك معرّف دليل له (أي الدلائل التي تم اختيارها باستخدام window.showDirectoryPicker()). ويستخدم الرمز البرمجي أعلاه مبادئ مماثلة، ولكنه ينشئ Response يتألف من ReadableStream وعنصر تحكّم يضيف إلى "قائمة الانتظار" وحدات البايت التي يتم قراءتها من الملف العميق. بعد ذلك، تتم توجيه Response.body الناتج إلى كاتب الملف الجديد داخل نظام OPFS.

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

واجهة مستخدم Kiwix مع شريط في أسفل الصفحة يحذر المستخدم من عدم إغلاق التطبيق ويعرض مستوى تقدّم تنزيل أرشيف ‎.zim

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

تنفيذ تطبيق مصغّر لإدارة الملفات داخل التطبيق

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

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

تعتمد عملية تصدير الملفات على إمكانية الحصول على معرّف ملف لملف أو دليل تم اختيارهما وسيتم حفظ الملف الذي تم تصديره فيهما، لذا لا تعمل هذه العملية إلا في السياقات التي يمكن فيها استخدام طريقة window.showSaveFilePicker(). إذا كانت ملفات Kiwix أصغر حجمًا من عدة غيغابايت، سنتمكّن من إنشاء ملف تعريف متعدّد العناصر في الذاكرة، ومنحه عنوان URL، ثم تنزيله إلى نظام الملفات المرئي للمستخدم. لا يمكن إجراء ذلك مع أرشيفات كبيرة بهذا الشكل. إذا كان التصدير متاحًا، يكون الإجراء بسيطًا إلى حدٍ كبير: وهو يشبه إلى حدٍ كبير عملية حفظملف في OPFS (احصل على معرّف للملف المطلوب حفظه، واطلب من المستخدم اختيارموقع حفظه باستخدام window.showSaveFilePicker()، ثم استخدِمcreateWriteable() على saveHandle). يمكنك الاطّلاع على الرمز البرمجي في المستودع.

تتيح جميع المتصفحات حذف الملفات، ويمكن إجراء ذلك من خلال إجراء dirHandle.removeEntry('filename') بسيط. في ما يتعلّق بتطبيق Kiwix، فضّلنا تكرار إدخالات OPFS كما فعلنا أعلاه، حتى نتمكّن من التحقّق من توفّر الملف المحدّد أولاً وطلب التأكيد، ولكن قد لا يكون ذلك ضروريًا للجميع. مرة أخرى، يمكنك فحص الرمز البرمجي إذا أردت ذلك.

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

مربّع حوار يسأل المستخدم عما إذا كان يريد حذف ملف ‎.zim

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

عمل المطوّر لا ينتهي أبدًا

إنّ نظام OPFS هو ابتكار رائع لمطوّري تطبيقات الويب التقدّمية، إذ يقدّم ميزات فعالة جدًا في إدارة الملفات تساهم بشكل كبير في سد الفجوة بين التطبيقات الأصلية وتطبيقات الويب. لكنّ المطوّرين هم مجموعة تعيسة، فهم لن يشعروا أبدًا بالسعادة. إنّ نظام OPFS مثالي تقريبًا، ولكن ليس تمامًا. من الرائع أن تعمل الميزات الرئيسية في كل من متصفّحَي Chromium وFirefox، وأن يتم تنفيذها على أجهزة Android والكمبيوتر المكتبي. نأمل أن يتم قريبًا أيضًا تطبيق مجموعة الميزات الكاملة في Safari وiOS. لا تزال المشاكل التالية قائمة:

  • يفرض Firefox حاليًا حدًا أقصى يبلغ 10 غيغابايت على حصة OPFS، بغض النظر عن مقدار مساحة القرص الأساسية المتوفّرة. وفي حين أنّ هذا الحدّ قد يكون مناسبًا لأغلب مؤلفي التطبيقات المتوافقة مع تقنية الويب التقدّمي، إلا أنّه قد يكون محدودًا جدًا بالنسبة إلى Kiwix. لحسن الحظ، تتيح متصفحات Chromium مساحة أكبر بكثير.
  • لا يمكن حاليًا تصدير الملفات الكبيرة من OPFS إلى نظام الملفات الظاهرة للمستخدمين على متصفّحات الأجهزة الجوّالة أو متصفّح Firefox لأجهزة الكمبيوتر المكتبي، لأنّه لم يتم تنفيذ window.showSaveFilePicker(). في هذه المتصفحات، يتم حجز الملفات الكبيرة بشكل فعّال في نظام OPFS. وهذا الإجراء يتعارض مع فلسفة Kiwix التي تدعو إلى الوصول المفتوح إلى المحتوى، وإمكانية مشاركة المحفوظات بين المستخدمين، خاصة في المناطق التي تواجه فيها مشاكل متكررة في الاتصال بالإنترنت أو تكاليف مرتفعة.
  • لا يمكن للمستخدم التحكّم في مساحة التخزين التي سيستهلكها نظام الملفات الافتراضي OPFS. يشكّل ذلك مشكلة بشكل خاص على الأجهزة الجوّالة، حيث قد يتوفر لدى المستخدمين مساحات تخزين كبيرة على بطاقة microSD، ولكن بمساحة تخزين صغيرة جدًا في مساحة تخزين الجهاز.

بوجهٍ عام، هذه مشاكل بسيطة في ميزة رائعة تمثل خطوة كبيرة للأمام للاطّلاع على الملفات في التطبيقات المتوافقة مع الأجهزة الجوّالة. يُقدّر فريق Kiwix PWA بشدة مطوّري Chromium والمدافعين عنه الذين اقترحوا واجهة برمجة التطبيقات File System Access API وصمّموا واجهة برمجة التطبيقات هذه لأول مرة، كما يُقدّرون العمل الجاد الذي أدّى إلى تحقيق توافق بين مورّدي المتصفّحات بشأن أهمية نظام الملفات الخاص بالمورّد. بالنسبة إلى تطبيق Kiwix JS PWA، فقد تم حلّ العديد من مشاكل تجربة المستخدم التي كانت تؤثر في أداء التطبيق في السابق، ويساعدنا في سعينا لتحسين إمكانية وصول الجميع إلى محتوى Kiwix. يُرجى تجربة تطبيق Kiwix المتوافق مع الأجهزة الجوّالة (PWA) وإخبار المطوّرين برأيك.

للحصول على بعض المراجع الرائعة حول إمكانات التطبيقات المتوافقة مع الأجهزة الجوّالة، يمكنك الاطّلاع على المواقع الإلكترونية التالية: