قراءة الملفات والأدلة وكتابتها باستخدام مكتبة 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 فحسب، بل يمكن استخدامه أيضًا في إصدار Edge القديم المستند إلى HTML وكذلك في 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 في المستقبل، لم يتم تصميم واجهة برمجة التطبيقات الخاصة بمتصفّح المتصفِّح-fs-access. وهذا يعني أن المكتبة ليست أداة polyfill، بل متجر ponyfill. يمكنك (بشكل ثابت أو ديناميكي) استيراد أي وظيفة تحتاج إليها بشكل حصري لإبقاء تطبيقك صغيرًا قدر الإمكان. وتتمثل الطرق المتاحة في الأسماء التالية: fileOpen() وdirectoryOpen() وfileSave(). داخليًا، ترصد ميزة المكتبة ما إذا كانت واجهة برمجة التطبيقات File System Access API متاحة، ثم تستورد مسار الرمز المقابل.

استخدام مكتبة المتصفح fs-access

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

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

مكتبة الوصول إلى المتصفِّحات العامة

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

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

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

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

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

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) {})، يمكنك عرض الزر حفظ باسم بالإضافة إلى الزر Save (حفظ). توضح لقطات الشاشة أدناه الفرق بين شريط أدوات التطبيق الرئيسي سريع الاستجابة من ExcaliDraw على iPhone وChrome لسطح المكتب. لاحِظ كيف أنّ الزر حفظ باسم غير متوفّر على هاتف iPhone.

شريط أدوات تطبيق ExcaliDraw على هاتف iPhone باستخدام زر &quot;حفظ&quot; فقط
شريط أدوات تطبيق ExcaliDraw على iPhone باستخدام الزر Save (حفظ) فقط
ارسم شريط أدوات التطبيق على سطح مكتب Chrome باستخدام الزر &quot;حفظ&quot; والزر &quot;حفظ باسم&quot;.
شريط أدوات تطبيق ExcaliDraw على Chrome مع النقر على Save (حفظ) وزر Save As (حفظ باسم) مركّز.

الاستنتاجات

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

شكر وتقدير

راجع هذه المقالة جو ميدلي وكايس باسك. شكرًا للمساهمين في ExcaliDraw على عملهم في المشروع ومراجعة طلبات السحب صورة رئيسية من إعداد إيليا بافلوف على Unلمحة