Creare caricatori di file React flessibili e riutilizzabili

Daniel è stato recentemente presentato nel blog di Eventbrite Engineering e siamo molto orgogliosi del lavoro che svolge.

Il team di creazione di eventi di Eventbrite aveva bisogno di un caricatore di immagini basato su React che offrisse flessibilità pur presentando un'interfaccia utente semplice. I componenti del caricatore di immagini dovevano funzionare in una varietà di scenari come parti riutilizzabili che potevano essere composte in modo diverso a seconda delle esigenze. Continua a leggere per scoprire come abbiamo risolto questo problema.

Cos'è un caricatore di file?

In passato, se volevi ottenere un file dai tuoi utenti web, dovevi usare un file "Tipo di input "file. Questo approccio era limitato in molti modi, soprattutto per il fatto che è un inputI dati vengono trasmessi solo al momento dell'invio del modulo, quindi gli utenti non hanno la possibilità di vedere il feedback prima o durante il caricamento.

Per questo motivo, gli uploader di React di cui parleremo non sono dei form input, ma degli "strumenti di trasporto immediato". L'utente sceglie un file, l'uploader lo trasporta su un server remoto e riceve una risposta con un identificatore unico. Questo identificativo viene immediatamente associato a un record del database o inserito in un campo nascosto del modulo.

Questa nuova strategia offre un'enorme flessibilità rispetto ai processi di caricamento tradizionali. Ad esempio, il disaccoppiamento del trasporto dei file dall'invio dei moduli ci permette di caricare direttamente su archivi di terze parti (come Amazon S3) senza inviare i file attraverso i nostri server.

Il compromesso per questa flessibilità è la complessità: i caricatori di file con il drag-and-drop sono bestie complesse. Avevamo anche bisogno che il nostro uploader React fosse semplice e usabile. Trovare un percorso che garantisse sia la flessibilità che la facilità d'uso non è stato facile.

Identificazione delle responsabilità

Stabilire le responsabilità di un uploader sembra facile... si carica, giusto? Certo, ma ci sono molte altre cose da fare perché ciò avvenga:

  • Deve avere una zona di caduta che cambia in base all'interazione dell'utente. Se l'utente trascina un file su di essa, deve indicare questo cambiamento di stato.
  • E se i nostri utenti non possono trascinare i file? Forse hanno problemi di accessibilità o forse stanno cercando di caricare dal loro telefono. In ogni caso, il nostro uploader deve mostrare un selezionatore di file quando l'utente fa clic o tocca.
  • Deve convalidare il file scelto per assicurarsi che sia di tipo e dimensioni accettabili.
  • Una volta scelto un file, dovrebbe mostrarne un'anteprima durante il caricamento.
  • Dovrebbe fornire all'utente un feedback significativo durante il caricamento, come una barra di avanzamento o un grafico di caricamento che comunichi che sta accadendo qualcosa.
  • Inoltre, cosa succede se fallisce? Deve mostrare all'utente un errore significativo, in modo che sappia che deve riprovare (o rinunciare).
  • Inoltre, deve caricare il file.

Queste responsabilità sono solo un breve elenco, ma ti rendi conto che possono diventare complicate molto rapidamente. Inoltre, durante il caricamento immagini è il nostro caso d'uso principale, ma le esigenze di caricamento dei file potrebbero essere molteplici. Se ti sei preso la briga di capire il comportamento di trascinamento / rilascio / evidenziazione / convalida / trasporto / successo / fallimento, perché scrivere tutto di nuovo quando improvvisamente hai bisogno di caricare CSV per quel report?

Quindi, come possiamo strutturare il nostro React Image Uploader per ottenere la massima flessibilità e riusabilità?

Separazione delle preoccupazioni

Nel diagramma seguente puoi vedere una panoramica del nostro approccio. Non preoccuparti se ti sembra complicato: di seguito analizzeremo ciascuno di questi componenti per conoscere nel dettaglio il loro scopo e il loro ruolo.

La nostra libreria di componenti basata su React non dovrebbe conoscere il funzionamento delle nostre API. Mantenere questa logica separata ha l'ulteriore vantaggio della riusabilità: i diversi prodotti non sono vincolati a un'unica API o addirittura a un unico stile di API. Al contrario, possono riutilizzare tutto o parte del materiale di cui hanno bisogno.

Anche all'interno di componenti di presentazioneÈ possibile separare la funzione dalla presentazione. Quindi abbiamo preso il nostro elenco di responsabilità e abbiamo creato una pila di componenti, dal più generale in basso al più specifico in alto.

Componenti fondamentali

UploaderDropzone

Questo componente è il cuore dell'interfaccia utente dell'uploader, dove inizia l'azione. Gestisce gli eventi di trascinamento e di click-to-browse. Non ha uno stato, ma sa solo come normalizzare e reagire (vedi cosa ho fatto?) a determinate azioni dell'utente. Accetta callback come props in modo da poter dire a chi lo implementa quando accadono delle cose.

Ascolta i file che vengono trascinati su di esso e invoca una callback. Allo stesso modo, quando viene scelto un file, attraverso il trascinamento o il click-to-browse, invoca un'altra callback con l'oggetto file JS.

Ha uno di quegli input di tipo "file" che ho menzionato prima, nascosto all'interno per gli utenti che non possono (o preferiscono) trascinare i file. Questa funzionalità è importante e, astraendola, i componenti che utilizzano la dropzone non devono pensare a come è stato scelto il file.

Quello che segue è un esempio di UploaderDropzone che utilizza React:

Trascina un file qui!

UploaderDropzone ha pochissime opinioni sul suo aspetto e quindi ha uno stile minimo. Ad esempio, Alcuni browser trattano gli eventi di trascinamento in modo diverso quando si verificano su discendenti profondi del nodo di destinazione. Per risolvere questo problema, la dropzone utilizza un unico div trasparente per coprire tutti i suoi discendenti. In questo modo si ottiene l'esperienza necessaria per gli utenti che trascinano e rilasciano, ma anche mantiene l'accessibilità per gli screen reader e altre tecnologie assistive.

UploaderLayoutManager

Il componente UploaderLayoutManager gestisce la maggior parte delle transizioni di stato e sa quale layout deve essere visualizzato per ogni fase del processo, accettando altri componenti React come oggetti di scena per ogni fase.

Questo permette agli implementatori di pensare a ogni fase come a un'idea visiva separata, senza preoccuparsi di come e quando avviene ogni transizione.. I sostenitori di questo componente React devono solo pensare a quale layout deve essere visibile in un determinato momento in base allo stato, non a come vengono popolati i file o a come deve apparire il layout.

Ecco un elenco di passi che possono essere forniti al LayoutManager come oggetti di scena:

  • Passi gestiti da LayoutManager:
    • Non popolata: una dropzone vuota con una call-to-action ("Carica un'immagine fantastica!")
    • File trascinato sulla finestra ma non sulla dropzone ("Lascia qui il file!")
    • File trascinato sulla dropzone ("Lascialo subito!")
    • Caricamento di file in corso ("Aspetta, lo sto inviando...")
  • Passo gestito da un componente che implementa LayoutManager:
    • Il file è stato caricato ed è popolato. Per il nostro caricatore di immagini, questa è un'anteprima dell'immagine con un pulsante "Rimuovi".

Il LayoutManager stesso ha pochi o nessun stile e visualizza solo le immagini che gli sono state passate come oggetti di scena. È responsabile di sapere quale fase del processo è stata raggiunta dall'utente e di visualizzare i contenuti relativi a quella fase.

L'unica fase del layout gestita esternamente è "Anteprima" (se l'Uploader ha un'immagine popolata). Questo perché il componente di implementazione deve definire lo stato in cui inizia l'uploader. Ad esempio, se l'utente ha caricato un'immagine in precedenza, vogliamo mostrarla quando torna sulla pagina.

Esempio di utilizzo di LayoutManager:

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

Componenti specifici per le risorse

Caricatore di immagini

Il componente ImageUploader è quasi interamente orientato alla presentazione: definisce l'aspetto e l'atmosfera di ogni fase e li passa come oggetti di scena a un UploadLayoutManager. È anche un ottimo punto di riferimento per la convalida (tipo di file, dimensione del file, ecc.).

I sostenitori di questo strumento possono concentrarsi quasi esclusivamente sull'aspetto visivo dell'uploader. Questo componente mantiene pochissima logica poiché le transizioni di stato sono gestite dall'UploaderLayoutManager. Possiamo cambiare le immagini in modo fluido e senza preoccuparci di danneggiare il funzionamento dell'uploader.

 

Esempio di ImageUploader:

const DropzoneLayout = () =&gt; (
    <p>Trascina un file qui o clicca per sfogliarlo</p>
);
const DragDropzoneLayout = () =&gt; (
    <p>Lascia il file ora!</p>
);
const LoadingLayout = () =&gt; (
    <p>Attendere, caricamento...</p>
);
const PreviewLayout = ({file, onRemove}) =&gt; (
    <div>
        <p>Nome: {file.name}</p>
        <button onclick="{onRemove}">Rimuovi il file</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; {
            // carica il file!
        })
        .catch(() =&gt; this.setState({file: undefined}))
    }
 
    render() {
        let {file} = this.state;
        lascia che l'anteprima;
 
        if (file) {
            anteprima = (
                <previewlayout file="{file}" onremove="{this._handleRemove}"></previewlayout>
            );
        }
 
        return (
            <uploaderlayoutmanager
                dropzoneelement="{<DropzoneLayout"></uploaderlayoutmanager>}
                dragDropzoneElement={<dragdropzonelayout></dragdropzonelayout>}
                loadingElement={<loadinglayout></loadinglayout>}
                previewElement={preview}
                showPreview={!!file}
                onReceiveFile={this._handleReceiveFile}
            /&gt;
        );
    }
};

Livello specifico dell'applicazione

L'esempio precedente ha un aspetto importante che non riguarda la presentazione: il trasporto dei file che avviene in _handleReceiveFile. Vogliamo che questo componente ImageUploader viva nella nostra libreria di componenti e sia disaccoppiato dal comportamento specifico dell'API, quindi dobbiamo rimuoverlo. Fortunatamente, è semplice accettare una funzione tramite i props che restituisce una promessa che si risolve al termine del caricamento.

_handleReceiveFile(file) {
    // potrebbe eseguire la convalida del file prima del trasporto. Se il file non viene convalidato, restituisce una promessa rifiutata.
    let {uploadImage} = this.props;
 
    this.setState({file});
 
    return uploadImage(file)
        .catch(() => this.setState({file: undefined}))
}

Con questa piccola modifica, lo stesso caricatore di immagini può essere utilizzato per diverse applicazioni. Una parte della tua applicazione può caricare le immagini direttamente su una terza parte (come Amazon S3), mentre un'altra può caricarle su un server locale per uno scopo e una gestione totalmente diversi, ma utilizzando la stessa presentazione visiva.

Ora, poiché tutta questa complessità è compartimentata in ogni componente, ImageUploader ha un'implementazione molto pulita:

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

Con questa base le applicazioni possono utilizzare lo stesso ImageUploader in diversi modi. Abbiamo fornito la flessibilità desiderata mantenendo l'API pulita e semplice. È possibile creare nuovi wrapper su UploadLayoutManager per gestire altri tipi di file o nuovi layout.

In chiusura

Immagina dei caricatori di immagini costruiti appositamente per ogni scenario, ma che contengano solo alcuni semplici componenti fatti di markup di presentazione. Ognuno di essi può utilizzare la stessa funzionalità di caricamento, se ha senso, ma con una presentazione totalmente diversa. Oppure capovolgere l'idea, utilizzando le stesse immagini del caricatore ma con interfacce API completamente diverse.

In quali altri modi potresti utilizzare questi componenti fondamentali? Quali altri uploader vorresti costruire? Il cielo è il limite se ti prendi il tempo di costruire componenti riutilizzabili.

it_ITIT
Scorri in alto