תאריך פרסום: 27 ביולי 2020
הדפדפנים יודעים להתמודד עם קבצים וספריות כבר הרבה זמן. 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 היא הפתרון שלי לאתגר הזה.
תפיסת העיצוב
מכיוון שסביר להניח ש-File System Access API עדיין ישתנה בעתיד, לא נעשה שימוש במודל שלו ב-browser-fs-access API.
כלומר, הספרייה היא לא polyfill, אלא ponyfill.
אתם יכולים לייבא (באופן סטטי או דינמי) באופן בלעדי את הפונקציונליות שאתם צריכים כדי לשמור על גודל האפליקציה קטן ככל האפשר.
השיטות הזמינות הן fileOpen(), directoryOpen() ו-fileSave().
באופן פנימי, הספרייה מזהה אם יש תמיכה ב-File System Access API, ואז מייבאת את נתיב הקוד המתאים.
שימוש בספרייה
השימוש בשלוש השיטות האלה הוא אינטואיטיבי.
אתם יכולים לציין את 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() מעבירה או את ה-handle של הקובץ או את null לפונקציה fileSave() של browser-fs-access, מה שגורם לה לדרוס את הקובץ אם ניתן handle, או לשמור בקובץ חדש אם לא ניתן 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) {}),
אפשר להציג לחצן שמירה בשם בנוסף ללחצן שמירה.
בצילומי המסך שלמטה אפשר לראות את ההבדל בין סרגל הכלים הראשי של אפליקציית Excalidraw שמותאם לנייד באייפון לבין סרגל הכלים הראשי של אפליקציית Excalidraw ב-Chrome במחשב.
שימו לב שבמכשיר אייפון לא מופיע הלחצן שמירה בשם.
מסקנות
מבחינה טכנית, אפשר לעבוד עם קבצי מערכת בכל הדפדפנים המודרניים. בדפדפנים שתומכים ב-File System Access API, אפשר לשפר את חוויית השימוש על ידי מתן אפשרות לשמירה ולשכתוב אמיתיים של קבצים (ולא רק להורדה), ועל ידי מתן אפשרות למשתמשים ליצור קבצים חדשים בכל מקום שהם רוצים, וכל זאת תוך שמירה על פונקציונליות בדפדפנים שלא תומכים ב-File System Access API. הספרייה browser-fs-access מקלה על החיים שלכם כי היא מטפלת בניואנסים של שיפור מתקדם, והופכת את הקוד לפשוט ככל האפשר.
תודות
המאמר הזה נבדק על ידי Joe Medley ועל ידי Kayce Basques. תודה לתורמים ל-Excalidraw על העבודה שלהם בפרויקט ועל בדיקת בקשות המיזוג שלי.