Browsers kunnen al lang met bestanden en mappen overweg. De File API biedt functies voor het weergeven van bestandsobjecten in webapplicaties, evenals voor het programmatisch selecteren en openen van hun gegevens. Maar als je beter kijkt, zie je dat het niet alleen goud is wat er blinkt.
De traditionele manier om met bestanden om te gaan
Bestanden openen
Als ontwikkelaar kun je bestanden openen en lezen via het element <input type="file">
. In de eenvoudigste vorm kan het openen van een bestand er ongeveer zo uitzien als het onderstaande codevoorbeeld. Het input
geeft je een FileList
, die in het onderstaande geval uit slechts één File
bestaat. Een File
is een specifiek type Blob
en kan in elke context worden gebruikt waarin een Blob kan worden gebruikt.
const openFile = async () => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.addEventListener('change', () => {
resolve(input.files[0]);
});
input.click();
});
};
Mappen openen
Voor het openen van mappen (of directories) kunt u het kenmerk <input webkitdirectory>
instellen. Verder werkt alles hetzelfde als hierboven. Ondanks de naam met het leveranciersprefix is webkitdirectory
niet alleen bruikbaar in Chromium- en WebKit-browsers, maar ook in de oudere EdgeHTML-gebaseerde Edge en Firefox.
Bestanden opslaan (of beter: downloaden)
Om een bestand op te slaan, bent u traditioneel beperkt tot het downloaden van een bestand, wat werkt dankzij het <a download>
-attribuut. Gegeven een blob kunt u het href
-attribuut van het anker instellen op een blob:
URL die u kunt ophalen via de methode 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();
};
Het probleem
Een groot nadeel van de downloadmethode is dat er geen klassieke open→bewerken→opslaan-flow mogelijk is, dat wil zeggen dat het originele bestand niet overschreven kan worden. In plaats daarvan krijg je een nieuwe kopie van het originele bestand in de standaardmap Downloads van het besturingssysteem telkens wanneer je "opslaat".
De API voor toegang tot het bestandssysteem
De File System Access API maakt beide bewerkingen, openen en opslaan, een stuk eenvoudiger. Het maakt ook 'true saving' mogelijk, dat wil zeggen dat u niet alleen kunt kiezen waar u een bestand wilt opslaan, maar ook een bestaand bestand kunt overschrijven.
Bestanden openen
Met de File System Access API is het openen van een bestand een kwestie van één aanroep van de window.showOpenFilePicker()
methode. Deze aanroep retourneert een bestandshandle, waarvan u het eigenlijke File
kunt ophalen via de getFile()
methode.
const openFile = async () => {
try {
// Always returns an array.
const [handle] = await window.showOpenFilePicker();
return handle.getFile();
} catch (err) {
console.error(err.name, err.message);
}
};
Mappen openen
Open een directory door window.showDirectoryPicker()
aan te roepen. Hierdoor worden directories selecteerbaar in het bestandsdialoogvenster.
Bestanden opslaan
Het opslaan van bestanden is net zo eenvoudig. Vanuit een bestandshandle maak je een schrijfbare stream aan via createWritable()
, vervolgens schrijf je de Blob-gegevens door de write()
methode van de stream aan te roepen en tot slot sluit je de stream door de close()
methode aan te roepen.
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);
}
};
Introductie van browser-fs-access
Hoewel de File System Access API prima is, is deze nog niet overal beschikbaar .

Daarom zie ik de File System Access API als een progressieve verbetering . Daarom wil ik deze gebruiken wanneer de browser deze ondersteunt, en de traditionele aanpak gebruiken als dat niet het geval is; en dat alles zonder de gebruiker te straffen met onnodige downloads van niet-ondersteunde JavaScript-code. De browser-fs-access- bibliotheek is mijn antwoord op deze uitdaging.
Ontwerpfilosofie
Omdat de File System Access API in de toekomst waarschijnlijk nog zal veranderen, is de browser-fs-access API hier niet naar gemodelleerd. Dat wil zeggen, de bibliotheek is geen polyfill , maar eerder een ponyfill . U kunt (statisch of dynamisch) exclusief alle functionaliteit importeren die u nodig hebt om uw app zo klein mogelijk te houden. De beschikbare methoden zijn de toepasselijk genaamde fileOpen()
, directoryOpen()
en fileSave()
. Intern detecteert de bibliotheek of de File System Access API wordt ondersteund en importeert vervolgens het bijbehorende codepad.
De browser-fs-access-bibliotheek gebruiken
De drie methoden zijn intuïtief in gebruik. U kunt de geaccepteerde mimeTypes
of extensions
van uw app specificeren en een multiple
vlag instellen om de selectie van meerdere bestanden of mappen toe te staan of te weigeren. Raadpleeg de documentatie van de browser-fs-access API voor meer informatie. Het onderstaande codevoorbeeld laat zien hoe u afbeeldingsbestanden kunt openen en opslaan.
// 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',
});
})();
Demonstratie
Je kunt de bovenstaande code in actie zien in een demo op GitHub. De broncode is daar eveneens beschikbaar. Omdat cross-origin subframes om veiligheidsredenen geen bestandskiezer mogen tonen, kan de demo niet in dit artikel worden ingesloten.
De browser-fs-access-bibliotheek in het wild
In mijn vrije tijd draag ik een klein steentje bij aan een installeerbare PWA genaamd Excalidraw , een whiteboardtool waarmee je eenvoudig diagrammen kunt tekenen met een handgetekende look. De tool is volledig responsief en werkt goed op diverse apparaten, van kleine mobiele telefoons tot computers met grote schermen. Dit betekent dat de tool bestanden op alle verschillende platforms moet kunnen verwerken, ongeacht of ze de File System Access API ondersteunen of niet. Dit maakt het een uitstekende kandidaat voor de browser-fs-access-bibliotheek.
Ik kan bijvoorbeeld een tekening beginnen op mijn iPhone, deze opslaan (technisch gezien: downloaden, aangezien Safari de File System Access API niet ondersteunt) in de map Downloads op mijn iPhone, het bestand openen op mijn bureaublad (nadat ik het heb overgezet vanaf mijn telefoon), het bestand wijzigen en overschrijven met mijn wijzigingen, of het zelfs opslaan als een nieuw bestand.




Voorbeeld van een code uit het echte leven
Hieronder ziet u een actueel voorbeeld van browser-fs-access zoals het wordt gebruikt in Excalidraw. Dit fragment is afkomstig uit /src/data/json.ts
. Bijzonder interessant is hoe de methode saveAsJSON()
een bestandshandle of null
doorgeeft aan de fileSave()
-methode van browser-fs-access, waardoor deze de handle overschrijft wanneer deze wordt opgegeven, of in een nieuw bestand opslaat als dat niet het geval is.
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);
};
UI-overwegingen
Of het nu in Excalidraw of in je app is, de gebruikersinterface moet zich aanpassen aan de ondersteuningssituatie van de browser. Als de File System Access API wordt ondersteund ( if ('showOpenFilePicker' in window) {}
), kun je naast de knop Opslaan ook een knop Opslaan als weergeven . De onderstaande schermafbeeldingen tonen het verschil tussen de responsieve werkbalk van de Excalidraw-app op de iPhone en op Chrome Desktop. Merk op dat de knop Opslaan als op de iPhone ontbreekt.


Conclusies
Werken met systeembestanden werkt technisch gezien in alle moderne browsers. In browsers die de File System Access API ondersteunen, kunt u de ervaring verbeteren door bestanden daadwerkelijk op te slaan en te overschrijven (niet alleen te downloaden) en door uw gebruikers de mogelijkheid te bieden om nieuwe bestanden te maken waar ze maar willen, terwijl alles gewoon werkt in browsers die de File System Access API niet ondersteunen. De browser-fs-access maakt uw leven gemakkelijker door de subtiliteiten van progressieve verbetering aan te pakken en uw code zo eenvoudig mogelijk te maken.
Dankbetuigingen
Dit artikel is beoordeeld door Joe Medley en Kayce Basques . Dank aan de medewerkers van Excalidraw voor hun werk aan het project en voor het beoordelen van mijn pull requests. Hero-afbeelding van Ilya Pavlov op Unsplash.