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

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 .

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 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.

Een Excalidraw-tekening op een iPhone.
Een Excalidraw-tekening starten op een iPhone waarop de File System Access API niet wordt ondersteund, maar waar een bestand kan worden opgeslagen (gedownload) in de map Downloads.
De gewijzigde Excalidraw-tekening in Chrome op het bureaublad.
Het openen en wijzigen van de Excalidraw-tekening op het bureaublad waarop de File System Access API wordt ondersteund en het bestand dus toegankelijk is via de API.
Het originele bestand met de wijzigingen overschrijven.
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 opslaan in een nieuw Excalidraw-bestand. Het originele bestand blijft onaangeroerd.

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.

Excalidraw app-werkbalk op iPhone met slechts een knop 'Opslaan'.
Excalidraw-appwerkbalk op iPhone met slechts een knop Opslaan .
Excalidraw app-werkbalk op Chrome-desktop met een knop 'Opslaan' en 'Opslaan als'.
Excalidraw-appwerkbalk in Chrome met een knop Opslaan en een gerichte knop Opslaan als .

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.