قراءة الملفات والأدلة وكتابتها باستخدام مكتبة open-fs-access

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

الطريقة التقليدية للتعامل مع الملفات

فتح الملفات

بصفتك مطوّرًا، يمكنك فتح الملفات وقراءتها من خلال العنصر <input type="file">. في أبسط أشكاله، يمكن أن يبدو فتح ملف مثل نموذج الرمز البرمجي أدناه. يمنحك عنصر input FileList، الذي يتكوّن في الحالة أدناه من File واحد فقط. File هو نوع معيّن من Blob، ويمكن استخدامه في أي سياق يمكن استخدام Blob فيه.

const openFile = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

فتح الأدلة

لفتح المجلدات (أو الأدلة)، يمكنك ضبط السمة <input webkitdirectory>. بخلاف ذلك، تعمل جميع العناصر الأخرى بالطريقة نفسها كما هو موضح أعلاه. على الرغم من الاسم الذي يسبقه اسم المورّد، لا يمكن استخدام webkitdirectory في متصفّحات Chromium وWebKit فقط، بل يمكن استخدامه أيضًا في متصفّح EdgeHTML القديم وFirefox.

حفظ (أو تنزيل) الملفات

لحفظ أي ملف، تقتصر في العادة على تنزيل ملف، وهو أمر يمكن تنفيذه استنادًا إلى السمة <a download>. يمكنك ضبط السمة href الخاصة بالارتساء على عنوان URL blob: الذي يمكنك الحصول عليه من الطريقة URL.createObjectURL().

const saveFile = async (blob) => {
  const a = document.createElement('a');
  a.download = 'my-file.txt';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

المشكلة

من بين الجوانب السلبية الكبيرة لطريقة التنزيل أنّه لا يمكن تنفيذ الخطوات الكلاسيكية التالية: الفتح→التعديل→الحفظ، أي أنّه لا يمكن استبدال الملف الأصلي. بدلاً من ذلك، ستحصل على نسخة جديدة من الملف الأصلي في مجلد "عمليات التنزيل" التلقائي في نظام التشغيل عند "الحفظ".

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

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

فتح الملفات

باستخدام File System Access API، يمكنك فتح ملف من خلال طلب واحد إلى طريقة window.showOpenFilePicker(). تُعرِض هذه الدعوة معرّف ملف يمكنك من خلاله الحصول على File الفعلي من خلال طريقة getFile().

const openFile = async () => {
  try {
    // Always returns an array.
    const [handle] = await window.showOpenFilePicker();
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

فتح الأدلة

افتح دليلاً من خلال استدعاء دالة window.showDirectoryPicker() التي تتيح اختيار الأدلة في مربّع حوار الملفات.

حفظ الملفات

وبالمثل، فإن حفظ الملفات أمر واضح تمامًا. من معرّف ملف، يمكنك إنشاء مصدر بيانات قابل للكتابة من خلال createWritable()، ثم كتابة بيانات Blob من خلال استدعاء طريقة write() لمصدر البيانات، وفي النهاية، يمكنك إغلاق مصدر البيانات من خلال استدعاء طريقة close().

const saveFile = async (blob) => {
  try {
    const handle = await window.showSaveFilePicker({
      types: [{
        accept: {
          // Omitted
        },
      }],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
    return handle;
  } catch (err) {
    console.error(err.name, err.message);
  }
};

لمحة عن متصفّح-fs-access

على الرغم من أنّ واجهة برمجة التطبيقات File System Access API جيدة تمامًا، إلا أنّها غير متاحة على نطاق واسع بعد.

جدول توافق المتصفّحات لواجهة برمجة التطبيقات File System Access API يتم وضع علامة &quot;لا يتوفّر دعم&quot; أو &quot;مُعلَن عنه&quot; على جميع المتصفّحات.
جدول توافق المتصفّحات لواجهة برمجة التطبيقات File System Access API (المصدر)

لهذا السبب، أرى أنّ File System Access API هي ميزة تحسين تدريجي. ولهذا السبب، أريد استخدامها عندما يكون المتصفّح متوافقًا معها، واستخدام النهج التقليدي في حال عدم توافقه، مع عدم تحميل المستخدم لأي رمز JavaScript غير متوافق. مكتبة browser-fs-access هي الحلّ الذي أقدّمه لهذا التحدي.

فلسفة التصميم

وبما أنّه من المحتمل أن تتغيّر واجهة برمجة التطبيقات File System Access API في المستقبل، لم يتم تصميم نموذج لـ browser-fs-access API. وهذا يعني أنّ المكتبة ليست polyfill، بل عبارة عن ponyfill. يمكنك استيراد أي وظائف تحتاجها حصريًا (سواءً بشكل ثابت أو ديناميكي) للحفاظ على حجم تطبيقك صغيرًا قدر الإمكان. والطرق المتاحة هي ذات الاسم المناسب fileOpen() وdirectoryOpen() وfileSave(). داخل المكتبة، ترصد ميزة المكتبة ما إذا كانت واجهة برمجة التطبيقات File System Access API متوافقة، ثم تستورد مسار الرمز البرمجي المقابل.

استخدام مكتبة web-fs-access

الطرق الثلاث سهلة الاستخدام. يمكنك تحديد mimeTypes أو ملف extensions المقبولَين في تطبيقك، وضبط علامة multiple للسماح باختيار ملفات أو أدلة متعددة أو عدم السماح بذلك. للاطّلاع على التفاصيل الكاملة، يُرجى الاطّلاع على مستندات واجهة برمجة التطبيقات browser-fs-access API. يوضح نموذج التعليمات البرمجية أدناه كيفية فتح ملفات الصور وحفظها.

// The imported methods will use the File
// System Access API or a fallback implementation.
import {
  fileOpen,
  directoryOpen,
  fileSave,
} from 'https://unpkg.com/browser-fs-access';

(async () => {
  // Open an image file.
  const blob = await fileOpen({
    mimeTypes: ['image/*'],
  });

  // Open multiple image files.
  const blobs = await fileOpen({
    mimeTypes: ['image/*'],
    multiple: true,
  });

  // Open all files in a directory,
  // recursively including subdirectories.
  const blobsInDirectory = await directoryOpen({
    recursive: true
  });

  // Save a file.
  await fileSave(blob, {
    fileName: 'Untitled.png',
  });
})();

عرض توضيحي

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

مكتبة browser-fs-access في الاستخدام الفعلي

في وقت فراغي، أساهم قليلاً في تطوير تطبيق متوافق مع الأجهزة الجوّالة (PWA) قابل للتثبيت يُسمى Excalidraw، وهو أداة لوح معلومات تتيح لك بسهولة رسم مخطّطات بيانية بأسلوب يشبه الرسم اليدوي. وهو مستجيب بالكامل ويعمل بشكل جيد على مجموعة من الأجهزة بدءًا من الهواتف المحمولة الصغيرة إلى أجهزة الكمبيوتر ذات الشاشات الكبيرة. وهذا يعني أنّه يجب التعامل مع الملفات على جميع الأنظمة الأساسية المختلفة سواء كانت متوافقة مع واجهة برمجة التطبيقات File System Access API أم لا. وهذا يجعله مرشحًا رائعًا لمكتبة browser-fs-access.

على سبيل المثال، يمكنني بدء رسم على هاتف iPhone، وحفظه (من الناحية الفنية: تنزيله، لأنّ Safari لا يتيح استخدام واجهة برمجة التطبيقات File System Access API) في مجلد "عمليات التنزيل" على هاتف iPhone، وفتح الملف على الكمبيوتر المكتبي (بعد نقله من هاتفي)، وتعديل الملف واستبداله بتغييراتي، أو حتى حفظه كملف جديد.

رسم Excalidraw على هاتف iPhone
بدء رسم ExcaliDraw على هاتف iPhone حيث تكون واجهة برمجة التطبيقات File System Access API غير متاحة، ولكن يمكن حفظ (تنزيله) ملف في مجلد "عمليات التنزيل"
رسم Excalidraw المعدَّل على Chrome على الكمبيوتر المكتبي
فتح رسم Excalidraw وتعديله على الكمبيوتر المكتبي الذي تتوفّر فيه واجهة برمجة التطبيقات File System Access API وبالتالي يمكن الوصول إلى الملف من خلال واجهة برمجة التطبيقات
استبدال الملف الأصلي بالتعديلات
سيتم استبدال الملف الأصلي بالملف المعدَّل من ملف الرسم الأصلي في Excalidraw. يعرض المتصفّح مربّع حوار يسألني ما إذا كان ذلك مقبولاً.
حفظ التعديلات في ملف رسم جديد على Excalidraw
يتم حفظ التعديلات في ملف Excalidraw جديد. ويظل الملف الأصلي بدون أي تغيير.

عيّنة تعليمات برمجية من واقع الحياة

في ما يلي مثال فعلي على استخدام browser-fs-access في Excalidraw. هذا المقتطف مأخوذ من /src/data/json.ts. من المثير للاهتمام معرفة كيفية تمرير الطريقة saveAsJSON() إما معالِج ملف أو null إلى الطريقة fileSave() في browser-fs-access، ما يؤدي إلى استبدال الملف عند تقديم معالِج، أو حفظه في ملف جديد في حال عدم تقديم معالِج.

export const saveAsJSON = async (
  elements: readonly ExcalidrawElement[],
  appState: AppState,
  fileHandle: any,
) => {
  const serialized = serializeAsJSON(elements, appState);
  const blob = new Blob([serialized], {
    type: "application/json",
  });
  const name = `${appState.name}.excalidraw`;
  (window as any).handle = await fileSave(
    blob,
    {
      fileName: name,
      description: "Excalidraw file",
      extensions: ["excalidraw"],
    },
    fileHandle || null,
  );
};

export const loadFromJSON = async () => {
  const blob = await fileOpen({
    description: "Excalidraw files",
    extensions: ["json", "excalidraw"],
    mimeTypes: ["application/json"],
  });
  return loadFromBlob(blob);
};

اعتبارات واجهة المستخدم

سواء في Excalidraw أو تطبيقك، يجب أن يتم تكييف واجهة المستخدم مع حالة توافق المتصفّح. إذا كانت File System Access API متوافقة (if ('showOpenFilePicker' in window) {})، يمكنك عرض الزر حفظ باسم بالإضافة إلى الزر حفظ. توضِّح لقطات الشاشة أدناه الفرق بين شريط أدوات التطبيق الرئيسي في Excalidraw المتوافق مع مختلف الأجهزة على هاتف iPhone وعلى متصفّح Chrome للكمبيوتر المكتبي. يُرجى ملاحظة أنّ زر الحفظ باسم غير متوفّر على هاتف iPhone.

شريط أدوات تطبيق Excalidraw على هاتف iPhone يتضمّن زر &quot;حفظ&quot; فقط
شريط أدوات تطبيق ExcaliDraw على هاتف iPhone، من خلال النقر على الزر حفظ
شريط أدوات تطبيق Excalidraw على متصفّح Chrome لأجهزة الكمبيوتر المكتبي مع زرَّي &quot;حفظ&quot; و&quot;حفظ باسم&quot;
شريط أدوات تطبيق Excalidraw على Chrome مع زر حفظ وزر حفظ باسم مُركّز.

الاستنتاجات

من الناحية الفنية، يمكن التعامل مع ملفات النظام على جميع المتصفّحات الحديثة. في المتصفّحات التي تتوافق مع File System Access API، يمكنك تحسين تجربة الاستخدام من خلال السماح بحفظ الملفات واستبدالها بشكل حقيقي (وليس فقط تنزيلها)، وكذلك السماح للمستخدمين بإنشاء ملفات جديدة أينما أرادوا، وكل ذلك مع الاحتفاظ بوظائفهم على المتصفحات التي لا تتوافق مع واجهة File System Access API. تسهِّل لك واجهة برمجة التطبيقات browser-fs-access الحياة، من خلال التعامل مع التفاصيل الدقيقة للتحسين التدريجي وجعل رمزك البرمجي بسيطًا قدر الإمكان.

شكر وتقدير

تمت مراجعة هذه المقالة من قِبل جو ميدلي و كايسي باسكيز. نشكر المساهمين في Excali draw على عملهم في المشروع وعلى مراجعة "طلبات السحب" التي أقدّمها. الصورة الرئيسية من أعمال Ilya Pavlov على Unsplash