Les navigateurs peuvent gérer les fichiers et les répertoires depuis longtemps. L'API File fournit des fonctionnalités permettant de représenter des objets de fichier dans des applications Web, ainsi que de les sélectionner par programmation et d'accéder à leurs données. Mais en y regardant de plus près, il apparaît que tout ce qui brille n'est pas de l'or.
Méthode traditionnelle de gestion des fichiers
Ouvrir des fichiers
En tant que développeur, vous pouvez ouvrir et lire des fichiers via l'élément <input type="file">
.
Dans sa forme la plus simple, l'ouverture d'un fichier peut ressembler à l'exemple de code ci-dessous.
L'objet input
vous donne un FileList
, qui, dans le cas ci-dessous, ne se compose que d'un seul File
.
Un File
est un type spécifique de Blob
et peut être utilisé dans n'importe quel contexte où un Blob peut l'être.
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 de répertoires
Pour ouvrir des dossiers (ou des répertoires), vous pouvez définir l'attribut <input webkitdirectory>
.
En dehors de cela, tout fonctionne comme ci-dessus.
Malgré son nom avec un préfixe du fournisseur, webkitdirectory
n'est pas seulement utilisable dans les navigateurs Chromium et WebKit, mais également dans l'ancien Edge basé sur EdgeHTML, ainsi que dans Firefox.
Enregistrement (ou téléchargement) de fichiers
Pour enregistrer un fichier, vous êtes traditionnellement limité au téléchargement d'un fichier, qui fonctionne grâce à l'attribut <a download>
.
Étant donné un blob, vous pouvez définir l'attribut href
de l'ancre sur une URL blob:
que vous pouvez obtenir à partir de la méthode 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'approche de téléchargement présente un inconvénient majeur : il est impossible de suivre un flux classique d'ouverture → modification → enregistrement, c'est-à-dire qu'il est impossible d'écraser le fichier d'origine. Vous obtenez plutôt une nouvelle copie du fichier d'origine dans le dossier de 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 grandement les deux opérations, l'ouverture et l'enregistrement. Il permet également un enregistrement réel, c'est-à-dire que vous pouvez non seulement choisir l'emplacement d'enregistrement d'un fichier, mais aussi écraser un fichier existant.
Ouvrir des fichiers
Avec l'API File System Access, l'ouverture d'un fichier ne nécessite qu'un appel à la méthode window.showOpenFilePicker()
.
Cet appel renvoie un descripteur 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 de répertoires
Ouvrez un répertoire en appelant window.showDirectoryPicker()
, ce qui permet de le sélectionner dans la boîte de dialogue de fichier.
Enregistrer des fichiers
L'enregistrement de fichiers est tout aussi simple.
À partir d'un descripteur de fichier, vous créez un flux en écriture via createWritable()
, puis vous écrivez les données du blob en appelant la méthode write()
du flux, et 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
Bien que l'API File System Access soit parfaitement adaptée, elle n'est pas encore largement disponible.
C'est pourquoi je considère l'API File System Access comme une amélioration progressive. Je souhaite donc l'utiliser lorsque le navigateur est compatible avec celui-ci, et utiliser l'approche traditionnelle dans le cas contraire, tout en évitant de pénaliser l'utilisateur avec des téléchargements inutiles de code JavaScript non compatible. La bibliothèque browser-fs-access est ma réponse à ce défi.
Philosophie de conception
Étant donné que l'API File System Access est susceptible d'être modifiée à l'avenir, l'API browser-fs-access n'est pas modélisée sur elle.
Autrement dit, la bibliothèque n'est pas un polyfill, mais un ponyfill.
Vous pouvez importer (de manière statique ou dynamique) uniquement les fonctionnalités dont vous avez besoin pour réduire au maximum la taille de votre application.
Les méthodes disponibles sont fileOpen()
, directoryOpen()
et fileSave()
.
En interne, la bibliothèque détecte si l'API File System Access est compatible, puis importe le chemin de code correspondant.
Utiliser la bibliothèque browser-fs-access
Les trois méthodes sont intuitives.
Vous pouvez spécifier le mimeTypes
ou le extensions
de fichier accepté par votre application, et définir un indicateur multiple
pour autoriser ou non la sélection de plusieurs fichiers ou répertoires.
Pour en savoir plus, consultez la 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. Pour des raisons de sécurité, les sous-cadres inter-origines ne sont pas autorisés à afficher un sélecteur de fichiers. Par conséquent, la démonstration ne peut pas être intégrée à cet article.
La bibliothèque browser-fs-access dans la nature
Dans mon temps libre, je contribue un peu à une PWA installable appelée Excalidraw, un outil de tableau blanc qui vous permet de dessiner facilement des diagrammes avec un aspect dessiné à la main. Il est entièrement responsif et fonctionne bien sur une grande variété d'appareils, des petits téléphones mobiles aux ordinateurs à grand écran. Cela signifie qu'il doit gérer les fichiers sur toutes les plates-formes, qu'elles soient compatibles ou non avec l'API File System Access. Ce qui 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, ouvrir le fichier sur mon ordinateur (après l'avoir transféré depuis mon téléphone), le modifier et le remplacer par mes modifications, ou même l'enregistrer en tant que nouveau fichier.
Exemple de code concret
Vous trouverez ci-dessous un exemple concret de browser-fs-access tel qu'il est utilisé dans Excalidraw.
Cet extrait est tiré de /src/data/json.ts
.
La méthode saveAsJSON()
transmet un descripteur de fichier ou null
à la méthode fileSave()
de browser-fs-access, ce qui entraîne un écrasement lorsqu'un descripteur est fourni ou un enregistrement 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 concernant l'interface utilisateur
Que ce soit dans Excalidraw ou dans votre application, l'UI doit s'adapter à la situation d'assistance du navigateur.
Si l'API File System Access est prise en charge (if ('showOpenFilePicker' in window) {}
), vous pouvez afficher un bouton Enregistrer sous en plus d'un bouton Enregistrer.
Les captures d'écran ci-dessous montrent la différence entre la barre d'outils principale de l'application Excalidraw responsive sur iPhone et sur Chrome pour ordinateur.
Notez que sur iPhone, le bouton Enregistrer sous est manquant.
Conclusions
Techniquement, travailler avec des fichiers système fonctionne sur tous les navigateurs modernes. Sur les navigateurs compatibles avec l'API File System Access, vous pouvez améliorer l'expérience en autorisant l'enregistrement et le remplacement (et non seulement le téléchargement) de fichiers, et en permettant à vos utilisateurs de créer des fichiers où ils le souhaitent, tout en restant fonctionnel sur les navigateurs qui ne sont pas compatibles avec l'API File System Access. browser-fs-access vous facilite la vie en traitant les subtilités de l'amélioration progressive et en rendant votre code aussi simple que possible.
Remerciements
Cet article a été relu par Joe Medley et Kayce Basques. Merci aux contributeurs à Excalidraw pour leur travail sur le projet et pour avoir examiné mes requêtes pull. Image principale par Ilya Pavlov sur Unsplash.