Browser können schon seit Langem mit Dateien und Verzeichnissen umgehen. Die File API bietet Funktionen zur Darstellung von Dateiobjekten in Webanwendungen sowie zur programmatischen Auswahl von Dateiobjekten und für den Zugriff auf ihre Daten. Wenn man jedoch genau hinsieht, ist nicht alles Gold glänzend.
Die traditionelle Art der Verarbeitung von Dateien
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 wie im folgenden Codebeispiel aussehen.
Das input
-Objekt liefert ein FileList
, das im folgenden Fall aus nur einem File
besteht.
Ein File
ist eine bestimmte Art von Blob
und kann in jedem Kontext verwendet werden, in dem ein Blob möglich ist.
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.
Ansonsten funktioniert alles wie oben.
Trotz des Anbieternamens kann webkitdirectory
nicht nur in Chromium- und WebKit-Browsern verwendet werden, sondern auch im Legacy-EdgeHTML-basierten Edge und in Firefox.
Speichern (statt Herunterladen) von Dateien
Wenn Sie eine Datei speichern möchten, müssen Sie sie normalerweise nur herunterladen. Dies funktioniert dank des Attributs <a download>
.
Bei einem Blob können Sie das Attribut href
des Ankers auf eine blob:
-URL festlegen, die Sie über die 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 Download-Ansatzes besteht darin, dass es keine Möglichkeit gibt, einen klassischen Ablauf des Typs Öffnen → Bearbeiten → Speichern durchzuführen. Es gibt also keine Möglichkeit, die Originaldatei zu überschreiben. Stattdessen erhalten Sie bei jedem "Speichern" eine neue Kopie der Originaldatei im Standard-Downloadordner des Betriebssystems.
File System Access API
Die File System Access API vereinfacht sowohl das Öffnen als auch das Speichern von Dateien. Außerdem wird das echte Speichern ermöglicht. Das bedeutet, dass Sie nicht nur auswählen können, wo eine Datei gespeichert werden soll, sondern dass Sie auch eine vorhandene Datei überschreiben können.
Dateien öffnen
Mit der File System Access API muss eine Datei mit einem Aufruf der window.showOpenFilePicker()
-Methode geöffnet werden.
Dieser Aufruf gibt ein Datei-Handle zurück, mit dem Sie die tatsächliche File
über die getFile()
-Methode 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 durch Aufrufen von window.showDirectoryPicker()
ein Verzeichnis. Dadurch werden Verzeichnisse im Dateidialogfeld auswählbar.
Dateien werden gespeichert
Das Speichern von Dateien ist ähnlich einfach.
Von einem Datei-Handle erstellen Sie über createWritable()
einen beschreibbaren Stream. Anschließend schreiben Sie die Blob-Daten, indem Sie die write()
-Methode des Streams aufrufen. Zum Schluss schließen Sie den Stream, indem Sie seine close()
-Methode aufrufen.
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
Auch wenn die File System Access API so perfekt ist, ist sie noch nicht allgemein verfügbar.
Aus diesem Grund betrachte ich die File System Access API als fortschrittliche Verbesserung. Daher möchte ich es verwenden, wenn es vom Browser unterstützt wird, und ansonsten den traditionellen Ansatz, ohne den Nutzer mit unnötigen Downloads von nicht unterstütztem JavaScript-Code zu bestrafen. Die Bibliothek browser-fs-access ist meine Antwort auf diese Herausforderung.
Designphilosophie
Da sich die File System Access API in Zukunft wahrscheinlich noch ändern wird, wird die browser-fs-access API nicht entsprechend modelliert.
Das heißt, die Bibliothek ist kein polyfill, sondern ein Ponyfill.
Sie können (statisch oder dynamisch) ausschließlich alle Funktionen importieren, die Sie benötigen, um Ihre App so klein wie möglich zu halten.
Die verfügbaren Methoden sind die passend benannten Methoden fileOpen()
, directoryOpen()
und fileSave()
.
Intern erkennt die Bibliotheksfunktion, ob die File System Access API unterstützt wird, und importiert dann den entsprechenden Codepfad.
Bibliothek „browser-fs-access“ verwenden
Die drei Methoden sind intuitiv zu bedienen.
Sie können die akzeptierte mimeTypes
oder die Datei extensions
Ihrer Anwendung angeben und ein multiple
-Flag setzen, um die Auswahl mehrerer Dateien oder Verzeichnisse zuzulassen oder zu verbieten.
Ausführliche Informationen finden Sie in der Dokumentation zur browser-fs-access API.
Im Codebeispiel unten sehen Sie, 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
Du kannst dir den obigen Code in einer Demo auf Glitch ansehen. Auch der Quellcode ist dort verfügbar. Da aus Sicherheitsgründen ursprungsübergreifende Subframes keine Dateiauswahl anzeigen dürfen, kann die Demo nicht in diesen Artikel eingebettet werden.
Die neue Browser-FS-Access-Bibliothek
In meiner Freizeit trage ich ein kleines bisschen zu einer installierbaren PWA namens Excalidraw bei, einem Whiteboard-Tool, mit dem sich Diagramme ganz einfach handgezeichnet lassen können. Sie ist vollständig responsiv und funktioniert gut auf einer Reihe von Geräten von kleinen Mobiltelefonen bis hin zu Computern mit großen Bildschirmen. Das bedeutet, dass es mit Dateien auf den verschiedenen Plattformen umgehen muss, unabhängig davon, ob sie die File System Access API unterstützen oder nicht. Dies macht sie zu einem hervorragenden Kandidat für die Bibliothek „browser-fs-access“.
Ich kann zum Beispiel eine Zeichnung auf meinem iPhone starten, sie herunterladen (technisch gesehen: Download, da Safari die File System Access API nicht unterstützt) in meinem iPhone-Downloadordner, öffne die Datei auf meinem Desktop (nach der Übertragung von meinem Smartphone), bearbeite sie und überschreibe sie mit meinen Änderungen oder speichere sie sogar als neue Datei.
Codebeispiel aus der Praxis
Unten sehen Sie ein Praxisbeispiel für browser-fs-access, wie er in Excalidraw verwendet wird.
Dieser Auszug stammt aus /src/data/json.ts
.
Von besonderem Interesse ist die Art und Weise, wie die Methode saveAsJSON()
entweder ein Datei-Handle oder null
an die Methode fileSave()
von browser-fs-access übergibt. Dadurch wird sie überschrieben, wenn ein Handle angegeben ist, oder dass sie in einer neuen Datei gespeichert wird, falls 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 in Excalidraw oder Ihrer Anwendung, sollte sich die Benutzeroberfläche an die Supportsituation 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 Speichern unter anzeigen lassen.
Die folgenden Screenshots zeigen den Unterschied zwischen der Symbolleiste der responsiven Haupt-App von Excalidraw auf dem iPhone und auf der Chrome-Desktop-App.
Wie Sie sehen, fehlt auf dem iPhone die Schaltfläche Speichern unter.
Ergebnisse
Die Arbeit mit Systemdateien funktioniert technisch in allen modernen Browsern. Bei Browsern, die die File System Access API unterstützen, können Sie die Abläufe optimieren, indem Sie Dateien richtig speichern und überschreiben (nicht nur herunterladen) und Ihre Nutzer jederzeit neue Dateien erstellen können. In Browsern, die die File System Access API nicht unterstützen, bleiben die Funktionen erhalten. Der browser-fs-access erleichtert Ihnen das Leben, da er die Feinheiten Progressive Enhancement angeht und Ihren Code so einfach wie möglich gestaltet.
Danksagungen
Dieser Artikel wurde von Joe Medley und Kayce Basques geprüft. vielen Dank an die Mitwirkenden zu Excalidraw für ihre Arbeit an dem Projekt und für die Prüfung meiner Pull-Anfragen. Hero-Image von Ilya Pavlov auf Unsplash.