Créer des téléchargeurs de fichiers React flexibles et réutilisables

Daniel a récemment fait l'objet d'un article sur le blog d'ingénierie d'Eventbrite et nous sommes très fiers du travail qu'il y accomplit.

L'équipe de création d'événements chez Eventbrite avait besoin d'un téléchargeur d'images basé sur React qui offrirait de la flexibilité tout en présentant une interface utilisateur simple. Les composants de l'uploader d'images devraient fonctionner dans une variété de scénarios en tant que parties réutilisables qui pourraient être composées différemment selon les besoins. Lis la suite pour voir comment nous avons résolu ce problème.

Qu'est-ce qu'un téléchargeur de fichiers ?

Dans le passé, si tu voulais obtenir un fichier de tes utilisateurs web, tu devais utiliser un "Type d'entrée "fichier. Cette approche était limitée à bien des égards, notamment en ce qu'elle ne permettait pas d'atteindre les objectifs fixés. c'est une entrée: les données ne sont transmises que lorsque tu soumets le formulaire, les utilisateurs n'ont donc pas la possibilité de voir les commentaires avant ou pendant le téléchargement.

En gardant cela à l'esprit, les téléchargeurs React dont nous allons parler ne sont pas des entrées de formulaire ; ce sont des " outils de transport immédiat. " L'utilisateur choisit un fichier, l'uploader le transporte vers un serveur distant, puis reçoit une réponse avec un certain identifiant unique. Cet identifiant est alors immédiatement associé à un enregistrement de base de données ou placé dans un champ de formulaire caché.

Cette nouvelle stratégie offre une grande flexibilité par rapport aux processus de téléchargement traditionnels. Par exemple, le fait de dissocier le transport de fichiers des soumissions de formulaires nous permet de télécharger directement vers un espace de stockage tiers (comme Amazon S3) sans envoyer les fichiers via nos serveurs..

La contrepartie de cette flexibilité est la complexité ; les téléchargeurs de fichiers par glisser-déposer sont des bêtes complexes. Nous avions également besoin que notre téléchargeur React soit simple et utilisable. Trouver un chemin pour offrir à la fois flexibilité et facilité d'utilisation n'a pas été une tâche facile.

Identifier les responsabilités

Établir les responsabilités d'un téléchargeur semble facile... il télécharge, n'est-ce pas ? Bien sûr, mais il y a beaucoup d'autres choses à faire pour que cela se produise :

  • Il doit avoir une zone de dépôt qui change en fonction de l'interaction de l'utilisateur. Si l'utilisateur fait glisser un fichier dessus, elle doit indiquer ce changement d'état.
  • Et si nos utilisateurs ne peuvent pas faire glisser les fichiers ? Peut-être ont-ils des problèmes d'accessibilité ou peut-être essaient-ils de télécharger depuis leur téléphone. Quoi qu'il en soit, notre téléchargeur doit afficher un sélecteur de fichiers lorsque l'utilisateur clique ou tape.
  • Il doit valider le fichier choisi pour s'assurer qu'il est d'un type et d'une taille acceptables.
  • Une fois qu'un fichier est sélectionné, il devrait afficher un aperçu de ce fichier pendant qu'il est téléchargé.
  • Il doit donner des informations significatives à l'utilisateur pendant le téléchargement, comme une barre de progression ou un graphique de chargement qui indique que quelque chose est en train de se passer.
  • De plus, que se passe-t-il en cas d'échec ? Il doit afficher une erreur significative à l'utilisateur pour qu'il sache qu'il doit réessayer (ou abandonner).
  • Oh, et il doit en fait télécharger le fichier.

Ces responsabilités ne sont qu'une courte liste, mais tu vois l'idée, elles peuvent devenir compliquées très rapidement. De plus, en téléchargeant images est notre cas d'utilisation principal, il peut y avoir une variété de besoins en matière de téléchargement de fichiers. Si tu t'es donné beaucoup de mal pour comprendre le comportement glisser/déposer/mettre en évidence/valider/transporter/succès/échec, pourquoi tout réécrire lorsque tu as soudain besoin de télécharger des fichiers CSV pour ce seul rapport ?

Alors, comment pouvons-nous structurer notre téléchargeur d'images React pour obtenir un maximum de flexibilité et de réutilisation ?

Séparation des préoccupations

Dans le diagramme suivant, tu peux voir une vue d'ensemble de l'approche que nous voulons adopter. Ne t'inquiète pas si cela te semble compliqué - nous allons approfondir chacun de ces composants ci-dessous pour voir les détails de leur but et de leur rôle.

Notre bibliothèque de composants basée sur React ne devrait pas avoir à savoir comment fonctionnent nos API. Garder cette logique séparée présente l'avantage supplémentaire de la réutilisabilité ; différents produits ne sont pas enfermés dans une seule API ou même un seul style d'API. Au lieu de cela, ils peuvent réutiliser autant ou aussi peu que nécessaire.

Même au sein de éléments de présentationIl est donc possible de séparer la fonction de la présentation. Nous avons donc pris notre liste de responsabilités et créé une pile de composants, allant du plus général en bas au plus spécifique en haut.

Composantes fondamentales

UploaderDropzone

Ce composant est le cœur de l'interface utilisateur du téléchargeur, là où l'action commence. Il gère les événements de glisser/déposer ainsi que le clic pour naviguer. Il n'a pas d'état lui-même, il sait seulement comment normaliser et réagir (tu vois ce que j'ai fait là ?) à certaines actions de l'utilisateur. Il accepte les rappels en tant qu'accessoires afin de pouvoir dire à son implémenteur quand les choses se produisent.

Il attend que des fichiers soient glissés sur lui, puis invoque un rappel. De même, lorsqu'un fichier est choisi, soit par glisser/déposer, soit par cliquer pour naviguer, il invoque un autre rappel avec l'objet JS file.

Elle possède une de ces entrées de type "fichier" dont j'ai parlé plus haut, cachée à l'intérieur pour les utilisateurs qui ne peuvent pas (ou préfèrent ne pas) faire glisser des fichiers. Cette fonctionnalité est importante, et en l'abstrayant ici, les composants qui utilisent la zone de dépôt n'ont pas à réfléchir à la façon dont le fichier a été choisi.

Ce qui suit est un exemple d'UploaderDropzone utilisant React :

Fais glisser un fichier ici !

UploaderDropzone n'a que très peu d'avis sur son apparence, et n'a donc qu'un style minimal. Par exemple, Certains navigateurs traitent les événements de déplacement différemment lorsqu'ils se produisent sur des descendants profonds du nœud cible. Pour résoudre ce problème, la dropzone utilise une seule div transparente pour couvrir tous ses descendants. Cela permet d'offrir l'expérience nécessaire aux utilisateurs qui glissent/déposent, mais aussi aux utilisateurs de la zone de dépôt. maintient l'accessibilité pour les lecteurs d'écran et autres technologies d'assistance.

UploaderLayoutManager

Le composant UploaderLayoutManager gère la plupart des transitions d'état et sait quelle disposition doit être affichée pour chaque étape du processus, tout en acceptant d'autres composants React comme accessoires pour chaque étape.

Cela permet aux responsables de la mise en œuvre de penser à chaque étape comme à une idée visuelle distincte sans se préoccuper de savoir comment et quand chaque transition se produit. Les partisans de ce composant React doivent uniquement réfléchir à la disposition qui doit être visible à un moment donné en fonction de l'état, et non à la manière dont les fichiers sont alimentés ou à l'aspect de la disposition.

Voici une liste d'étapes qui peuvent être fournies au LayoutManager en tant qu'accessoires :

  • Étapes gérées par LayoutManager :
    • Non remplie - une zone de dépôt vide avec un appel à l'action (" Téléchargez une belle image ! ").
    • Fichier glissé sur la fenêtre mais pas sur la zone de dépôt ("Dépose ce fichier ici !").
    • Fichier glissé sur la zone de dépôt ("Dépose-le maintenant !")
    • Téléchargement de fichier en cours ("Attends, je l'envoie...")
  • Étape gérée par un composant qui implémente LayoutManager :
    • Le fichier a été téléchargé et est rempli. Pour notre téléchargeur d'images, il s'agit d'un aperçu de l'image avec un bouton "Supprimer".

Le LayoutManager lui-même n'a que peu ou pas de styles, et n'affiche que les éléments visuels qui lui ont été transmis en tant qu'accessoires. Il est chargé de gérer l'étape du processus atteinte par l'utilisateur et d'afficher le contenu de cette étape.

La seule étape de la mise en page qui est gérée de manière externe est "Aperçu" (si l'Uploader a une image remplie). En effet, le composant de mise en œuvre doit définir l'état dans lequel le téléchargeur démarre. Par exemple, si l'utilisateur a déjà téléchargé une image, nous voulons afficher cette image lorsqu'il revient sur la page.

Exemple d'utilisation de LayoutManager :

<uploaderlayoutmanager
    dropzoneElement={}
    windowDragDropzoneElement={}
    dragDropzoneElement={}
    loadingElement={}
    previewElement={}
 
    showPreview={!!fichier}
 
    onReceiveFile={handleReceiveFile}
/>

Composants spécifiques aux ressources

Chargeur d'images

Le composant ImageUploader est presque entièrement orienté vers la présentation ; il définit l'aspect et la convivialité de chaque étape et les transmet en tant qu'accessoires à un UploadLayoutManager. C'est également un endroit idéal pour effectuer la validation (type de fichier, taille du fichier, etc.).

Les partisans de cet outil peuvent se concentrer presque entièrement sur l'aspect visuel de l'uploader. Ce composant conserve très peu de logique puisque les transitions d'état sont gérées par le UploaderLayoutManager. Nous pouvons changer les visuels de façon fluide sans trop nous soucier d'endommager la fonction de l'uploader.

 

Exemple ImageUploader :

const DropzoneLayout = () =&gt; (
    <p>Fais glisser un fichier ici ou clique pour le parcourir</p>
) ;
const DragDropzoneLayout = () =&gt; (
    <p>Dépose le fichier maintenant !</p>
) ;
const LoadingLayout = () =&gt; (
    <p>Merci de patienter, le chargement est en cours...</p>
) ;
const PreviewLayout = ({file, onRemove}) =&gt; (
    <div>
        <p>Nom : {file.name}</p>
        <button onclick="{onRemove}">Supprimer le fichier</button>
    </div>
) ;
class ImageUploader extends React.Component {
    state = {file : undefined} ;
 
    _handleRemove = () =&gt; this.setState({file : undefined}) ;
 
    _handleReceiveFile = (file) =&gt; {
        this.setState({file}) ;
 
        return new Promise((resolve, reject) =&gt; {
            // télécharge le fichier !
        })
        .catch(() =&gt; this.setState({file : undefined}))
    }
 
    render() {
        let {file} = this.state ;
        let preview ;
 
        if (file) {
            preview = (
                <previewlayout file="{file}" onremove="{this._handleRemove}"></previewlayout>
            ) ;
        }
 
        return (
            <uploaderlayoutmanager
                dropzoneelement="{<DropzoneLayout"></uploaderlayoutmanager>}
                dragDropzoneElement={<dragdropzonelayout></dragdropzonelayout>}
                loadingElement={<loadinglayout></loadinglayout>}
                previewElement={preview}
                showPreview={!!fichier}
                onReceiveFile={this._handleReceiveFile}
            /&gt;
        ) ;
    }
} ;

Couche spécifique à l'application

L'exemple ci-dessus présente un aspect important qui n'a rien à voir avec la présentation : le transport de fichiers qui se produit dans _handleReceiveFile. Nous voulons que ce composant ImageUploader vive dans notre bibliothèque de composants et soit découplé du comportement spécifique de l'API, nous devons donc le supprimer. Heureusement, c'est aussi simple que d'accepter une fonction via props qui renvoie une promesse qui se résout lorsque le téléchargement est terminé.

_handleReceiveFile(file) {
    // pourrait faire la validation du fichier ici avant le transport. Si le fichier échoue à la validation, renvoie une promesse rejetée.
    let {uploadImage} = this.props ;
 
    this.setState({file}) ;
 
    return uploadImage(file)
        .catch(() => this.setState({file : undefined}))
}

Avec ce petit changement, ce même téléchargeur d'images peut être utilisé pour une variété d'applications. Une partie de ton application peut télécharger des images directement vers un tiers (comme Amazon S3), tandis qu'une autre peut les télécharger vers un serveur local dans un but et une manipulation totalement différents, mais en utilisant la même présentation visuelle.

Et maintenant, parce que toute cette complexité est compartimentée dans chaque composant, l'ImageUploader a une implémentation très propre :

<imageuploader uploadImage={S3ImageUploadApi}></imageuploader>

Grâce à cette base, les applications peuvent utiliser le même ImageUploader de différentes manières. Nous avons apporté la flexibilité que nous souhaitions tout en gardant l'API propre et simple. De nouveaux wrappers peuvent être construits sur UploadLayoutManager pour gérer d'autres types de fichiers ou de nouvelles mises en page.

En clôture

Imagine des téléchargeurs d'images qui ont été conçus pour chaque scénario, mais qui ne contiennent que quelques composants simples faits de balises de présentation. Ils peuvent chacun utiliser la même fonctionnalité de téléchargement si cela a du sens, mais avec une présentation totalement différente. Ou inverser cette idée, en utilisant les mêmes visuels de téléchargement mais avec des interfaces API totalement différentes.

De quelles autres façons pourrais-tu utiliser ces composants de base ? Quels autres téléchargeurs construirais-tu ? Il n'y a pas de limites si tu prends le temps de construire des composants réutilisables.

fr_FRFR
Retour en haut