קריאה וכתיבה של קבצים וספריות באמצעות ספריית הדפדפן-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 וגם ב-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 בפועל באמצעות ה-method 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 באמצעות קריאה ל-method‏ write() של המקור, ובסוף סוגרים את המקור באמצעות קריאה ל-method‏ 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. כל הדפדפנים מסומנים כ &#39;ללא תמיכה&#39; או כ &#39;מאחורי דגל&#39;.
טבלת תמיכה בדפדפנים של File System Access API. (מקור)

לכן, אני רואה את File System Access API כשיפור הדרגתי. לכן, אני רוצה להשתמש בה כשהדפדפן תומך בה, ולהשתמש בגישה המסורתית אם לא. חשוב לי לא להעניש את המשתמש בהורדות מיותרות של קוד JavaScript לא נתמך. הספרייה browser-fs-access היא התשובה שלי לאתגר הזה.

פילוסופיית העיצוב

מכיוון ש-File System Access API צפוי להשתנות בעתיד, ממשק ה-API של browser-fs-access לא מבוסס עליו. כלומר, הספרייה היא לא 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, ולכן אפשר לגשת לקובץ דרך ה-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 לבין סרגל הכלים הראשי של אפליקציית Excalidraw ב-Chrome במחשב. שימו לב איך הלחצן שמירה בשם ב-iPhone לא מופיע.

סרגל הכלים של אפליקציית Excalidraw ב-iPhone עם לחצן &#39;שמירה&#39; בלבד.
סרגל הכלים של אפליקציית Excalidraw ב-iPhone עם לחצן שמירה בלבד.
סרגל הכלים של אפליקציית Excalidraw במחשב עם לחצן &#39;שמירה&#39; ולחצן &#39;שמירה בתור&#39;.
סרגל הכלים של אפליקציית Exacalidraw ב-Chrome עם לחצן שמירה ולחצן שמירה בתור מודגש.

מסקנות

מבחינה טכנית, אפשר לעבוד עם קובצי מערכת בכל הדפדפנים המודרניים. בדפדפנים שתומכים ב-File System Access API, אפשר לשפר את חוויית השימוש באמצעות הרשאה לשמירה והחלפה של קבצים באופן אמיתי (ולא רק בהורדה) של קבצים, ולאפשר למשתמשים ליצור קבצים חדשים בכל מקום שתרצו, ועדיין להמשיך להיות פונקציונלי בדפדפנים שלא תומכים ב-File System Access API. ה-browser-fs-access מקל עליכם על ידי טיפול בהיבטים העדינים של שיפור הדרגתי והפיכת הקוד לפשוט ככל האפשר.

תודות

המאמר הזה נבדק על ידי Joe Medley ו-Kayce Basques. תודה לשותפי התוכן של Excalidraw על העבודה שלהם בפרויקט ועל בדיקת בקשות המשיכה שלי. תמונה ראשית (Hero) מאת איליה פבלוב ב-Un פעילות.