Les navigateurs sont capables de 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 et d'accéder à leurs données de manière programmatique. Mais en y regardant de plus près, vous vous rendrez compte que tout ce qui brille n'est pas d'or.
La méthode traditionnelle de gestion des fichiers
Ouvrir des fichiers
En tant que développeur, vous pouvez ouvrir et lire des fichiers à l'aide de 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 contient qu'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();
});
};
Ouvrir des répertoires
Pour ouvrir des dossiers (ou des répertoires), vous pouvez définir l'attribut <input webkitdirectory>
.
À part cela, tout le reste fonctionne comme indiqué ci-dessus.
Malgré son nom préfixé par le fournisseur, webkitdirectory
n'est pas seulement utilisable dans les navigateurs Chromium et WebKit, mais aussi dans l'ancienne version d'Edge basée sur EdgeHTML, ainsi que dans Firefox.
Enregistrer (ou plutôt télécharger) des fichiers
Pour enregistrer un fichier, vous êtes généralement limité au téléchargement d'un fichier, ce qui fonctionne grâce à l'attribut <a download>
.
À partir d'un blob, vous pouvez définir l'attribut href
de l'ancrage 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'inconvénient majeur de l'approche Télécharger est qu'il n'existe aucun moyen de suivre le flux classique Ouvrir → Modifier → Enregistrer, c'est-à-dire qu'il n'existe aucun moyen d'écraser le fichier d'origine. Au lieu de cela, vous obtenez 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 l'enregistrez.
API File System Access
L'API File System Access simplifie considérablement ces deux opérations. Il permet également d'enregistrer réellement un fichier, c'est-à-dire de choisir où l'enregistrer, mais aussi d'écraser un fichier existant.
Ouvrir des fichiers
Avec l'API File System Access, l'ouverture d'un fichier se fait en un seul 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);
}
};
Ouvrir des répertoires
Ouvrez un répertoire en appelant window.showDirectoryPicker()
, ce qui permet de sélectionner des répertoires dans la boîte de dialogue de fichier.
Enregistrer des fichiers
L'enregistrement des fichiers est tout aussi simple.
À partir d'un handle de fichier, vous créez un flux accessible en écriture via createWritable()
, puis vous écrivez les données 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
L'API File System Access est tout à fait correcte, mais elle n'est pas encore largement disponible.

C'est pourquoi je considère l'API File System Access comme une amélioration progressive. Par conséquent, je souhaite l'utiliser lorsque le navigateur le prend en charge et utiliser l'approche traditionnelle dans le cas contraire. Je ne veux en aucun cas pénaliser l'utilisateur en lui faisant télécharger inutilement du 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 d'après celle-ci.
Autrement dit, la bibliothèque n'est pas un polyfill, mais plutôt un ponyfill.
Vous pouvez importer (de manière statique ou dynamique) exclusivement les fonctionnalités dont vous avez besoin pour que votre application soit aussi petite que possible.
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 fichier extensions
acceptés par votre application, et définir un indicateur multiple
pour autoriser ou interdire 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 GitHub. Son code source est également disponible. Pour des raisons de sécurité, les sous-frames d'origine croisée ne sont pas autorisés à afficher un sélecteur de fichiers. La démo ne peut donc pas être intégrée à cet article.
La bibliothèque browser-fs-access en action
Pendant 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 à main levée. Il est entièrement responsif et fonctionne bien sur une large gamme d'appareils, des petits téléphones mobiles aux ordinateurs avec de grands écrans. 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. Il s'agit donc d'un excellent candidat pour la bibliothèque browser-fs-access.
Par exemple, je peux 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 l'écraser avec mes modifications, ou même l'enregistrer sous un nouveau nom.




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
.
Il est particulièrement intéressant de noter comment la méthode saveAsJSON()
transmet un descripteur de fichier ou null
à la méthode fileSave()
de browser-fs-access, ce qui entraîne l'écrasement lorsqu'un descripteur est fourni, ou l'enregistrement dans un nouveau fichier dans le cas contraire.
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'UI
Que ce soit dans Excalidraw ou dans votre application, l'UI doit s'adapter à la situation de compatibilité 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 principale responsive de l'application Excalidraw sur iPhone et sur Chrome pour ordinateur.
Notez que le bouton Enregistrer sous est manquant sur l'iPhone.


Conclusions
Techniquement, l'utilisation 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 permettant l'enregistrement et l'écrasement (et pas seulement le téléchargement) des 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. La bibliothèque browser-fs-access vous facilite la vie en gérant les subtilités de l'amélioration progressive et en simplifiant au maximum votre code.
Remerciements
Cet article a été examiné 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 principale par Ilya Pavlov sur Unsplash.