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 rappresentare gli oggetti file nelle applicazioni web, nonché per selezionarli in modo programmatico e accedere ai relativi dati. Ma se guardi più da vicino, 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 al codice di esempio riportato di seguito. L'oggetto input fornisce un FileList, che nel caso seguente è costituito da un solo File. Un File è un tipo specifico di Blob e può essere utilizzato in qualsiasi contesto in cui può essere utilizzato 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();
  });
};

Aprire directory

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

Salvataggio (o meglio, download) dei file

Per il salvataggio di un file, tradizionalmente, 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

Un enorme svantaggio dell'approccio di download è che non è possibile eseguire un flusso classico apertura→modifica→salvataggio, ovvero non è possibile sovrascrivere il file originale. Invece, ogni volta che "salvi", ottieni una nuova copia del file originale nella cartella Download predefinita del sistema operativo.

API File System Access

L'API File System Access semplifica notevolmente entrambe le operazioni, apertura e salvataggio. Consente inoltre il salvataggio vero e proprio, ovvero puoi non solo 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 richiede una sola chiamata al metodo window.showOpenFilePicker(). Questa chiamata restituisce un handle del file, da cui puoi ottenere l'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);
  }
};

Aprire directory

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

Salvataggio dei file

Il salvataggio dei file è altrettanto semplice. Da un handle di file, crei un flusso scrivibile tramite createWritable(), quindi scrivi i dati del blob chiamando il metodo write() del flusso e infine chiudi il flusso chiamando il 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 di browser-fs-access

Per quanto l'API File System Access sia perfetta, 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;dietro un flag&quot;.
Tabella del supporto del 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 utilizzare l'approccio tradizionale in caso contrario, senza mai penalizzare l'utente con download non necessari di codice JavaScript non supportato. La libreria browser-fs-access è la mia risposta a questa sfida.

Filosofia di progettazione

Poiché è probabile che l'API File System Access cambi in futuro, l'API browser-fs-access non è modellata su di essa. ovvero non è un polyfill, ma un ponyfill. Puoi importare (staticamente o dinamicamente) esclusivamente le funzionalità che ti servono per mantenere l'app il più piccola possibile. I metodi disponibili sono fileOpen(), directoryOpen() e fileSave(). Internamente, la libreria rileva se l'API File System Access è supportata e quindi importa il percorso del codice corrispondente.

Utilizzo della libreria browser-fs-access

I tre metodi sono intuitivi da usare. Puoi specificare il mimeTypes o il file extensions accettati dalla tua app e impostare un flag multiple per consentire o impedire la selezione di più file o directory. Per tutti i dettagli, consulta la documentazione dell'API browser-fs-access. L'esempio di codice riportato di seguito 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 precedente in azione in una demo su GitHub. Anche il suo codice sorgente è disponibile. Poiché per motivi di sicurezza i frame secondari cross-origin non sono autorizzati a mostrare un selettore di file, la demo non può essere incorporata in questo articolo.

La libreria browser-fs-access in natura

Nel tempo libero, contribuisco in minima parte a una PWA installabile chiamata Excalidraw, uno strumento di lavagna che ti consente di disegnare facilmente diagrammi con un aspetto disegnato a mano. È completamente reattivo e funziona bene su una vasta gamma 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 File System Access. Ciò 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 dell'iPhone, aprire il file sul mio computer (dopo averlo trasferito dallo smartphone), modificarlo e sovrascriverlo con le mie modifiche o persino 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 un file può essere salvato (scaricato) nella cartella Download.
Il disegno modificato di Excalidraw su Chrome sul computer.
Apertura e modifica del disegno di Excalidraw sul computer dove è supportata l'API File System Access e quindi è possibile accedere al file tramite l'API.
Sovrascrivendo il file originale con le modifiche.
Sovrascrittura del file originale con le modifiche apportate al file di disegno Excalidraw originale. Il browser mostra una finestra di dialogo che mi chiede se va bene.
Salvataggio delle modifiche in un nuovo file di disegno Excalidraw.
Salvataggio delle modifiche in un nuovo file Excalidraw. Il file originale rimane invariato.

Esempio di codice reale

Di seguito è riportato un esempio reale di browser-fs-access utilizzato in Excalidraw. Questo estratto è tratto da /src/data/json.ts. Di particolare interesse è il modo in cui il metodo saveAsJSON() passa un handle di file o null al metodo fileSave() di browser-fs-access, che lo fa sovrascrivere quando viene fornito un handle o salvare in un nuovo file in caso contrario.

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 alla UI

Nell'app o in Excalidraw, la UI deve adattarsi alla situazione di supporto del browser. Se l'API File System Access è supportata (if ('showOpenFilePicker' in window) {}), puoi mostrare un pulsante Salva con nome oltre a un pulsante Salva. Gli screenshot riportati di seguito mostrano la differenza tra la barra degli strumenti principale reattiva dell'app Excalidraw su iPhone e su Chrome da computer. Nota che su iPhone manca 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 Excalidraw 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 con nome&quot;.
Barra degli strumenti dell'app Excalidraw su Chrome con un pulsante Salva e un pulsante Salva con nome.

Conclusioni

Tecnicamente, l'utilizzo dei file di sistema funziona su tutti i browser moderni. Sui browser che supportano l'API File System Access, puoi migliorare l'esperienza consentendo il salvataggio e la sovrascrittura (non solo il download) dei file e permettendo agli utenti di creare nuovi file ovunque vogliano, il tutto mantenendo la funzionalità sui browser che non supportano l'API File System Access. browser-fs-access ti semplifica la vita gestendo le sottigliezze del miglioramento progressivo e rendendo il codice il più semplice possibile.

Ringraziamenti

Questo articolo è stato rivisto da Joe Medley e Kayce Basques. Grazie ai contributori di Excalidraw per il loro lavoro sul progetto e per la revisione delle mie richieste di pull. Immagine promozionale di Ilya Pavlov su Unsplash.