Browsers kunnen al heel lang met bestanden en mappen omgaan. De File API biedt functies voor het weergeven van bestandsobjecten in webapplicaties, het programmatisch selecteren ervan en het verkrijgen van toegang tot hun gegevens. Maar op het moment dat je beter kijkt, is het niet alleen goud wat blinkt.
De traditionele manier van omgaan met bestanden
Bestanden openen
Als ontwikkelaar kun je bestanden openen en lezen via het <input type="file">
element. In de eenvoudigste vorm kan het openen van een bestand er ongeveer uitzien als het onderstaande codevoorbeeld. Het input
geeft u een FileList
, die in het onderstaande geval uit slechts één File
bestaat. Een File
is een specifiek soort Blob
en kan in elke context worden gebruikt waarin een Blob dat kan.
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 mappen) kunt u het <input webkitdirectory>
attribuut instellen. Afgezien daarvan werkt al het andere hetzelfde als hierboven. Ondanks de door de leverancier vooraf ingestelde naam is webkitdirectory
niet alleen bruikbaar in Chromium- en WebKit-browsers, maar ook in de oudere, op EdgeHTML gebaseerde Edge en in Firefox.
Bestanden opslaan (beter: downloaden).
Voor het opslaan van een bestand bent u traditioneel beperkt tot het downloaden van een bestand, wat werkt dankzij het <a download>
attribuut. Bij een blob kunt u het href
kenmerk van het anker instellen op een blob:
URL die u kunt verkrijgen 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 enorm nadeel van de downloadaanpak is dat er geen manier is om een klassieke open → bewerken → opslaan-stroom te laten plaatsvinden, dat wil zeggen dat er geen manier is om het originele bestand te overschrijven . In plaats daarvan krijgt u telkens wanneer u "opslaat" een nieuwe kopie van het originele bestand in de standaardmap Downloads van het besturingssysteem.
De API voor bestandssysteemtoegang
De File System Access API maakt beide handelingen, openen en opslaan, een stuk eenvoudiger. Het maakt ook echt opslaan 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 methode window.showOpenFilePicker()
. Deze aanroep retourneert een bestandsingang, waaruit u het daadwerkelijke 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 map door window.showDirectoryPicker()
aan te roepen, waardoor mappen selecteerbaar worden in het bestandsdialoogvenster.
Bestanden opslaan
Het opslaan van bestanden is eveneens eenvoudig. Vanuit een bestandsingang maak je een beschrijfbare stream via createWritable()
, vervolgens schrijf je de Blob-gegevens door de write()
methode van de stream aan te roepen, en ten slotte 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-toegang
Hoe prima de File System Access API ook is, hij is nog niet algemeen beschikbaar .
Daarom zie ik de File System Access API als een progressieve verbetering . Daarom wil ik het gebruiken wanneer de browser het ondersteunt, en anders de traditionele aanpak gebruiken; En dit alles terwijl de gebruiker nooit wordt gestraft 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 er niet naar gemodelleerd. Dat wil zeggen dat de bibliotheek geen polyfill is, maar eerder een ponyfill . U kunt (statisch of dynamisch) uitsluitend de functionaliteit importeren die u nodig heeft om uw app zo klein mogelijk te houden. De beschikbare methoden zijn de toepasselijke namen fileOpen()
, directoryOpen()
en fileSave()
. Intern detecteert de bibliotheekfunctie of de File System Access API wordt ondersteund en importeert vervolgens het overeenkomstige 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 opgeven en een multiple
vlag instellen om de selectie van meerdere bestanden of mappen toe te staan of te weigeren. Zie de browser-fs-access API-documentatie voor volledige details. 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',
});
})();
Demo
Je kunt de bovenstaande code in actie zien in een demo op Glitch. De broncode is daar eveneens beschikbaar. Omdat om veiligheidsredenen cross-origine subframes 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 beetje bij aan een installeerbare PWA genaamd Excalidraw , een whiteboard-tool waarmee je eenvoudig diagrammen kunt schetsen met een handgetekend gevoel. Het is volledig responsief en werkt goed op een reeks apparaten, van kleine mobiele telefoons tot computers met grote schermen. Dit betekent dat het bestanden op alle verschillende platforms moet verwerken, ongeacht of deze de File System Access API ondersteunen of niet. Dit maakt het een geweldige kandidaat voor de browser-fs-access bibliotheek.
Ik kan bijvoorbeeld een tekening op mijn iPhone starten, deze opslaan (technisch: downloaden, aangezien Safari de File System Access API niet ondersteunt) in de map Downloads van mijn iPhone, het bestand openen op mijn bureaublad (nadat ik het van mijn iPhone heb overgezet) phone), wijzig het bestand en overschrijf het met mijn wijzigingen, of sla het zelfs op als een nieuw bestand.
Codevoorbeeld uit het echte leven
Hieronder ziet u een feitelijk voorbeeld van browser-fs-toegang zoals het wordt gebruikt in Excalidraw. Dit fragment is afkomstig uit /src/data/json.ts
. Van bijzonder belang is hoe de methode saveAsJSON()
een bestandsingang of null
doorgeeft aan de browser-fs-access' fileSave()
methode, waardoor deze wordt overschreven wanneer een ingang wordt gegeven, of wordt opgeslagen in een nieuw bestand 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 uw 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) {}
) kunt u naast een knop Opslaan ook een knop Opslaan als weergeven . De onderstaande schermafbeeldingen laten het verschil zien tussen de responsieve hoofdapp-werkbalk van Excalidraw op iPhone en op Chrome-desktop. Merk op dat op de iPhone de knop Opslaan als ontbreekt.
Conclusies
Werken met systeembestanden werkt technisch gezien op alle moderne browsers. Op browsers die de File System Access API ondersteunen, kunt u de ervaring verbeteren door bestanden daadwerkelijk op te slaan en te overschrijven (niet alleen downloaden) en door uw gebruikers nieuwe bestanden te laten maken waar ze maar willen, terwijl ze functioneel blijven in browsers die dat wel doen ondersteunt de File System Access API niet. De browser-fs-toegang maakt uw leven gemakkelijker door de subtiliteiten van progressieve verbeteringen 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 bijdragers aan Excalidraw voor hun werk aan het project en voor het beoordelen van mijn Pull Requests. Heldenafbeelding door Ilya Pavlov op Unsplash.