Browser können schon lange mit Dateien und Verzeichnissen umgehen. Die File API bietet Funktionen zum Darstellen von Dateiobjekten in Webanwendungen sowie zum programmgesteuerten Auswählen und Abrufen ihrer Daten. Bei näherem Hinsehen zeigt sich jedoch, dass nicht alles Gold ist, was glänzt.
Die traditionelle Dateiverwaltung
Dateien öffnen
Als Entwickler können Sie Dateien über das Element <input type="file">
öffnen und lesen.
In der einfachsten Form kann das Öffnen einer Datei in etwa so aussehen wie im folgenden Codebeispiel.
Das input
-Objekt gibt ein FileList
zurück, das im folgenden Fall nur aus einem File
besteht.
Eine File
ist eine bestimmte Art von Blob
und kann in jedem Kontext verwendet werden, in dem auch ein Blob verwendet werden kann.
const openFile = async () => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.addEventListener('change', () => {
resolve(input.files[0]);
});
input.click();
});
};
Verzeichnisse öffnen
Zum Öffnen von Ordnern (oder Verzeichnissen) können Sie das Attribut <input webkitdirectory>
festlegen.
Abgesehen davon funktioniert alles wie oben beschrieben.
Trotz des Namens mit dem Anbieterpräfix kann webkitdirectory
nicht nur in Chromium- und WebKit-Browsern, sondern auch im alten EdgeHTML-basierten Edge und in Firefox verwendet werden.
Dateien speichern (bzw. herunterladen)
Zum Speichern einer Datei sind Sie traditionell auf das Herunterladen einer Datei beschränkt. Das funktioniert dank des Attributs <a download>
.
Für einen Blob können Sie das href
-Attribut des Ankers auf eine blob:
-URL festlegen, die Sie mit der Methode URL.createObjectURL()
abrufen können.
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();
};
Das Problem
Ein großer Nachteil des Herunterladens ist, dass es keine Möglichkeit gibt, die klassische Abfolge „Öffnen → Bearbeiten → Speichern“ auszuführen. Das bedeutet, dass die Originaldatei nicht überschrieben werden kann. Stattdessen wird beim Speichern eine neue Kopie der ursprünglichen Datei im Standard-Downloadordner des Betriebssystems erstellt.
File System Access API
Mit der File System Access API werden sowohl das Öffnen als auch das Speichern von Dateien erheblich vereinfacht. Außerdem ist das echte Speichern möglich. Das bedeutet, dass Sie nicht nur auswählen können, wo eine Datei gespeichert werden soll, sondern auch eine vorhandene Datei überschreiben können.
Dateien öffnen
Mit der File System Access API können Sie eine Datei mit einem einzigen Aufruf der Methode window.showOpenFilePicker()
öffnen.
Dieser Aufruf gibt einen Dateihandle zurück, über den Sie die tatsächliche File
über die Methode getFile()
abrufen können.
const openFile = async () => {
try {
// Always returns an array.
const [handle] = await window.showOpenFilePicker();
return handle.getFile();
} catch (err) {
console.error(err.name, err.message);
}
};
Verzeichnisse öffnen
Öffnen Sie ein Verzeichnis, indem Sie window.showDirectoryPicker()
aufrufen. Dadurch werden Verzeichnisse im Dateidialogfeld auswählbar.
Dateien speichern
Das Speichern von Dateien ist ebenfalls ganz einfach.
Sie erstellen über createWritable()
einen beschreibbaren Stream aus einem Dateihandle, schreiben dann die Blob-Daten durch Aufrufen der Methode write()
des Streams und schließen den Stream schließlich durch Aufrufen der Methode 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);
}
};
Jetzt neu: browser-fs-access
Die File System Access API ist zwar sehr gut, aber noch nicht weit verbreitet.
Aus diesem Grund betrachte ich die File System Access API als progressive Verbesserung. Daher möchte ich es verwenden, wenn der Browser es unterstützt, und den traditionellen Ansatz verwenden, wenn nicht. Dabei möchte ich Nutzer nicht mit unnötigen Downloads von nicht unterstütztem JavaScript-Code bestrafen. Die Bibliothek browser-fs-access ist meine Antwort auf diese Herausforderung.
Designphilosophie
Da die File System Access API sich in Zukunft wahrscheinlich noch ändern wird, ist die browser-fs-access API nicht nach ihr modelliert.
Das heißt, die Bibliothek ist keine Polyfill, sondern eine Ponyfill.
Sie können (statisch oder dynamisch) ausschließlich die Funktionen importieren, die Sie benötigen, um Ihre App so klein wie möglich zu halten.
Die verfügbaren Methoden sind die treffend benannten
fileOpen()
,
directoryOpen()
und
fileSave()
.
Intern wird von der Bibliothek-Funktion erkannt, ob die File System Access API unterstützt wird, und dann wird der entsprechende Codepfad importiert.
Bibliothek „browser-fs-access“ verwenden
Die drei Methoden sind intuitiv zu bedienen.
Sie können die zulässige mimeTypes
oder Datei extensions
Ihrer App angeben und ein multiple
-Flag setzen, um die Auswahl mehrerer Dateien oder Verzeichnisse zuzulassen oder zu verhindern.
Weitere Informationen finden Sie in der API-Dokumentation für browser-fs-access.
Im folgenden Codebeispiel wird gezeigt, wie Sie Bilddateien öffnen und speichern.
// 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',
});
})();
Demo
In einer Demo auf Glitch können Sie sich den Code in Aktion ansehen. Der Quellcode ist dort ebenfalls verfügbar. Da in untergeordneten Frames mit unterschiedlichen Ursprüngen aus Sicherheitsgründen keine Dateiauswahl angezeigt werden darf, kann die Demo nicht in diesen Artikel eingebettet werden.
Die browser-fs-access-Bibliothek in der Praxis
In meiner Freizeit arbeite ich ein wenig an einer installierbaren PWA namens Excalidraw mit. Das ist ein Whiteboard-Tool, mit dem sich Diagramme ganz einfach zeichnen lassen, als wären sie handgezeichnet. Es ist vollständig responsiv und funktioniert auf einer Vielzahl von Geräten, von kleinen Smartphones bis hin zu Computern mit großen Bildschirmen. Das bedeutet, dass es mit Dateien auf allen verschiedenen Plattformen umgehen muss, unabhängig davon, ob sie die File System Access API unterstützen oder nicht. Das macht es zu einem guten Kandidaten für die browser-fs-access-Bibliothek.
Ich kann beispielsweise auf meinem iPhone ein Bild zeichnen, es im Downloads-Ordner meines iPhones speichern (technisch gesehen: herunterladen, da Safari die File System Access API nicht unterstützt) und die Datei auf meinem Computer öffnen (nachdem ich sie von meinem Smartphone übertragen habe). Ich kann die Datei dann ändern und mit meinen Änderungen überschreiben oder sie sogar als neue Datei speichern.
Praxisnahes Codebeispiel
Unten sehen Sie ein Beispiel für „browser-fs-access“, wie es in Excalidraw verwendet wird.
Dieser Auszug stammt aus /src/data/json.ts
.
Besonders interessant ist, wie die saveAsJSON()
-Methode entweder einen Dateihandle oder null
an die fileSave()
-Methode von browser-fs-access übergibt, wodurch die Datei überschrieben wird, wenn ein Handle angegeben ist, oder in einer neuen Datei gespeichert wird, wenn dies nicht der Fall ist.
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);
};
Hinweise zur Benutzeroberfläche
Unabhängig davon, ob Sie Excalidraw oder Ihre App verwenden, sollte sich die Benutzeroberfläche an die Unterstützung des Browsers anpassen.
Wenn die File System Access API unterstützt wird (if ('showOpenFilePicker' in window) {}
), können Sie zusätzlich zur Schaltfläche Speichern die Schaltfläche Als Datei speichern anzeigen.
Die folgenden Screenshots zeigen den Unterschied zwischen der responsiven Symbolleiste der Haupt-App von Excalidraw auf dem iPhone und in Chrome auf dem Computer.
Auf dem iPhone fehlt die Schaltfläche Als.
Ergebnisse
Die Arbeit mit Systemdateien ist technisch in allen modernen Browsern möglich. In Browsern, die die File System Access API unterstützen, können Sie die Nutzerfreundlichkeit verbessern, indem Sie das Speichern und Überschreiben (nicht nur das Herunterladen) von Dateien zulassen und es Nutzern ermöglichen, neue Dateien an beliebiger Stelle zu erstellen. Dabei bleibt die Funktionalität in Browsern erhalten, die die File System Access API nicht unterstützen. browser-fs-access macht Ihnen das Leben leichter, da es die Feinheiten der progressiven Verbesserung berücksichtigt und Ihren Code so einfach wie möglich macht.
Danksagungen
Dieser Artikel wurde von Joe Medley und Kayce Basques geprüft. Vielen Dank an die Mitwirkenden an Excalidraw für ihre Arbeit am Projekt und für die Überprüfung meiner Pull-Requests. Hero-Image von Ilya Pavlov auf Unsplash.