خواندن و نوشتن فایل ها و دایرکتوری ها با کتابخانه مرورگر-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 مبتنی بر EdgeHTML قدیمی و همچنین در فایرفاکس قابل استفاده است.

ذخیره (و نه: دانلود) فایل ها

برای ذخیره یک فایل، به طور سنتی، شما محدود به دانلود یک فایل هستید که به لطف ویژگی <a download> کار می کند. با توجه به یک Blob، می‌توانید ویژگی href لنگر را روی یک 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 واقعی را از طریق متد 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

همانطور که API دسترسی به فایل سیستم کاملاً خوب است، هنوز به طور گسترده در دسترس نیست .

جدول پشتیبانی مرورگر برای File System Access API. همه مرورگرها به عنوان "بدون پشتیبانی" یا "پشت پرچم" علامت گذاری شده اند.
جدول پشتیبانی مرورگر برای File System Access API. ( منبع )

به همین دلیل است که من File System Access API را به عنوان یک پیشرفت پیشرونده می بینم. به این ترتیب، من می‌خواهم زمانی که مرورگر از آن پشتیبانی می‌کند از آن استفاده کنم و اگر نه، از روش سنتی استفاده کنم. در حالی که هرگز کاربر را با دانلودهای غیر ضروری کد جاوا اسکریپت پشتیبانی نشده تنبیه نمی کنید. کتابخانه مرورگر-fs-access پاسخ من به این چالش است.

فلسفه طراحی

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

با استفاده از کتابخانه مرورگر-fs-access

استفاده از این سه روش بصری است. می‌توانید 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',
  });
})();

نسخه ی نمایشی

می‌توانید کد بالا را در یک دمو در Glitch مشاهده کنید. کد منبع آن نیز در آنجا موجود است. از آنجایی که به دلایل امنیتی، فریم های فرعی متقاطع مجاز به نمایش انتخابگر فایل نیستند، نسخه آزمایشی را نمی توان در این مقاله جاسازی کرد.

کتابخانه مرورگر-fs-دسترسی در طبیعت

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

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

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

نمونه کد زندگی واقعی

در زیر، می توانید یک نمونه واقعی از مرورگر-fs-access را همانطور که در Excalidraw استفاده می شود، مشاهده کنید. این گزیده از /src/data/json.ts گرفته شده است. نکته جالب توجه این است که چگونه متد saveAsJSON() یک دسته فایل یا null را به متد fileSave() مرورگر-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);
};

ملاحظات UI

چه در Excalidraw یا برنامه شما، رابط کاربری باید با وضعیت پشتیبانی مرورگر سازگار شود. اگر API دسترسی به فایل سیستم پشتیبانی می‌شود ( if ('showOpenFilePicker' in window) {} ) می‌توانید علاوه بر دکمه ذخیره ، دکمه ذخیره به‌عنوان را نیز نشان دهید. اسکرین شات های زیر تفاوت بین نوار ابزار اصلی برنامه پاسخگو Excalidraw در آیفون و دسکتاپ کروم را نشان می دهد. توجه داشته باشید که چگونه دکمه Save As در آیفون وجود ندارد.

نوار ابزار برنامه Excalidraw در آیفون تنها با یک دکمه "ذخیره".
نوار ابزار برنامه Excalidraw در آیفون تنها با یک دکمه ذخیره .
نوار ابزار برنامه Excalidraw روی دسکتاپ Chrome با دکمه «ذخیره» و «ذخیره به عنوان».
نوار ابزار برنامه Excalidraw در Chrome با یک دکمه ذخیره و متمرکز Save As .

نتیجه گیری

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

قدردانی ها

این مقاله توسط Joe Medley و Kayce Basques بررسی شده است. از مشارکت کنندگان Excalidraw برای کارشان روی پروژه و بررسی درخواست های کشش من تشکر می کنم. تصویر قهرمان توسط ایلیا پاولوف در Unsplash.