Veröffentlicht: 27. Juli 2020
Browser können schon lange mit Dateien und Verzeichnissen umgehen. Die File API bietet Funktionen zum Darstellen von Datei-Objekten in Webanwendungen sowie zum programmgesteuerten Auswählen und Zugreifen auf ihre Daten. Bei genauerem Hinsehen stellt sich jedoch heraus, dass nicht alles Gold ist, was glänzt.
Der herkömmliche Umgang mit Dateien
Dateien öffnen
Sie können Dateien mit dem Element <input type="file"> öffnen und lesen.
In der einfachsten Form kann das Öffnen einer Datei so aussehen wie im Codebeispiel.
Das input-Objekt enthält ein FileList, das in unserem Beispiel nur aus einem File besteht.
Ein File ist eine spezielle Art von Blob und kann in jedem Kontext verwendet werden, in dem 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.
Ansonsten funktioniert alles wie oben beschrieben.
Trotz des Anbietervorsatzes im Namen ist webkitdirectory nicht nur in Chromium- und WebKit-Browsern, sondern auch im alten EdgeHTML-basierten Edge sowie in Firefox verfügbar.
Dateien speichern und herunterladen
Zum Speichern einer Datei sind Sie normalerweise darauf beschränkt, eine Datei herunterzuladen. Das funktioniert dank des Attributs <a download>.
Wenn Sie ein Blob haben, können Sie das Attribut href 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 Download-Ansatzes besteht darin, dass es keine Möglichkeit gibt, den klassischen Ablauf „Öffnen“ → „Bearbeiten“ → „Speichern“ zu nutzen, d. h. die Originaldatei zu überschreiben. Stattdessen wird bei jedem Speichern eine neue Kopie der Originaldatei 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 wird echtes Speichern ermöglicht. Sie können also auswählen, wo die Datei gespeichert werden soll, und eine vorhandene Datei überschreiben.
Dateien öffnen
Mit der File System Access API ist das Öffnen einer Datei ein Aufruf der Methode window.showOpenFilePicker().
Dieser Aufruf gibt ein Dateihandle zurück, über das Sie mit der Methode getFile() die tatsächliche File 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 ähnlich einfach.
Aus einem Dateihandle erstellen Sie mit createWritable() einen beschreibbaren Stream. Dann schreiben Sie die Blob-Daten, indem Sie die Methode write() des Streams aufrufen. Schließlich schließen Sie den Stream, indem Sie seine Methode close() 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);
}
};
browser-fs-access
Die File System Access API ist zwar eine gute Lösung, aber noch nicht weit verbreitet.
Deshalb betrachte ich die File System Access API als progressive Verbesserung. Daher möchte ich es verwenden, wenn der Browser es unterstützt, und den herkömmlichen Ansatz, wenn nicht. Dabei soll der Nutzer nie durch unnötige Downloads von nicht unterstütztem JavaScript-Code belastet werden. Die browser-fs-access-Bibliothek ist meine Antwort auf diese Herausforderung.
Designphilosophie
Da sich die File System Access API in Zukunft wahrscheinlich noch ändern wird, ist die browser-fs-access API nicht daran angelehnt.
Die Bibliothek ist also kein Polyfill, sondern ein 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 fileOpen(), directoryOpen() und fileSave().
Intern wird in der Bibliothek erkannt, ob die File System Access API unterstützt wird, und dann wird der entsprechende Codepfad importiert.
Bibliothek verwenden
Die drei Methoden sind intuitiv zu bedienen.
Sie können die akzeptierte mimeTypes oder Datei extensions Ihrer App angeben und ein multiple-Flag festlegen, um die Auswahl mehrerer Dateien oder Verzeichnisse zuzulassen oder zu verbieten.
Vollständige Informationen finden Sie in der API-Dokumentation für den Browser-Dateisystemzugriff.
Das Codebeispiel zeigt, wie Sie Bilddateien öffnen und speichern können.
// 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
Den Code in Aktion sehen Sie in einer GitHub-Demo. Dort ist auch der Quellcode verfügbar.
Die browser-fs-access-Bibliothek in der Praxis
In meiner Freizeit trage ich ein wenig zu einer installierbaren PWA namens Excalidraw bei, einem Whiteboard-Tool, mit dem Sie Diagramme mit einem handgezeichneten Look erstellen können. 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 sie Dateien auf allen verschiedenen Plattformen verarbeiten muss, unabhängig davon, ob sie die File System Access API unterstützen. Daher eignet sie sich hervorragend für die browser-fs-access-Bibliothek.
Ich kann beispielsweise eine Zeichnung auf meinem iPhone beginnen, sie speichern (technisch: herunterladen, da Safari die File System Access API nicht unterstützt) und in den Downloadordner auf meinem iPhone verschieben, die Datei auf meinem Computer öffnen (nachdem ich sie von meinem Smartphone übertragen habe), die Datei ändern und mit meinen Änderungen überschreiben oder sogar als neue Datei speichern.
Codebeispiel aus der Praxis
Unten sehen Sie ein echtes 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 Methode saveAsJSON() entweder ein Dateihandle oder null an die Methode fileSave() von browser-fs-access übergibt. Dadurch wird die Datei überschrieben, wenn ein Handle angegeben wird, oder in einer neuen Datei gespeichert, 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);
};
Überlegungen zur Benutzeroberfläche
Die Benutzeroberfläche sollte sich in Excalidraw oder Ihrer App 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 auch die Schaltfläche Speichern unter anzeigen.
Die Screenshots unten zeigen den Unterschied zwischen der responsiven Hauptsymbolleiste der Excalidraw App auf dem iPhone und in Chrome auf dem Computer.
Beachten Sie, dass die Schaltfläche Speichern unter auf dem iPhone fehlt.
Zusammenfassung
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 tatsächliche Speichern und Überschreiben (nicht nur das Herunterladen) von Dateien ermöglichen und Nutzern erlauben, neue Dateien zu erstellen, wo immer sie möchten. Gleichzeitig bleibt die Funktionalität in Browsern erhalten, die die File System Access API nicht unterstützen. Die browser-fs-access-Bibliothek macht Ihnen das Leben leichter, da sie sich um die Feinheiten der progressiven Verbesserung kümmert und Ihren Code so einfach wie möglich hält.
Danksagungen
Dieser Artikel wurde von Joe Medley und Kayce Basques geprüft. Vielen Dank an die Beitragenden von Excalidraw für ihre Arbeit am Projekt und für die Überprüfung meiner Pull Requests.