קריאה וכתיבה של קבצים וספריות באמצעות ספריית הדפדפן-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 של התג anchor לכתובת 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(). הקריאה הזו מחזירה את ה-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() שמאפשרת לבחור ספריות בתיבת הדו-שיח של הקובץ.

שמירת קבצים

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

לכן אני רואה את File System Access API כשיפור הדרגתי. לכן, אני רוצה להשתמש בו כשהדפדפן תומך בו, ולהשתמש בגישה המסורתית אם לא, וכל זאת בלי להעניש את המשתמש בהורדות מיותרות של קוד JavaScript שלא נתמך. הספרייה browser-fs-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',
  });
})();

הדגמה (דמו)

אפשר לראות את הקוד שלמעלה בפעולה בהדגמה ב-GitHub. קוד המקור שלו זמין גם שם. מטעמי אבטחה, לא ניתן להציג חלונות משנה חוצי-מקור עם בוחר קבצים, ולכן אי אפשר להטמיע את ההדגמה במאמר הזה.

הספרייה browser-fs-access בשימוש

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

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

ציור ב-Excalidraw באייפון.
התחלת ציור ב-Excalidraw באייפון שבו לא נתמך 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 שמותאם לנייד באייפון לבין סרגל הכלים הראשי של אפליקציית Excalidraw ב-Chrome במחשב. שימו לב שבמכשיר אייפון לא מופיע הלחצן שמירה בשם.

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

מסקנות

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

תודות

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