Les navigateurs gèrent les fichiers et les répertoires depuis longtemps. L'API File fournit des fonctionnalités permettant de représenter des objets fichier dans des applications Web, ainsi que de les sélectionner de manière automatisée et d'accéder à leurs données. Mais au moment où vous regardez de plus près, tout ce qui brille n'est pas or.
La façon traditionnelle de traiter les 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 se présenter comme dans l'exemple de code ci-dessous.
L'objet input
vous donne un FileList
, qui, dans le cas ci-dessous, ne comprend qu'un seul File
.
Un File
est un type spécifique de Blob
et peut être utilisé dans tous les contextes possibles d'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 de répertoires
Pour ouvrir des dossiers (ou répertoires), vous pouvez définir l'attribut <input webkitdirectory>
.
À part cela, tout le reste fonctionne de la même manière que ci-dessus.
Malgré son nom avec le préfixe de fournisseur, webkitdirectory
n'est pas seulement utilisable dans les navigateurs Chromium et WebKit, mais aussi dans l'ancien Edge EdgeHTML et dans Firefox.
Enregistrer (au lieu de télécharger) des fichiers
En règle générale, pour enregistrer un fichier, vous êtes limité à son téléchargement, ce qui fonctionne grâce à l'attribut <a download>
.
Pour un objet Blob, vous pouvez définir l'attribut href
de l'ancre sur une URL blob:
que vous pouvez obtenir à l'aide 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'un des inconvénients majeurs de l'approche de téléchargement est qu'il n'existe aucun moyen d'effectuer un processus classique d'ouverture → modification → enregistrement, c'est-à-dire qu'il n'existe aucun moyen d'écraser le fichier d'origine. À la place, vous obtenez une nouvelle copie du fichier d'origine dans le dossier "Téléchargements" par défaut du système d'exploitation à chaque enregistrement.
API File System Access
L'API File System Access simplifie considérablement les opérations d'ouverture et d'enregistrement. Elle active également le vrai enregistrement, c'est-à-dire que vous pouvez non seulement choisir où enregistrer un fichier, mais également écraser un fichier existant.
Ouvrir des fichiers
Avec l'API File System Access, l'ouverture d'un fichier consiste à 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 de répertoires
Ouvrez un répertoire en appelant window.showDirectoryPicker()
qui rend les répertoires sélectionnables dans la boîte de dialogue de 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()
, puis vous écrivez les données Blob en appelant la méthode write()
du flux, puis 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 bonne que l'API File System Access, elle n'est pas encore disponible pour tous.
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 permet, et utiliser l'approche traditionnelle si ce n'est pas le cas, tout en ne punissant jamais l'utilisateur de 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 de changer à l'avenir, l'API browser-fs-access n'est pas modélisée sur celle-ci.
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 réduire au maximum la taille de votre application.
Les méthodes disponibles sont les suivantes : fileOpen()
, directoryOpen()
et fileSave()
.
En interne, la fonctionnalité de 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
L'utilisation de ces trois méthodes est intuitive.
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 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émonstration
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-frames multi-origines ne sont pas autorisés à afficher un sélecteur de fichier. La démo ne peut donc pas être intégrée dans cet article.
La bibliothèque browser-fs-access dans le monde réel
Pendant mon temps libre, je contribue brièvement à une PWA installable appelée Excalidraw, un outil pour tableau blanc qui vous permet de dessiner facilement des diagrammes à la main. Il est entièrement réactif et fonctionne bien sur de nombreux appareils, des petits téléphones mobiles aux ordinateurs dotés de grands écrans. Cela signifie qu'il doit traiter les fichiers sur toutes les différentes plates-formes, qu'elles soient compatibles ou non avec l'API File System Access. Il s'agit donc d'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 ne prend pas en charge l'API File System Access) dans le dossier des téléchargements de mon iPhone, ouvrir le fichier sur mon bureau (après l'avoir transféré depuis mon téléphone), le modifier et l'écraser avec mes modifications, ou même l'enregistrer en tant que nouveau fichier.
Exemple de code réel
Vous trouverez ci-dessous un exemple concret d'accès "browser-fs-access" tel qu'il est utilisé dans Excalidraw.
Cet extrait est issu de /src/data/json.ts
.
Il est particulièrement intéressant de voir comment la méthode saveAsJSON()
transmet un handle de fichier ou null
à la méthode fileSave()
de browser-fs-access, qui entraîne son écrasement lorsqu'un handle est fourni, ou son 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);
};
Remarques sur 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 compatible (if ('showOpenFilePicker' in window) {}
), vous pouvez afficher un bouton Save As (Enregistrer sous) en plus du bouton Save (Enregistrer).
Les captures d'écran ci-dessous montrent la différence entre la barre d'outils réactive de l'application principale d'Excalidraw sur iPhone et sur Chrome pour ordinateur.
Notez que sur iPhone, le bouton Save As (Enregistrer sous) est manquant.
Conclusions
L'utilisation des fichiers système fonctionne techniquement sur tous les navigateurs récents. Dans les navigateurs compatibles avec l'API File System Access, vous pouvez améliorer l'expérience en autorisant l'enregistrement et l'écrasement réel des fichiers, et en permettant aux utilisateurs de créer des fichiers où qu'ils le souhaitent, tout en restant fonctionnel sur les navigateurs non compatibles avec cette API. 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é 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 d'Ilya Pavlov sur Unsplash.