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

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

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

فتح الملفات

بصفتك مطوّرًا، يمكنك فتح الملفات وقراءتها من خلال العنصر <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>. في حال توفّر عنصر Blob، يمكنك ضبط سمة 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);
  }
};

تقديم browser-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 متوافقة، ثم تستورد مسار الرمز البرمجي المقابل.

استخدام مكتبة browser-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 الحياة من خلال التعامل مع التفاصيل الدقيقة للتحسين التدريجي وجعل رمزك البرمجي بسيطًا قدر الإمكان.

الشكر والتقدير

تمت مراجعة هذه المقالة من قِبل جو ميدلي و كايسي باسكيز. أشكر المساهمين في Excalidraw على عملهم في المشروع ومراجعتهم لطلبات سحب المشارِكين. الصورة الرئيسية من أعمال إيليا بافلوف على Unsplash