خواندن و نوشتن فایل‌ها و دایرکتوری‌ها

منتشر شده: ۲۷ ژوئیه ۲۰۲۰

مرورگرها مدت‌هاست که می‌توانند با فایل‌ها و دایرکتوری‌ها کار کنند. رابط برنامه‌نویسی کاربردی (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> کار می‌کند. با توجه به یک Blob، می‌توانید ویژگی href مربوط به anchor را روی یک blob: URL تنظیم کنید که می‌توانید از متد 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();
};

مشکل

یک عیب بزرگ رویکرد دانلود این است که هیچ راهی برای ایجاد جریان کلاسیک باز کردن→ویرایش→ذخیره وجود ندارد، یعنی هیچ راهی برای بازنویسی فایل اصلی وجود ندارد. در عوض، هر زمان که "ذخیره" می‌کنید، یک کپی جدید از فایل اصلی در پوشه پیش‌فرض دانلودهای سیستم عامل خواهید داشت.

API دسترسی به سیستم فایل

رابط برنامه‌نویسی کاربردی دسترسی به سیستم فایل (File System Access API) هر دو عملیات باز کردن و ذخیره کردن را بسیار ساده‌تر می‌کند. همچنین امکان ذخیره واقعی را فراهم می‌کند. این بدان معناست که می‌توانید محل ذخیره فایل و بازنویسی یک فایل موجود را انتخاب کنید.

باز کردن فایل‌ها

با استفاده از رابط برنامه‌نویسی کاربردی دسترسی به سیستم فایل (File System Access API )، باز کردن یک فایل تنها با یک فراخوانی متد window.showOpenFilePicker() امکان‌پذیر است. این فراخوانی یک شناسه فایل (file handle) برمی‌گرداند که از طریق آن می‌توانید 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() که دایرکتوری‌ها را در کادر محاوره‌ای فایل قابل انتخاب می‌کند، یک دایرکتوری را باز کنید.

ذخیره فایل‌ها

ذخیره فایل‌ها نیز به همین ترتیب ساده است. از یک file handle، شما یک جریان قابل نوشتن را از طریق createWritable() ایجاد می‌کنید، سپس با فراخوانی متد write() جریان، داده‌های Blob را می‌نویسید و در نهایت با فراخوانی متد 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) را به عنوان یک پیشرفت تدریجی می‌بینم. به همین دلیل، می‌خواهم وقتی مرورگر از آن پشتیبانی می‌کند از آن استفاده کنم و اگر پشتیبانی نکرد، از رویکرد سنتی استفاده کنم؛ در عین حال هرگز کاربر را با دانلودهای غیرضروری کد جاوا اسکریپت پشتیبانی نشده مجازات نمی‌کنم. کتابخانه browser-fs-access پاسخ من به این چالش است.

فلسفه طراحی

از آنجایی که API دسترسی به سیستم فایل (File System Access API) احتمالاً در آینده تغییر خواهد کرد، API مرورگر-fs-access از آن الگوبرداری نشده است. یعنی، این کتابخانه یک polyfill نیست، بلکه یک ponyfill است. شما می‌توانید (به صورت ایستا یا پویا) منحصراً هر عملکردی را که برای کوچک نگه داشتن برنامه خود تا حد امکان نیاز دارید، وارد کنید. متدهای موجود با نام‌های مناسب fileOpen() ، directoryOpen() و fileSave() هستند. در داخل، این کتابخانه تشخیص می‌دهد که آیا API دسترسی به سیستم فایل پشتیبانی می‌شود یا خیر، و سپس مسیر کد مربوطه را وارد می‌کند.

از کتابخانه استفاده کنید

استفاده از این سه روش آسان است. می‌توانید mimeTypes یا extensions فایل مورد قبول برنامه خود را مشخص کنید و یک پرچم multiple برای اجازه یا عدم اجازه انتخاب چندین فایل یا دایرکتوری تنظیم کنید. برای جزئیات کامل، به مستندات API مرورگر-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',
  });
})();

نسخه آزمایشی

می‌توانید کد را در نسخه آزمایشی گیت‌هاب (GitHub demo) مشاهده کنید. کد منبع آن نیز در آنجا موجود است.

کتابخانه browser-fs-access در دنیای واقعی

در اوقات فراغتم، کمی در ساخت یک PWA قابل نصب به نام Excalidraw مشارکت می‌کنم، ابزاری برای تخته سفید که به شما امکان می‌دهد نمودارهایی با حس طراحی دستی رسم کنید. این ابزار کاملاً واکنش‌گرا است و روی طیف وسیعی از دستگاه‌ها، از تلفن‌های همراه کوچک گرفته تا رایانه‌هایی با صفحه نمایش بزرگ، به خوبی کار می‌کند. این بدان معناست که باید با فایل‌ها در تمام پلتفرم‌های مختلف، صرف نظر از اینکه از API دسترسی به سیستم فایل پشتیبانی می‌کنند یا خیر، کار کند. این امر آن را به گزینه‌ای عالی برای کتابخانه browser-fs-access تبدیل می‌کند.

برای مثال، می‌توانم یک نقاشی را در آیفونم شروع کنم، آن را در پوشه دانلودهای آیفونم ذخیره کنم (از نظر فنی: آن را دانلود کنم، زیرا سافاری از API دسترسی به سیستم فایل پشتیبانی نمی‌کند) ، فایل را روی دسکتاپم باز کنم (بعد از انتقال آن از گوشی‌ام)، فایل را تغییر دهم و تغییراتم را روی آن بنویسم، یا حتی آن را به عنوان یک فایل جدید ذخیره کنم.

نقاشی Excalidraw روی آیفون
شروع طراحی Excalidraw روی آیفونی که از File System Access API پشتیبانی نمی‌کند، اما می‌توان فایل را در پوشه Downloads ذخیره (دانلود) کرد.
نقاشی اصلاح‌شده‌ی Excalidraw در کروم روی دسکتاپ.
باز کردن و تغییر ترسیم Excalidraw روی دسکتاپی که از File System Access API پشتیبانی می‌کند و بنابراین می‌توان از طریق API به فایل دسترسی داشت.
بازنویسی فایل اصلی با تغییرات اعمال شده.
بازنویسی فایل اصلی با تغییرات در فایل طراحی اصلی Excalidraw. مرورگر یک کادر محاوره‌ای نشان می‌دهد که از من می‌پرسد آیا این مورد اشکالی دارد یا خیر.
ذخیره تغییرات در یک فایل طراحی جدید Excalidraw.
ذخیره تغییرات در یک فایل جدید Excalidraw. فایل اصلی دست نخورده باقی می‌ماند.

نمونه کد در دنیای واقعی

در زیر، می‌توانید یک مثال واقعی از browser-fs-access را همانطور که در Excalidraw استفاده می‌شود، مشاهده کنید. این گزیده از /src/data/json.ts گرفته شده است. نکته جالب توجه این است که چگونه متد saveAsJSON() یک file handle یا null را به متد fileSave() مرورگر-fs-access ارسال می‌کند، که باعث می‌شود هنگام دریافت یک handle، آن را بازنویسی کند یا در صورت عدم دریافت، در یک فایل جدید ذخیره کند.

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 As ) را نیز نمایش دهید. تصاویر زیر تفاوت بین نوار ابزار اصلی واکنش‌گرای برنامه Excalidraw در آیفون و کروم دسکتاپ را نشان می‌دهد. توجه داشته باشید که دکمه ذخیره به عنوان در آیفون وجود ندارد.

نوار ابزار برنامه Excalidraw در آیفون فقط با یک دکمه «ذخیره».
نوار ابزار برنامه Excalidraw در آیفون فقط با یک دکمه ذخیره .
نوار ابزار برنامه Excalidraw در کروم با دکمه ذخیره و دکمه ذخیره با عنوان متمرکز.

نتیجه‌گیری

کار با فایل‌های سیستمی از نظر فنی روی همه مرورگرهای مدرن کار می‌کند. در مرورگرهایی که از API دسترسی به سیستم فایل پشتیبانی می‌کنند، می‌توانید با فراهم کردن امکان ذخیره و بازنویسی واقعی (نه فقط دانلود) فایل‌ها و با اجازه دادن به کاربران خود برای ایجاد فایل‌های جدید در هر کجا که می‌خواهند، تجربه را بهتر کنید، در حالی که همه این‌ها در مرورگرهایی که از API دسترسی به سیستم فایل پشتیبانی نمی‌کنند، همچنان قابل اجرا باقی می‌مانند. browser-fs-access با پرداختن به ظرافت‌های بهبود تدریجی و ساده‌سازی هرچه بیشتر کد شما، زندگی شما را آسان‌تر می‌کند.

تقدیرنامه‌ها

این توسط جو مدلی و کیس باسک بررسی شده است. از مشارکت‌کنندگان Excalidraw برای کارشان روی این پروژه و بررسی درخواست‌های Pull من متشکرم.