Bestanden en mappen lezen en schrijven met de browser-fs-access bibliotheek

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 .

Browserondersteuningstabel voor de File System Access API. Alle browsers zijn gemarkeerd als 'geen ondersteuning' of 'achter een vlag'.
Browserondersteuningstabel voor de File System Access API. ( Bron )

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.

Een Excalidraw-tekening op een iPhone.
Een Excalidraw-tekening starten op een iPhone waarop de File System Access API niet wordt ondersteund, maar waarop een bestand kan worden opgeslagen (gedownload) in de map Downloads.
De aangepaste Excalidraw-tekening op Chrome op het bureaublad.
Openen en wijzigen van de Excalidraw-tekening op het bureaublad waar de File System Access API ondersteund wordt en het bestand dus via de API toegankelijk is.
Het originele bestand overschrijven met de wijzigingen.
Het originele bestand overschrijven met de wijzigingen in het originele Excalidraw-tekenbestand. De browser toont een dialoogvenster met de vraag of dit in orde is.
De wijzigingen opslaan in een nieuw Excalidraw-tekeningbestand.
De wijzigingen worden opgeslagen in een nieuw Excalidraw-bestand. Het originele bestand blijft ongewijzigd.

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.

Excalidraw-appwerkbalk op de iPhone met alleen een 'Opslaan'-knop.
Excalidraw-appwerkbalk op de iPhone met alleen een Bewaar -knop.
De Excalidraw-app-werkbalk op het Chrome-bureaublad met een 'Opslaan'- en een 'Opslaan als'-knop.
Excalidraw-app-werkbalk op Chrome met een Opslaan-knop en een gerichte Opslaan als -knop.

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.