Lettura e scrittura di file e directory con la libreria browser-fs-access

I browser sono in grado di gestire file e directory da molto tempo. L'API File fornisce funzionalità per la rappresentazione di oggetti file nelle applicazioni web, nonché per selezionarli e accedere ai relativi dati in modo programmatico. Tuttavia, se guardi più da vicino, scoprirai che non è tutto oro quel che luccica.

Il modo tradizionale di gestire i file

Apertura dei file

In qualità di sviluppatore, puoi aprire e leggere i file tramite l'elemento <input type="file">. Nella sua forma più semplice, l'apertura di un file può essere simile all'esempio di codice riportato di seguito. L'oggetto input fornisce un FileList, che nel caso riportato di seguito è costituito da un solo File. Un File è un tipo specifico di Blob e può essere utilizzato in qualsiasi contesto in cui è possibile utilizzare un blob.

const openFile = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

Apertura di directory

Per aprire cartelle (o directory), puoi impostare l'attributo <input webkitdirectory>. A parte questo, tutto il resto funziona come sopra. Nonostante il nome con prefisso del fornitore, webkitdirectory non è utilizzabile solo nei browser Chromium e WebKit, ma anche nell'Edge precedente basato su EdgeHTML e in Firefox.

Salvataggio (o download) di file

Per salvare un file, in genere, puoi solo scaricarlo, grazie all'attributo <a download>. Dato un blob, puoi impostare l'attributo href dell'ancora su un URL blob: che puoi ottenere dal metodo 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();
};

Il problema

Uno svantaggio enorme dell'approccio di download è che non è possibile eseguire un flusso classico di apertura, modifica e salvataggio, ovvero non è possibile sovrascrivere il file originale. Al contrario, ogni volta che selezioni "Salva", ottieni una nuova copia del file originale nella cartella Download predefinita del sistema operativo.

L'API File System Access

L'API File System Access semplifica notevolmente entrambe le operazioni, apertura e salvataggio. Consente inoltre il salvataggio effettivo, ovvero non solo puoi scegliere dove salvare un file, ma anche sovrascrivere un file esistente.

Apertura dei file

Con l'API File System Access, l'apertura di un file è questione di una chiamata al metodo window.showOpenFilePicker(). Questa chiamata restituisce un handle file, da cui puoi ottenere il file File effettivo tramite il metodo getFile().

const openFile = async () => {
  try {
    // Always returns an array.
    const [handle] = await window.showOpenFilePicker();
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Apertura di directory

Apri una directory chiamando window.showDirectoryPicker() che rende le directory selezionabili nella finestra di dialogo del file.

Salvataggio dei file

Anche il salvataggio dei file è altrettanto semplice. Da un handle file, crei uno stream scrivibile tramite createWritable(), poi scrivi i dati del blob chiamando il metodo write() dello stream e infine chiudi lo stream chiamando il relativo metodo close().

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);
  }
};

Introduzione a browser-fs-access

L'API File System Access è perfettamente valida, ma non è ancora ampiamente disponibile.

Tabella di supporto dei browser per l&#39;API File System Access. Tutti i browser sono contrassegnati come &quot;Nessun supporto&quot; o &quot;Con un flag&quot;.
Tabella di supporto dei browser per l'API File System Access. (Fonte)

Per questo motivo, considero l'API File System Access un miglioramento progressivo. Pertanto, voglio utilizzarlo quando il browser lo supporta e, in caso contrario, utilizzare l'approccio tradizionale, senza mai punire l'utente con download non necessari di codice JavaScript non supportato. La libreria browser-fs-access è la mia risposta a questa sfida.

Filosofia del design

Poiché l'API File System Access potrebbe ancora cambiare in futuro, l'API browser-fs-access non è modellata su di essa. In altre parole, la libreria non è un polyfill, ma un ponyfill. Puoi importare (staticamente o dinamicamente) esclusivamente le funzionalità di cui hai bisogno per mantenere l'app il più piccola possibile. I metodi disponibili sono fileOpen(), directoryOpen() e fileSave(). All'interno, la funzionalità della libreria rileva se l'API Accesso al file system è supportata, quindi importa il percorso del codice corrispondente.

Utilizzo della libreria browser-fs-access

I tre metodi sono intuitivi da utilizzare. Puoi specificare il mimeTypes o il file extensions accettati dalla tua app e impostare un flag multiple per consentire o meno la selezione di più file o directory. Per tutti i dettagli, consulta la documentazione dell'API browser-fs-access. L'esempio di codice seguente mostra come aprire e salvare i file immagine.

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

Puoi vedere il codice riportato sopra in azione in una demo su Glitch. Anche il codice sorgente è disponibile lì. Poiché per motivi di sicurezza i frame secondari di origine diversa non sono autorizzati a mostrare un selettore di file, la demo non può essere incorporata in questo articolo.

La libreria browser-fs-access in uso

Nel tempo libero, collaboro a una PWA installabile chiamata Excalidraw, uno strumento per lavagne che ti consente di disegnare facilmente diagrammi con un'esperienza simile a quella di un disegno a mano. È completamente adattabile e funziona bene su una serie di dispositivi, dai piccoli cellulari ai computer con schermi di grandi dimensioni. Ciò significa che deve gestire i file su tutte le varie piattaforme, indipendentemente dal fatto che supportino o meno l'API Accesso al file system. Questo lo rende un ottimo candidato per la libreria browser-fs-access.

Ad esempio, posso iniziare un disegno sul mio iPhone, salvarlo (tecnicamente: scaricarlo, poiché Safari non supporta l'API File System Access) nella cartella Download del mio iPhone, aprire il file sul mio computer (dopo averlo trasferito dal telefono), modificarlo e sovrascriverlo con le mie modifiche o addirittura salvarlo come nuovo file.

Un disegno di Excalidraw su un iPhone.
Avvio di un disegno di Excalidraw su un iPhone in cui l'API File System Access non è supportata, ma in cui è possibile salvare (scaricare) un file nella cartella Download.
Il disegno di Excalidraw modificato su Chrome sul computer.
Aprire e modificare il disegno di Excalidraw sul computer in cui è supportata l'API File System Access, in modo da poter accedere al file tramite l'API.
Sovrascrivendo il file originale con le modifiche.
Sostituzione del file originale con le modifiche al file del disegno Excalidraw originale. Il browser mostra una finestra di dialogo che mi chiede se va bene.
Salvare le modifiche in un nuovo file di disegno Excalidraw.
Salvare le modifiche in un nuovo file Excalidraw. Il file originale rimane invariato.

Esempio di codice reale

Di seguito è riportato un esempio pratico di browser-fs-access, così come viene utilizzato in Excalidraw. Questo estratto è tratto da /src/data/json.ts. Di particolare interesse è il modo in cui il metodo saveAsJSON() passa un handle file o null al metodo fileSave() di browser-fs-access, che lo sovrascrive quando viene fornito un handle o lo salva in un nuovo file se non è presente.

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);
};

Considerazioni relative all'interfaccia utente

Che si tratti di Excalidraw o della tua app, l'interfaccia utente deve adattarsi alla situazione di supporto del browser. Se l'API File System Access è supportata (if ('showOpenFilePicker' in window) {}), puoi mostrare un pulsante Salva come oltre a un pulsante Salva. Gli screenshot di seguito mostrano la differenza tra la barra degli strumenti dell'app principale adattabile di Excalidraw su iPhone e su Chrome per computer. Tieni presente che su iPhone non è presente il pulsante Salva come.

Barra degli strumenti dell&#39;app Excalidraw su iPhone con un solo pulsante &quot;Salva&quot;.
Barra degli strumenti dell'app Excalibur su iPhone con un solo pulsante Salva.
Barra degli strumenti dell&#39;app Excalidraw su Chrome per computer con i pulsanti &quot;Salva&quot; e &quot;Salva come&quot;.
Barra degli strumenti dell'app Excalibur su Chrome con i pulsanti Salva e Salva come attivi.

Conclusioni

Il lavoro con i file di sistema funziona tecnicamente su tutti i browser moderni. Sui browser che supportano l'API Accesso al file system, puoi migliorare l'esperienza consentendo un salvataggio e una sovrascrittura (non solo il download) effettivi dei file e consentendo agli utenti di creare nuovi file dove vogliono, il tutto rimanendo funzionale sui browser che non supportano l'API Accesso al file system. browser-fs-access semplifica la vita grazie al trattamento delle sfumature del miglioramento progressivo e alla semplificazione del codice.

Ringraziamenti

Questo articolo è stato esaminato da Joe Medley e Kayce Basques. Grazie ai collaboratori di Excalidraw per il loro lavoro sul progetto e per aver esaminato le mie richieste pull. Immagine hero di Ilya Pavlov su Unsplash.