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 degli oggetti file nelle applicazioni web, oltre alla selezione programmatica e all'accesso ai loro dati. Nel momento in cui ti guardi più da vicino, però, non è tutto dorato tutto quel luccichio.

Il modo tradizionale di gestire i file

Apertura di file in corso...

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ò avere un aspetto simile all'esempio di codice riportato di seguito. L'oggetto input ti fornisce un FileList, che nel caso seguente è costituito da un solo elemento File. Un File è un tipo specifico di Blob e può essere utilizzato in qualsiasi contesto a disposizione di 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 delle directory in corso

Per aprire le cartelle (o le 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 nei browser Edge basati su EdgeHTML legacy e in Firefox.

Salvare (anziché scaricare) i file

Per salvare un file, in genere si è limitati a scaricare un file, il che funziona grazie all'attributo <a download>. Dato un BLOB, puoi impostare l'attributo href dell'ancoraggio 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 importante dell'approccio del download è che non è possibile eseguire un flusso classico di apertura → modifica → salvataggio, ossia non è possibile sovrascrivere il file originale. Ogni volta che esegui il "salvat", riceverai una nuova copia del file originale nella cartella Download predefinita del sistema operativo.

API File System Access

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

Apertura di file in corso...

Con l'API File System Access, l'apertura di un file richiede una chiamata al metodo window.showOpenFilePicker(). Questa chiamata restituisce un handle di file, da cui puoi ottenere il valore 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 delle directory in corso

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

Salvataggio dei file in corso...

Anche il salvataggio di file è semplice. Da un handle di file, crei un flusso accessibile in scrittura tramite createWritable(), poi scrivi i dati BLOB chiamando il metodo write() del flusso e infine chiudi il flusso 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 al browser-fs-access

Per quanto perfettamente l'API File System Access sia, 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 di supporto dei browser per l'API File System Access. (Fonte)

Questo è il motivo per cui vedo l'API File System Access come miglioramento progressivo. Pertanto, voglio utilizzarlo quando il browser lo supporta e utilizzare l'approccio tradizionale in caso contrario, il tutto senza punire mai l'utente con download inutili 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 continui a cambiare in futuro, l'API browser-fs-access non viene modellata sulla base di tale API. In altre parole, la libreria non è un polyfill, ma piuttosto un ponyfill. Puoi importare (staticamente o dinamicamente) esclusivamente le funzionalità di cui hai bisogno per ridurre il più possibile le dimensioni della tua app. I metodi disponibili sono i nomi appropriati di fileOpen(), directoryOpen() e fileSave(). Internamente, la funzionalità della libreria rileva se l'API File System Access è supportata e poi importa il percorso del codice corrispondente.

Utilizzo della libreria browser-fs-access

I tre metodi sono intuitivi. Puoi specificare l'elemento mimeTypes o il file extensions accettati dalla tua app e impostare un flag multiple per consentire o non consentire la selezione di più file o directory. Per dettagli completi, consulta la documentazione relativa all'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 sopra indicato in azione in una demo su Glitch. Anche il suo codice sorgente è disponibile. Poiché per motivi di sicurezza i frame secondari multiorigine non sono autorizzati a mostrare un selettore file, la demo non può essere incorporata in questo articolo.

La libreria browser-fs-access in natura

Nel mio tempo libero, do il mio contributo a una PWA installabile chiamata Excalidraw, uno strumento di lavagna con cui puoi disegnare facilmente diagrammi dall'aspetto disegnato a mano. È completamente reattivo e funziona bene su una gamma di dispositivi, dai telefoni cellulari di piccole dimensioni 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. Questo lo rende un ottimo candidato per la libreria browser-fs-access.

Ad esempio, posso iniziare un disegno sul mio iPhone, salvarlo (tecnicamente: scaricalo, dato che Safari non supporta l'API File System Access) nella cartella Download dell'iPhone, aprire il file sul desktop (dopo averlo trasferito dal telefono), modificarlo e sovrascriverlo con le mie modifiche o persino salvarlo come un nuovo file.

Un disegno Excalidraw su un iPhone.
Avvio di un disegno 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 Excalidraw modificato su Chrome sul computer.
Apertura e modifica del disegno Excalidraw sul desktop in cui è supportata l'API File System Access, di conseguenza al file è possibile accedere tramite l'API.
Sovrascrivendo il file originale con le modifiche.
Viene sovrascritto il 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.
È in corso il salvataggio delle modifiche in un nuovo file di disegno Excalidraw.
Salvataggio delle modifiche in un nuovo file Excalidraw in corso... Il file originale rimane invariato.

Esempio di codice reale

Di seguito puoi vedere un esempio effettivo 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() trasmette un handle di file o il metodo null al metodo browser-fs-access' fileSave(), che ne causa la sovrascrittura quando viene fornito un handle o, in caso contrario, il salvataggio in un nuovo file.

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

In Excalidraw o nella tua app, l'interfaccia utente dovrebbe adattarsi alla situazione di assistenza 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 di seguito mostrano la differenza tra la barra degli strumenti adattabile dell'app principale di Excalidraw su iPhone e su desktop Chrome. Tieni presente che sull'iPhone manca il pulsante Salva con nome.

Barra degli strumenti dell&#39;app Excalidraw su iPhone con solo un pulsante &quot;Salva&quot;.
Barra degli strumenti dell'app Excalidraw su iPhone con un solo pulsante Salva.
Barra degli strumenti dell&#39;app Excalidraw sul desktop di Chrome con 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 attivo.

Conclusioni

La gestione dei file di sistema funziona tecnicamente su tutti i browser moderni. Sui browser che supportano l'API File System Access, puoi migliorare l'esperienza consentendo un vero salvataggio e una sovrascrittura (non solo il download) dei file e consentendo agli utenti di creare nuovi file dove vogliono, il tutto continuando a funzionare sui browser che non supportano l'API File System Access. browser-fs-access ti rende la vita più semplice affrontando le sfumature del miglioramento progressivo e rendendo il tuo codice il più semplice possibile.

Ringraziamenti

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