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. In dem Moment, in dem du genauer hinsiehst, ist jedoch nicht alles Gold, was glänzt.
Die traditionelle Dateiverwaltung
Dateien werden geöffnet
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 werden geöffnet
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.
Speichern (statt Herunterladen) von Dateien
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.
Die File System Access API
Die File System Access API vereinfacht sowohl das Öffnen als auch das Speichern erheblich. Außerdem wird echtes Speichern aktiviert, das heißt, Sie können nicht nur auswählen, wo eine Datei gespeichert werden soll, sondern auch eine vorhandene Datei überschreiben.
Dateien werden geöffnet
Mit der File System Access API ist das Öffnen einer Datei mit einem einzigen Aufruf der window.showOpenFilePicker()
-Methode möglich.
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 durch Aufrufen von window.showDirectoryPicker()
. Dadurch werden Verzeichnisse im Dialogfeld „Datei“ 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
So gut die File System Access API auch ist, sie ist aber noch nicht allgemein verfügbar.
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.
Die Bibliothek ist also kein Polyfill, sondern ein Ponyfill.
Sie können (statisch oder dynamisch) exklusiv alle 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 in der Bibliothek geprüft, 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 skizzieren 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. Daher eignet sie sich hervorragend für die Bibliothek browser-fs-access.
Ich kann beispielsweise eine Zeichnung auf meinem iPhone starten, sie in meinem iPhone-Ordner „Downloads“ speichern, die Datei auf meinem iPhone öffnen (nach der Übertragung von meinem Smartphone), sie ändern und mit meinen Änderungen überschreiben oder sie sogar als neue Datei speichern.
Codebeispiel aus der Praxis
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 Hauptsymbolleiste der App von Excalidraw auf dem iPhone und in der Chrome-Desktopversion.
Auf dem iPhone fehlt die Schaltfläche Als.
Schlussfolgerungen
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.