Blog

HTML5 File API, React et créativité

L’API File de HTML5 est là depuis un petit moment maintenant et est désormais plutôt bien supportée (en lecture tout du moins). Elle a le mérite de pouvoir nous aider en tant que développeurs à proposer des solutions plus simples en tirant partie du navigateur client. Sur des parties administratives il est possible en quelques lignes de code de faciliter le traitement de fichiers, voire leur conversion.

Le but de cet article est de montrer à quel point il est simple d’utiliser cette API afin que vous l’envisagiez pour des tâches simples ou phases de prototypage.

TL;DR

Le chargement et la création de fichiers dans un navigateur tiennent en moins de 20 lignes. Avec un peu de créativité, je suis sûr que nous pouvons trouver beaucoup d’utilisations concrètes au quotidien comme le montre l’exemple final s’apparentant à de l’Event Sourcing.

Les bibliothèques permettant l’utilisation de modules NodeJS côté client, ou les frameworks frontend ne font qu’agrandir le champ des possibilités d’un point de vue technique.

Un éditeur markdown simpliste

Pour voir quelques exemples basiques d’utilisation de l’API dans le cadre d’une application concrète nous allons créer un éditeur Markdown simpliste contenant :

  • un champ d’édition de texte (source Markdown)
  • un espace de prévisualisation du rendu HTML en temps réel
  • la possibilité de charger / sauvegarder des fichiers Markdown
  • une barre de debug avec des exports / imports de différentes choses liée à l’application

Comme nous sommes au top des technos, nous utiliserons la librairie React couplée à Dispatchr afin d’avoir un flux de données unidirectionnel. On pense à tout : en cette période de fêtes, vous pourrez fièrement glisser à tonton Michel entre la dinde et le fromage que vous travaillez sur des technos de Facebook et Yahoo ;)

Le code illustrant cet article est sur Github, ainsi qu’une démonstration du rendu final. Si seule la partie manipulation de la FileAPI vous intéresse, focalisez vous sur la section « Ajout de chargement / sauvegarde de fichiers ».

ATTENTION : c’est ici ma première utilisation de React, le but n’est donc pas d’illustrer les bonnes pratiques de cette librairie … veuillez excuser mes erreurs de débutant.

Application initiale

Voici donc l’architecture de notre application initiale : un éditeur et la prévisualisation du rendu HTML, avec un flux de données unidirectionnel. Ci-dessous le schéma illustrant ce dernier :

Illustration de l'architecture initiale

L’éditeur

Le composant d’édition est un composant React contenant un champ de texte.

Le code restant de ce composant est ce qui permet d’activer le flux présenté dans le schéma précédent … préparez vous pour le départ :

  • lors de la modification du contenu une action changeContent est déclenchée avec le nouveau contenu
  • celle-ci aura pour effet de créer l’évènement métier CONTENT_CHANGED
  • lui-même écouté par le ContentStore pour se mettre à jour
  • notre éditeur "écoute" les mises à jour du ContentStore par le biais d’un Mixin basique que nous allons réutiliser juste après. Sans entrer dans le détail, dès que le ContentStore a changé, l’état du composant est mis à jour avec les valeurs renvoyées par _stateFromStore => la valeur du champ de texte se met à jour !

Nous aurions pu ignorer cette dernière étape si tôt. Mais imaginez que le contenu soit modifié d’une autre manière, par exemple en chargeant des données depuis un fichier (exemple totalement fortuit ;)), notre éditeur aurait alors été désynchronisé !

La prévisualisation

Le composant de prévisualisation du rendu est très proche de l’éditeur. La structure ayant déjà été mise en place précédemment, il suffit d’écouter les mises à jour du ContentStore et de mettre à jour le HTML d’après la source Markdown. Pour cela nous utilisons la librairie markdown-js.

Ça y est, nous avons maintenant une base suffisante pour pouvoir jouer avec nos fichiers. Le plus laborieux est fait !

Ajout de chargement / sauvegarde de fichiers

Un éditeur c’est bien, mais pouvoir sauvegarder son contenu pour le partager ou le terminer plus tard c’est mieux ! Nous allons donc ajouter un nouveau composant à notre interface.

Celui-ci aura deux rôles distincts dans notre application :

  • permettre à l’utilisateur de sélectionner un fichier Markdown sur sa machine afin de charger son contenu dans notre application
  • sauvegarder la source Markdown dans un fichier sur sa machine

Pour nous aider dans l’implémentation technique, nous allons créer deux sous-composants distincts qui ne contiendront aucune logique "métier".

Charger du contenu

Le composant permettant de charger le contenu d’un fichier a un contrat très simple : dès que l’utilisateur sélectionne un fichier, il transmet le nom du fichier et son contenu à une méthode de callback.

Nous voici dans le vif du sujet ! Si vous ne devez retenir qu’une chose, c’est la taille du code nécessaire pour récupérer le contenu du fichier.

Il nous faut un champ de type file. Celui-ci expose une API vous permettant de récupérer une liste des fichiers sélectionnées par l’utilisateur, par sa propriété Element.files, de type FileList. Cela permet d’accéder par un index de tableau ou par la méthode item(index) à un objet File.

Une fois que l’objet File récupéré il faut accéder à son contenu. Pour cela, nous passons par un FileReader qui permettra de lire le contenu de différentes manières selon le type de données (texte ou binaire). De plus vous pouvez écouter les différentes phases du chargement de fichier afin, par exemple, d’afficher une barre de progression à l’utilisateur ou de bloquer certaines interactions durant le chargement d’un gros fichier.

Dans notre cas nous utilisons simplement FileReader.readAsText(file) en écoutant l’évènement de fin de chargement pour pouvoir transmettre le contenu du fichier à la fonction de traitement "métier" (passée à notre composant FileUploader par la propriété onUpload).

Sachez enfin que cette interface est compatible avec tous les navigateurs récents (IE10+), donc pas de limite de ce côté là. Je vous invite à prendre 1 minute afin de regarder la documentation de FileReader car cela peut vous donner des idées d’utilisation des fichiers reçus. Quelques exemple :

  • afficher directement l’image dans la page en utilisant FileReader.readAsDataURL(file) pour renseigner l’attribut src d’une image
  • transmettre le contenu du fichier à votre backend en utilisant FileReader.readAsArrayBuffer(file) pour transmettre le contenu d’un gros fichier via un WebSocket, ou en AJAX

Sauvegarder la source

Passons maintenant au second composant dont nous aurons besoin techniquement pour permettre à l’utilisateur de télécharger un fichier contenant la source Markdown.

Le contrat de ce composant est lui aussi simple : dès que l’utilisateur clique sur un lien, il demande les informations "métier" du fichier (contenu et nom) par un callback getFileData puis crée et fait télécharger le fichier au client.

Là encore, la création d’un fichier en terme de code est ultra simple car il suffit de créer un objet de type Blob. Le constructeur accepte un tableau de contenu pouvant être de pas mal de types différents (ArrayBuffer, ArrayBufferView, Blob ou DOMString), il est donc en pratique très simple de "sérialiser" un ensemble de données sous forme de Blob (y compris des images et autres données binaires).

Une fois le Blob créé vous avez la possibilité de lui assigner une URL temporaire pour le navigateur client. Cette URL vous permet ensuite d’y rediriger l’utilisateur, ou comme mentionné précédemment de l’assigner comme src à une image voir une iframe. La création d’une URL se fait par l’appel à URL.createObjectURL(blob) disponible dans le navigateur. Voici un exemple d’URL générée : blob:http%3A//real34.github.io/dc1f911f-f6ae-46f9-89bb-090a57fddac7

Ce qui est chouette c’est qu’en terme de compatibilité navigateur nous sommes jusqu’ici toujours compatible avec plus de 90% des navigateurs (IE10+), et pour le reste il y a Blob.js !

Nous arrivons maintenant aux limites de compatibilités actuelles. En effet, la spécification FileWriterAPI du W3C ayant été abandonnée il n’y a plus beaucoup de moyens standards de demander la sauvegarde d’un fichier au navigateur. Voici quelques pistes possibles pour parvenir à nos fins :

  • rediriger l’utilisateur vers l’URL générée pour le Blob, en lui ayant donné un type application/octet-stream afin de forcer dans la plupart des navigateurs un téléchargement : dans ce cas le nom du fichier est alors incontrôlable, ce qui est dommage …
  • utiliser un polyfill tel que FileSaver.js ou d’autres à base de Flash que je vous laisse retrouver par vous-même !
  • utiliser l’attribut a[download] pour demander au navigateur le téléchargement de la cible d’un lien.

C’est cette dernière solution que nous avons implémenté dans notre composant, afin de pouvoir contrôler le nom du fichier téléchargé : cette technique n’est compatible qu’avec Chrome et Firefox, soit près de 70% du parc Français aujourd’hui … à garder en tête avant de la réutiliser.

Intégration dans l’application

Nous venons de créer deux composants techniques nous permettant désormais de travailler sur l’interaction avec des fichiers simplement. Il est donc temps d’ajouter la fonctionnalité souhaitée à notre éditeur : sauvegarder la source actuelle dans un fichier et charger le contenu d’un fichier dans notre application.

Le code nécessaire est minimaliste (deux méthodes de callbacks "métier" : handleContentUpload et downloadedFileData) car il ne suffit plus qu’à connecter chaque partie de l’application les unes aux autres :

Pour ce faire, nous avons tiré parti de l’architecture en place et il nous a suffit :

  • d’écouter le ContentStore afin de modifier un état interne lors de la modification du contenu : cet état sera utilisé pour générer le contenu du fichier à exporter
  • d’ajouter une nouvelle action loadFile() et l’évènement métier lié FILE_LOADED qui sera écouté par le ContentStore pour se mettre à jour avec le nouveau contenu. Cette action sera appelée lors de la sélection d’un fichier par l’utilisateur.

… cela sera désormais la même chose à chaque fois, il ne reste plus qu’à être créatif !

Déboguage par fichiers

Nous avons tout en place pour nous amuser un petit peu et explorer quelques pistes d’utilisation de la FileAPI pour ajouter des fonctionnalités simplement à une application existante. Pour se faire nous allons nous focaliser sur des utilisations à but de déboguage.

Ajoutons alors une barre de déboguage super pratique à notre application ! J’entrerai moins dans les détails, mais le code ci-après devrait vous permettre de visualiser la simplicité de mise en oeuvre.

Représenter l’état d’une application

Lorsqu’un problème survient, une des premières choses qu’il faut comprendre est "quel est l’état de l’application qui pose problème" (pour la corriger vers l’état qui aurait été attendu). Je vous propose donc d’ajouter un premier composant à notre barre de déboguage, qui nous permettra de :

  • sauvegarder l’état actuel de l’application cliente (interface utilisateur) dans un fichier
  • recharger l’état d’une application depuis un fichier, afin de revenir exactement dans l’état où elle était lors de la sauvegarde

Grâce aux solutions choisies pour illustrer cet article c’est très simple ! Jugez par vous même :

En effet, nous utilisons les fonctionnalités de Dispatchr permettant le développement d’applications JS isomorphiques afin d’avoir un état sérialisé de l’interface. Pour cela nous avons dû implémenter les méthodes dehydrate / rehydrate au sein du ContentStore.

Tout le reste est "automatique" grâce aux propriétés de React et à l’architecture unidirectionnelle en place.

Représenter l’évolution d’une application

Lorsqu’on y réfléchit un peu, l’état d’une application n’est pas ce qui contient les informations nécessaires à la compréhension d’un problème. Ce qui permet de bien comprendre est plutôt l’enchaînement d’évènements qui a donné lieu au problème !

Au lieu de simplement pouvoir transmettre l’état d’une application à un instant t, nous allons permettre aux utilisateurs de nous transmettre l’intégralité de ce qu’ils ont fait sur la page. Et le pire c’est que c’est facile à faire !

En effet, si nous regardons l’architecture en place de plus près, nous nous rendons compte qu’il nous suffit de transmettre deux choses afin de tout pouvoir reconstruire et analyser :

  • l’état initial de l’application au chargement de celle-ci
  • la suite d’évènements qui ont été déclenchés

Cliquez sur l’image pour voir une animation du rendu :

React exemple evenements

C’est article est déjà bien assez long, aussi je vous invite à analyser le code de démonstration disponible pour juger de la facilité de mise en oeuvre. Nous avons ajouté :

  • un EventStore qui écoute tous les évènements et les garde en mémoire
  • un composant qui :
    • sauvegarde l’état initial de l’application lorsqu’il est initialisé (React utilise pour cela un callback componentDidMount)
    • récupère et sérialise l’historique des évènements lors du téléchargement d’un fichier
    • réinitialise l’état de l’application, puis rejoue les évènements successifs un après l’autre au chargement d’un fichier (avec un petit délai pour que cela soit visible à l’oeil dans le cadre de la démo !)

Note : nous venons de toucher du doigt une autre approche de persistance des données qui se nomme l’Event Sourcing. Je vous invite à vous renseigner sur ce sujet passionnant ;)

Les limites techniques

Tout ceci ne reste bien sûr valable que pour des besoins simples, et la solution de manipulation de fichiers a des limites qu’il faut garder en tête :

  • taille des fichiers : les fichiers transitant par le navigateur client, le traitement de gros fichiers peut rapidement amener des problèmes liés à une consommation excessive de la mémoire du poste client
  • compatibilité navigateurs : la plupart de ce que nous avons vu ici est compatible IE10+, ce qui est suffisant pour une utilisation dans un outil interne / espace d’administration ou prototype. En revanche si vous souhaitez allez plus loin, comme nous l’avons fait pour le téléchargement de fichier, je vous invite à regarder du côté des divers polyfills disponibles
  • l’utilisateur : manipuler des fichiers peut permettre de repousser à plus tard l’implémentation de fonctionnalités telles que le partage, la synchronisation multi-postes … en revanche cela n’est pas forcément la solution la plus pratique pour un utilisateur sur le long terme !

Il n’y a pas de limites à votre imagination

Quelques idées de choses possibles avec l’API, en ne restant qu’avec des fichiers textes (de nombreux exemples existent déjà avec des images par exemple) :

  • effectuer du traitement d’images côté client
  • pré-remplir un formulaire complexe depuis un fichier existant
  • effectuer un traitement en lot, en appelant plusieurs fois une action unitaire pour chaque ligne du fichier
  • génération d’un export CSV des données affichées sans appel serveur

… avec des outils comme Browserify rendant disponibles côté client de nombreux modules NodeJS, cela ne fait qu’agrandir le champ des possibles.

Enfin, gardez en tête que d’autres solutions telles que IndexedDb, remotestorage.io ou firebase vous permettront chacune à leur manière de lâcher l’utilisation de fichiers le moment venu, pour des choses plus robustes et fonctionnalités plus intéressantes !