Lire et écrire des fichiers et des répertoires avec la bibliothèquebrowser-fs-access

Les navigateurs sont capables de traiter des fichiers et des répertoires depuis longtemps. API File fournit des fonctionnalités permettant de représenter des objets fichier dans des applications Web, les sélectionner de manière programmatique et accéder à leurs données. Pourtant, à partir du moment où l'on regarde de plus près, tout ce qui brille n'est plus que de l'or.

La méthode traditionnelle de gestion des fichiers

Ouverture des fichiers

En tant que développeur, vous pouvez ouvrir et lire des fichiers via le <input type="file"> . Dans sa forme la plus simple, l'ouverture d'un fichier ressemble à l'exemple de code ci-dessous. L'objet input vous donne un objet FileList, qui, dans le cas ci-dessous, ne comprend qu'un seul File Un File est un type spécifique de Blob, et peuvent être utilisées dans n'importe quel contexte possible par un objet 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();
  });
};

Ouverture des répertoires

Pour ouvrir des dossiers (ou répertoires), vous pouvez définir le paramètre <input webkitdirectory> . À part cela, tout le reste fonctionne de la même manière que ci-dessus. Malgré son nom préfixé par le fournisseur, webkitdirectory peut être utilisé non seulement dans les navigateurs Chromium et WebKit, mais aussi dans l'ancienne version Edge HTML, ainsi que dans Firefox.

Enregistrer (au lieu de télécharger) des fichiers

Pour enregistrer un fichier, vous êtes généralement limité au téléchargement d'un fichier, qui fonctionne grâce aux <a download> . Avec un objet Blob, vous pouvez définir l'attribut href de l'ancre sur une URL blob: que vous pouvez obtenir à partir de 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();
};

Problème

L'inconvénient majeur de l'approche de téléchargement est qu'il n'existe aucun moyen de créer une version classique Ouvrir → modifier → enregistrer le flux, c'est-à-dire qu'il n'y a aucun moyen d'écraser le fichier d'origine. Vous obtenez alors une nouvelle copie du fichier d'origine. dans le dossier "Téléchargements" par défaut du système d'exploitation chaque fois que vous "enregistrez".

API File System Access

L'API File System Access simplifie considérablement les opérations, l'ouverture et l'enregistrement. Elle active également l'enregistrement réel, c'est-à-dire que vous pouvez non seulement choisir l'emplacement où enregistrer un fichier, mais aussi écraser un fichier existant.

Ouverture des fichiers

Avec l'API File System Access, Pour ouvrir un fichier, il suffit d'appeler la méthode window.showOpenFilePicker(). Cet appel renvoie un handle de fichier, à partir duquel vous pouvez obtenir le File réel via la méthode 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);
  }
};

Ouverture des répertoires

Ouvrez un répertoire en appelant window.showDirectoryPicker(), qui rend les répertoires sélectionnables dans la boîte de dialogue "File" (Fichier)

Enregistrement de fichiers

L'enregistrement de fichiers est tout aussi simple. À partir d'un handle de fichier, vous créez un flux accessible en écriture via createWritable(), Ensuite, vous écrivez les données Blob en appelant la méthode write() du flux, Enfin, vous fermez le flux en appelant sa méthode 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);
  }
};

Présentation de browser-fs-access

Aussi parfait que soit l’API File System Access, elle n'est pas encore disponible pour tous les utilisateurs.

<ph type="x-smartling-placeholder">
</ph> Tableau des navigateurs compatibles avec l&#39;API File System Access. Tous les navigateurs sont marqués comme non compatibles. ou &quot;derrière un drapeau&quot;. <ph type="x-smartling-placeholder">
</ph> Tableau des navigateurs compatibles avec l'API File System Access. (source).

C'est pourquoi je considère l'API File System Access comme une amélioration progressive. En tant que tel, je veux l’utiliser quand le navigateur le prend en charge, et utilisez l'approche traditionnelle si ce n'est pas le cas. tout en ne punissant jamais l'utilisateur avec des téléchargements inutiles de code JavaScript non pris en charge. browser-fs-access bibliothèque est ma réponse à ce défi.

Philosophie de conception

Étant donné que l'API File System Access est encore susceptible de changer à l'avenir, l'API browser-fs-access n'est pas modélisée d'après elle. Autrement dit, la bibliothèque n'est pas un polyfill, mais plutôt comme poney. Vous pouvez importer (de manière statique ou dynamique) exclusivement les fonctionnalités dont vous avez besoin pour réduire au maximum la taille de votre application. Les méthodes disponibles sont les API fileOpen(), directoryOpen() fileSave() En interne, la fonctionnalité de bibliothèque détecte si l'API File System Access est prise en charge puis importe le chemin de code correspondant.

Utiliser la bibliothèque browser-fs-access

Ces trois méthodes sont intuitives à utiliser. Vous pouvez spécifier le mimeTypes ou le fichier extensions accepté par votre application, et définir un indicateur multiple. pour autoriser ou interdire la sélection de plusieurs fichiers ou répertoires. Pour plus d'informations, consultez les documentation de l'API browser-fs-access. L'exemple de code ci-dessous montre comment ouvrir et enregistrer des fichiers image.

// 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',
  });
})();

Démo

Vous pouvez voir le code ci-dessus en action dans une démonstration sur Glitch. Son code source y est également disponible. Étant donné que, pour des raisons de sécurité, les sous-frames multi-origines ne sont pas autorisés à afficher un sélecteur de fichier, la démo ne peut pas être intégrée dans cet article.

La bibliothèque browser-fs-access à l'œuvre

Pendant mon temps libre, je contribue légèrement à une pWA installable appelée Excalidraw, Un outil de tableau blanc qui vous permet d'esquisser facilement des diagrammes avec une sensation de dessin à la main. Entièrement réactif, il fonctionne bien sur une gamme d'appareils, des petits téléphones mobiles aux ordinateurs dotés d'un grand écran. Cela signifie qu'il doit traiter des fichiers sur les différentes plates-formes sont compatibles ou non avec l'API File System Access. Cela en fait un candidat idéal pour la bibliothèque browser-fs-access.

Je peux, par exemple, commencer un dessin sur mon iPhone, l'enregistrer (techniquement: le télécharger, car Safari n'est pas compatible avec l'API File System Access) ; dans le dossier "Téléchargements" de mon iPhone, ouvre le fichier sur mon bureau (après l'avoir transféré depuis mon téléphone), modifier le fichier et l’écraser avec mes modifications, ou même l’enregistrer en tant que nouveau fichier.

<ph type="x-smartling-placeholder">
</ph> Dessin d&#39;Excalidraw sur un iPhone.
Démarrage d'un dessin Excalidraw sur un iPhone non compatible avec l'API File System Access, mais où un fichier peut être enregistré (téléchargé) dans le dossier "Téléchargements".
<ph type="x-smartling-placeholder">
</ph> Dessin Excalidraw modifié dans Chrome sur le bureau.
Ouverture et modification du dessin Excalidraw sur un ordinateur de bureau compatible avec l'API File System Access (qui permet d'accéder au fichier via l'API)
<ph type="x-smartling-placeholder">
</ph> Écraser le fichier d&#39;origine avec les modifications
Remplacement du fichier d'origine par les modifications apportées au fichier de dessin Excalidraw d'origine. Le navigateur affiche une boîte de dialogue me demandant si c'est correct.
<ph type="x-smartling-placeholder">
</ph> Enregistrement des modifications dans un nouveau fichier de dessin Excalidraw.
Enregistrez les modifications dans un nouveau fichier Excalidraw. Le fichier d'origine n'a pas été modifié.

Exemple de code réel

Vous trouverez ci-dessous un exemple d'utilisation de browser-fs-access dans Excalidraw. Cet extrait est tiré de /src/data/json.ts Il est particulièrement intéressant de savoir comment la méthode saveAsJSON() transmet un handle de fichier ou null à browser-fs-access. fileSave(), qui l'écrase lorsqu'un handle est attribué, ou enregistrer dans un nouveau fichier si ce n'est pas le cas.

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

Considérations relatives à l'interface utilisateur

Que ce soit dans Excalidraw ou dans votre application, l'interface utilisateur doit s'adapter à la situation de prise en charge du navigateur. Si l'API File System Access est compatible (if ('showOpenFilePicker' in window) {}) vous pouvez afficher un bouton Enregistrer sous en plus du bouton Enregistrer. Les captures d'écran ci-dessous montrent la différence entre la barre d'outils responsive principale d'Excalidraw sur iPhone et sur Chrome pour ordinateur. Notez que le bouton Save As (Enregistrer sous) n'est pas disponible sur iPhone.

<ph type="x-smartling-placeholder">
</ph> Barre d&#39;outils de l&#39;application Excalidraw sur iPhone avec un simple bouton &quot;Enregistrer&quot; .
Barre d'outils de l'application Excalidraw sur iPhone avec un simple bouton Enregistrer.
<ph type="x-smartling-placeholder">
</ph> Barre d&#39;outils de l&#39;application Excalidraw sur le bureau Chrome avec un bouton &quot;Enregistrer&quot; et l&#39;option &quot;Enregistrer sous&quot; .
Barre d'outils de l'application Excalidraw dans Chrome avec un bouton Enregistrer et un bouton Enregistrer sous sélectionné.

Conclusions

Techniquement, l'utilisation des fichiers système fonctionne avec tous les navigateurs modernes. Dans les navigateurs compatibles avec l'API File System Access, vous pouvez améliorer votre expérience en autorisant pour véritable sauvegarde et écrasement (pas seulement le téléchargement) de fichiers et en permettant à vos utilisateurs de créer de nouveaux fichiers où ils le souhaitent, tout en restant fonctionnel sur les navigateurs qui ne prennent pas en charge l'API File System Access. browser-fs-access vous simplifie la vie. en traitant les subtilités de l'amélioration progressive et en simplifiant au maximum votre code.

Remerciements

Cet article a été lu par Joe Medley et Kayce Basques. Merci aux contributeurs d'Excalidraw pour leur travail sur le projet et pour avoir examiné mes demandes d’extraction. Image héros de Ilya Pavlov sur Unsplash.