كيف يسمح تطبيق 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 متوسط المواصفات. كما تتوفر العديد من الموارد الأصغر حجمًا، بما في ذلك إصدارات ويكيبيديا ذات الطابع الخاص، مثل الرياضيات والطب وما إلى ذلك.

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

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

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

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

للتخفيف من حدة تجربة المستخدم السيئة هذه، مثل العديد من المطورين، انطلق مطورو Kiwix JS في البداية على مسار 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.

إنشاء بيئة عمل مدمجة مع المحتوى بالكامل: نظام الملفات الخاصة المصدر

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

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

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

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