قراءة الملفات والأدلة وكتابتها

تاريخ النشر: 27 يوليو 2020

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

استخدام المكتبة

الطرق الثلاث سهلة الاستخدام. يمكنك تحديد 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',
  });
})();

عرض توضيحي

يمكنك الاطّلاع على الرمز البرمجي أثناء التنفيذ في عرض توضيحي على GitHub. رمز المصدر الخاص به متاح أيضًا على هذا الرابط.

مكتبة browser-fs-access في العالم الحقيقي

في وقت فراغي، أساهم بشكل بسيط في تطبيق ويب تقدّمي قابل للتثبيت يُسمى 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 مع زر حفظ وزر حفظ باسم.

الاستنتاجات

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

الإقرارات

تمت مراجعة هذه المقالة من قِبل جو ميدلي وكايس باسكس. أودّ أن أشكر المساهمين في Excalidraw على عملهم في المشروع وعلى مراجعة طلبات الدمج التي أرسلتها.