Daniel wurde kürzlich im Eventbrite Engineering Blog vorgestellt und wir sind sehr stolz auf die Arbeit, die er dort leistet.
Das Eventbrite-Team brauchte einen React-basierten Bild-Uploader, der flexibel ist und gleichzeitig eine einfache Benutzeroberfläche bietet. Die Komponenten des Bild-Uploaders sollten in einer Vielzahl von Szenarien als wiederverwendbare Teile funktionieren, die je nach Bedarf unterschiedlich zusammengestellt werden können. Lies weiter, um zu sehen, wie wir dieses Problem gelöst haben.
Was ist ein Datei-Uploader?
Wenn du in der Vergangenheit eine Datei von deinen Webnutzern abrufen wolltest, musstest du eine Eingabetyp "Datei". Dieser Ansatz war in vielerlei Hinsicht begrenzt, vor allem dadurch, dass es ist eine Eingabe: Die Daten werden erst übertragen, wenn du das Formular abschickst, sodass die Nutzer keine Möglichkeit haben, vor oder während des Hochladens ein Feedback zu sehen.
In diesem Sinne sind die React-Uploader, über die wir sprechen werden, keine Formulareingaben, sondern "Sofort-Transport-Tools". Der Nutzer wählt eine Datei aus, der Uploader transportiert sie zu einem entfernten Server und erhält dann eine Antwort mit einer eindeutigen Kennung. Diese Kennung wird dann sofort mit einem Datenbankeintrag verknüpft oder in ein verstecktes Formularfeld eingetragen.
Diese neue Strategie bietet eine enorme Flexibilität gegenüber herkömmlichen Upload-Prozessen. Durch die Entkopplung des Dateitransports von den Formularübermittlungen können wir zum Beispiel direkt auf Speicherplätze von Drittanbietern (wie Amazon S3) hochladen, ohne die Dateien über unsere Server zu schicken.
Der Kompromiss für diese Flexibilität ist die Komplexität: Drag-and-Drop-Datei-Uploader sind komplexe Gebilde. Außerdem sollte unser React-Uploader einfach und benutzerfreundlich sein. Es war keine leichte Aufgabe, einen Weg zu finden, der sowohl Flexibilität als auch Benutzerfreundlichkeit bietet.
Verantwortlichkeiten festlegen
Die Aufgaben eines Uploaders zu bestimmen, scheint einfach zu sein... er lädt hoch, richtig? Sicherlich, aber es gibt noch eine Menge anderer Dinge, die dazu gehören, damit das passiert:
- Sie muss eine Drop-Zone haben, die sich je nach Benutzerinteraktion ändert. Wenn der/die Nutzer/in eine Datei darüber zieht, sollte sie diese Zustandsänderung anzeigen.
- Was ist, wenn unsere Nutzer keine Dateien ziehen können? Vielleicht haben sie Probleme mit der Barrierefreiheit oder sie versuchen, von ihrem Handy aus hochzuladen. In jedem Fall muss unser Uploader eine Dateiauswahl anzeigen, wenn der Nutzer klickt oder tippt.
- Es muss die ausgewählte Datei überprüfen, um sicherzustellen, dass sie einen akzeptablen Typ und eine akzeptable Größe hat.
- Sobald eine Datei ausgewählt wurde, sollte eine Vorschau der Datei angezeigt werden, während sie hochgeladen wird.
- Sie sollte dem/der Nutzer/in während des Hochladens ein sinnvolles Feedback geben, z.B. einen Fortschrittsbalken oder eine Ladegrafik, die anzeigt, dass etwas passiert.
- Und was ist, wenn es fehlschlägt? Es muss dem Benutzer eine aussagekräftige Fehlermeldung angezeigt werden, damit er weiß, dass er es noch einmal versuchen (oder aufgeben) soll.
- Oh, und die Datei muss tatsächlich hochgeladen werden.
Diese Aufgaben sind nur eine kurze Liste, aber du verstehst schon, dass sie sehr schnell kompliziert werden können. Außerdem müssen beim Hochladen Bilder unser Hauptanwendungsfall ist, kann es eine Vielzahl von Anforderungen für das Hochladen von Dateien geben. Wenn du dir schon die Mühe gemacht hast, das Verhalten von Drag / Drop / Markieren / Validieren / Transportieren / Erfolg / Misserfolg herauszufinden, warum solltest du dann alles noch einmal schreiben, wenn du plötzlich CSV-Dateien für diesen einen Bericht hochladen musst?
Wie können wir also unseren React Image Uploader strukturieren, um maximale Flexibilität und Wiederverwendbarkeit zu erreichen?
Trennung der Belange
Im folgenden Diagramm siehst du einen Überblick über unseren geplanten Ansatz. Keine Sorge, wenn es dir kompliziert vorkommt - wir werden jede dieser Komponenten weiter unten genauer unter die Lupe nehmen, um mehr über ihren Zweck und ihre Rolle zu erfahren.
Unsere React-basierte Komponentenbibliothek sollte nicht wissen müssen, wie unsere APIs funktionieren. Diese Logik getrennt zu halten, hat den zusätzlichen Vorteil der Wiederverwendbarkeit; verschiedene Produkte sind nicht an eine einzige API oder sogar an einen einzigen Stil der API gebunden. Stattdessen können sie so viel oder so wenig wiederverwenden, wie sie brauchen.
Auch innerhalb Präsentationskomponentengibt es die Möglichkeit, Funktion und Präsentation zu trennen. Wir haben also unsere Liste der Aufgaben genommen und einen Stapel von Komponenten erstellt, der von der allgemeinsten ganz unten bis zur spezifischsten ganz oben reicht.
Grundlegende Komponenten
UploaderDropzone
Diese Komponente ist das Herzstück der Uploader-Benutzeroberfläche, wo die Aktion beginnt. Sie verarbeitet Drag/Drop-Ereignisse und Click-to-Browse. Sie hat selbst keinen Status, sondern weiß nur, wie sie sich normalisiert und auf bestimmte Benutzeraktionen reagiert (siehst du, was ich da gemacht habe?). Er akzeptiert Rückrufe als Requisiten, damit er seinem Implementierer mitteilen kann, wann etwas passiert.
Es wartet darauf, dass Dateien darüber gezogen werden und ruft dann einen Callback auf. Wenn eine Datei ausgewählt wird, entweder durch Ziehen/Ablegen oder durch Klicken zum Durchsuchen, wird ein weiterer Callback mit dem JS-Dateiobjekt aufgerufen.
Sie hat eine der Eingaben vom Typ "Datei", die ich bereits erwähnt habe, für Benutzer, die keine Dateien ziehen können (oder wollen). Diese Funktion ist wichtig. Indem wir sie hier abstrahieren, müssen die Komponenten, die die Dropzone verwenden, nicht darüber nachdenken, wie die Datei ausgewählt wurde.
Im Folgenden siehst du ein Beispiel für UploaderDropzone mit React:
Ziehe eine Datei hierher!
UploaderDropzone hat sehr wenig Meinung darüber, wie es aussieht, und hat daher nur ein minimales Styling. Zum Beispiel, einige Browser behandeln Drag-Events unterschiedlich wenn sie auf tiefen Nachkommen des Zielknotens auftreten. Um dieses Problem zu lösen, verwendet die Dropzone ein einziges transparentes Div, das alle Unterknoten abdeckt. Dies sorgt für die nötige Erfahrung für Nutzer, die ziehen/ablegen, aber auch die Zugänglichkeit für Bildschirmleser und andere unterstützende Technologien aufrechterhält.
UploaderLayoutManager
Die Komponente UploaderLayoutManager verwaltet die meisten Zustandsübergänge und weiß, welches Layout für jeden Schritt des Prozesses angezeigt werden soll, während sie andere React-Komponenten als Requisiten für jeden Schritt akzeptiert.
Dies ermöglicht es, jeden Schritt als separate visuelle Idee zu betrachten, ohne sich Gedanken darüber zu machen, wie und wann jeder Übergang stattfindet.. Die Unterstützer dieser React-Komponente müssen sich nur Gedanken darüber machen, welches Layout zu einem bestimmten Zeitpunkt je nach Zustand sichtbar sein soll, nicht aber darüber, wie die Dateien aufgefüllt werden oder wie das Layout aussehen soll.
Hier ist eine Liste von Schritten, die dem LayoutManager als Requisiten übergeben werden können:
- Schritte, die vom LayoutManager verwaltet werden:
- Unbevölkert - eine leere Dropzone mit einem Aufruf zur Aktion ("Lade ein tolles Bild hoch!")
- Datei wird über das Fenster gezogen, aber nicht über die Dropzone ("Lass die Datei hier fallen!")
- Datei wird über Dropzone gezogen ("Jetzt fallen lassen!")
- Datei-Upload läuft ("Warte, ich schicke es...")
- Schritt, der von einer Komponente verwaltet wird, die den LayoutManager implementiert:
- Die Datei wurde hochgeladen und wird ausgefüllt. Für unseren Bild-Uploader ist dies eine Vorschau des Bildes mit einer Schaltfläche "Entfernen".
Der LayoutManager selbst hat wenig oder gar keine Stile und zeigt nur Bildmaterial an, das als Requisiten übergeben wurde. Er ist dafür verantwortlich, dass der Benutzer weiß, welchen Schritt im Prozess er erreicht hat, und zeigt die Inhalte für diesen Schritt an.
Der einzige Layout-Schritt, der extern verwaltet wird, ist "Vorschau" (ob der Uploader ein Bild eingefügt hat). Das liegt daran, dass die implementierende Komponente den Zustand festlegen muss, in dem der Uploader startet. Wenn der Nutzer z. B. zuvor ein Bild hochgeladen hat, soll dieses Bild angezeigt werden, wenn er auf die Seite zurückkehrt.
Beispiel für die Verwendung des LayoutManagers:
<uploaderlayoutmanager
dropzoneElement={}
windowDragDropzoneElement={}
dragDropzoneElement={}
loadingElement={}
previewElement={}
showPreview={!!file}
onReceiveFile={handleReceiveFile}
/>
Ressourcen-spezifische Komponenten
ImageUploader
Die ImageUploader-Komponente ist fast ausschließlich auf die Präsentation ausgerichtet. Sie definiert das Aussehen der einzelnen Schritte und übergibt sie als Requisiten an einen UploadLayoutManager. Hier ist auch ein guter Platz für die Validierung (Dateityp, Dateigröße usw.).
Unterstützer dieses Tools können sich fast ausschließlich auf das optische Erscheinungsbild des Uploaders konzentrieren. Diese Komponente enthält nur sehr wenig Logik, da die Zustandsübergänge vom UploaderLayoutManager verwaltet werden. Wir können das Bildmaterial fließend ändern, ohne uns Sorgen zu machen, dass die Funktion des Uploaders beeinträchtigt wird.
Beispiel ImageUploader:
const DropzoneLayout = () => (
<p>Ziehe eine Datei hierher oder klicke zum Durchsuchen</p>
);
const DragDropzoneLayout = () => (
<p>Datei jetzt ablegen!</p>
);
const LoadingLayout = () => (
<p>Bitte warten, laden...</p>
);
const PreviewLayout = ({file, onRemove}) => (
<div>
<p>Name: {Datei.Name}</p>
<button onclick="{onRemove}">Datei entfernen</button>
</div>
);
class ImageUploader extends React.Component {
state = {file: undefined};
_handleRemove = () => this.setState({file: undefined});
_handleReceiveFile = (file) => {
this.setState({file}));
return new Promise((resolve, reject) => {
// Lade die Datei hoch!
})
.catch(() => this.setState({file: undefined}))
}
render() {
let {file} = this.state;
let Vorschau;
if (file) {
preview = (
<previewlayout file="{file}" onremove="{this._handleRemove}"></previewlayout>
);
}
return (
<uploaderlayoutmanager
dropzoneelement="{<DropzoneLayout"></uploaderlayoutmanager>}
dragDropzoneElement={<dragdropzonelayout></dragdropzonelayout>}
loadingElement={<loadinglayout></loadinglayout>}
previewElement={Vorschau}
showPreview={!!file}
onReceiveFile={this._handleReceiveFile}
/>
);
}
};
Anwendungsspezifische Schicht
Das obige Beispiel hat einen auffälligen Aspekt, der nichts mit der Präsentation zu tun hat: der Dateitransport, der in _handleReceiveFile stattfindet. Wir wollen, dass diese ImageUploader-Komponente in unserer Komponentenbibliothek lebt und vom API-spezifischen Verhalten entkoppelt ist, also müssen wir das entfernen. Zum Glück ist es so einfach, wie eine Funktion über props zu akzeptieren, die ein Versprechen zurückgibt, das aufgelöst wird, wenn der Upload abgeschlossen ist.
_handleReceiveFile(file) {
// Hier könnte die Datei vor dem Transport geprüft werden. Wenn die Datei die Validierung nicht besteht, wird ein abgelehntes Versprechen zurückgegeben.
let {uploadImage} = this.props;
this.setState({file});
return uploadImage(file)
.catch(() => this.setState({file: undefined}))
}
Mit dieser kleinen Änderung kann derselbe Bild-Uploader für eine Vielzahl von Anwendungen genutzt werden. Ein Teil deiner Anwendung kann Bilder direkt zu einem Drittanbieter (z. B. Amazon S3) hochladen, während ein anderer Teil Bilder zu einem völlig anderen Zweck und für eine völlig andere Handhabung auf einen lokalen Server hochlädt, aber die gleiche visuelle Darstellung verwendet.
Und weil diese ganze Komplexität in jede Komponente aufgeteilt ist, hat der ImageUploader eine sehr saubere Implementierung:
<imageuploader uploadImage={S3ImageUploadApi}></imageuploader>
Mit dieser Grundlage können Anwendungen denselben ImageUploader auf verschiedene Weise nutzen. Wir haben die gewünschte Flexibilität geschaffen und gleichzeitig die API sauber und einfach gehalten. Neue Wrapper können auf dem UploadLayoutManager aufgebaut werden, um andere Dateitypen oder neue Layouts zu verarbeiten.
Zum Schluss
Stell dir Bild-Uploader vor, die speziell für jedes Szenario entwickelt wurden, aber nur ein paar einfache Komponenten aus Präsentationsmarkup enthalten. Sie können beide die gleiche Upload-Funktionalität nutzen, wenn es Sinn macht, aber mit einer völlig anderen Präsentation. Oder du drehst die Idee um und verwendest die gleichen Uploader-Visualisierungen, aber mit völlig unterschiedlichen API-Schnittstellen.
Wie würdest du diese grundlegenden Komponenten noch verwenden? Welche anderen Uploader würdest du bauen? Der Himmel ist die Grenze, wenn du dir die Zeit nimmst, wiederverwendbare Komponenten zu bauen.
