Publié le 27 juillet 2020
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 par programmation. 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
Vous pouvez ouvrir et lire des fichiers avec l'élément <input type="file">.
Dans sa forme la plus simple, l'ouverture d'un fichier peut ressembler à l'exemple de code.
L'objet input vous donne un FileList qui, dans notre exemple, 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 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 et 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>.
Étant donné 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
Un 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échargement 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 les opérations d'ouverture et d'enregistrement. Il permet également de réaliser de véritables économies. Cela signifie que vous pouvez choisir l'emplacement où enregistrer le fichier et é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 descripteur 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 très bien, 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, 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 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 prise en charge, puis importe le chemin de code correspondant.
Utiliser la bibliothèque
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 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 en action dans une démonstration GitHub. Son code source est également disponible.
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 des diagrammes à main levée. Il est entièrement responsive et fonctionne bien sur une large gamme d'appareils, des petits téléphones mobiles aux ordinateurs dotés 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 cas d'utilisation idéal 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 récents. 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.