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

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

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

لمحة عن Kiwix

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

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

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

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

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

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

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

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

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

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

التخزين والتخزين في كل الأجهزة

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

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

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

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

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

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

  • وتعمل فقط على أنظمة تشغيل سطح المكتب.
  • الحجم كبير وثقيل (من 70 ميغابايت إلى 100 ميغابايت).

بالمقارنة مع حجم تطبيقات Electron، يتم تضمين نسخة كاملة من Chromium مع كل تطبيق، مقارنةً بحجم غير مناسب بحجم 5.1 ميغابايت فقط لتطبيق الويب التقدّمي المصغّر والمضمّن.

إذًا، هل كانت هناك طريقة يمكن من خلالها لشركة Kiwix تحسين الوضع لمستخدمي تطبيق الويب التقدّمي (PWA)؟

File System Access API لإنقاذ الجهاز

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

إذًا، كيف يعمل جزء الوصول إلى نظام الملفات من واجهة برمجة التطبيقات؟ بعض النقاط المهمة التي يجب ملاحظتها:

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

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

فتح منتقي الملفات والدليل

يبدو فتح أداة اختيار الملفات على هذا النحو (هنا باستخدام Promis)، ولكن إذا كنت تفضّل استخدام سكر async/await، يمكنك الاطّلاع على الدليل التعليمي حول 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

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

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

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

طريقة عمل OPFS

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

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

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

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

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

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

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

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

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

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

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

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

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

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

لا يكتمل عمل المطوّر مطلقًا

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

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

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

للحصول على بعض الموارد الرائعة حول إمكانات تطبيق الويب التقدّمي (PWA)، ألقِ نظرة على المواقع الإلكترونية التالية: