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




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


מסקנות
מבחינה טכנית, אפשר לעבוד עם קבצי מערכת בכל הדפדפנים המודרניים. בדפדפנים שתומכים ב-File System Access API, אפשר לשפר את חוויית השימוש על ידי מתן אפשרות לשמירה ולכתיבה מחדש אמיתיות (לא רק הורדה) של קבצים, ועל ידי מתן אפשרות למשתמשים ליצור קבצים חדשים בכל מקום שהם רוצים, וכל זאת תוך שמירה על פונקציונליות בדפדפנים שלא תומכים ב-File System Access API. הספרייה browser-fs-access מקלה על החיים שלכם כי היא מטפלת בניואנסים של שיפור מתקדם, והופכת את הקוד לפשוט ככל האפשר.
תודות
המאמר הזה נבדק על ידי Joe Medley ו-Kayce Basques. תודה לתורמים ל-Excalidraw על העבודה שלהם בפרויקט ועל בדיקת בקשות המיזוג שלי. תמונה ראשית (Hero) מאת Ilya Pavlov ב-Unsplash.