2024-11-05

Uppy - Bessere UX bei File Uploads

Ich habe Uppy durch eines meiner letzten Kundenprojekte kennengelernt. Der Wunsch war eine einfache und ansprechende Frontend-Lösung zu finden, mit der man Dateien in ein Backend (Supabase Storage) hochladen kann.

Uppy erfüllte alle Anforderungen. In diesem Teil werden wir Uppy etwas genauer unter die Lupe nehmen. Allerdings werde ich nicht auf jedes einzelne Feature eingehen, denn dafür ist es besser die Dokumentation von Uppy zu lesen.

Resumable Uploads (tus)

Uppy unterstützt das tus Protokoll und ermöglicht es somit, einen länger andauernden Upload zu unterbrechen. Tus ist ein offenes Protokoll mit dem Ziel, den Upload-Prozess robuster zu machen.

Uppy speichert während des Uploads für jede Datei eine Referenz im lokalen Speicher des Browsers. Standardmäßig verhindert dies auch, dass eine Datei zweimal hochgeladen werden kann, da sie sich immer noch im Local Storage befindet. Dies kann jedoch abgeschaltet werden, wenn das mehrfache Hochladen der gleichen Datei ein Anwendungsfall ist.

new Uppy().use(Tus, {
  endpoint: // your endpoint,
  // other options
  removeFingerprintOnSuccess: true, // <---
});

Sources

Uppy unterstützt eine Vielzahl von Quellen, die als Datei hochgeladen werden können:

  • Datei-Upload über den normalen "Durchsuchen..." Dialog. Ganz normal wie bei jedem File Uploader.
  • Google Drive, OneDrive, etc.
  • Direkter Import von einer URL
  • WebCam und Mikrofon
  • usw.

Zeigt die verschiedenen Quellen (Sources) von welchen Uppy Files hochladen kann.

Leider funktioniert nur der normale Datei-Upload sowie per WebCam und Mikrofon ohne zusätzliches Setup. Für alle anderen Quellen (z.B. OneDrive) benötigt man entweder einen externen Service von Transloadit oder man hostet den Open Source verfügbaren Server (genannt Companion) selbst. Beides bedeutet zusätzlichen Aufwand. Mit Transloadit hat man einen weiteren Partner, über den die Daten laufen, was entsprechende Abklärungen und Überlegungen nach sich zieht. Einen Companion Server selbst zu hosten ist zwar schnell gemacht, aber man muss sich um Sicherheitsupdates und Wartung kümmern.

Deshalb habe ich es vorerst vorgezogen, nur den normalen Dateiupload zu unterstützen. Uppy bietet aber auch so genug Vorteile, dass es sich lohnt, das NPM-Paket einzubinden.

UI / UX

Uppy bietet von Haus aus verschiedene UIs. Die umfassendste ist das Dashboard. Wenn man etwas weniger extravagantes für seinen Anwendungsfall sucht, ist man mit dem Drag & Drop UI besser bedient. Das Drag & Drop UI kann wiederum modular um verschiedene Funktionen erweitert werden, die das Dashboard schon von Haus aus unterstützt.

Für mein Projekt habe ich das Dashboard UI verwendet, daher werden wir uns hier auch diesem UI widmen.

Das Dashboard UI besteht aus einer einfachen rechteckigen Box, in die man Dateien ziehen oder über "Browse files" auswählen kann. Screenshot welcher den uppy File Uploader zeigt.

Die ausgewählten Dateien werden dann aufgelistet und wenn möglich mit einer Vorschau angezeigt. Der Upload kann dann über den entsprechenden Button gestartet werden. Dieses Bild zeigt wie Uppy für den Upload ausgewählte Files auflistet.

Es gäbe noch weitere Plugins wie z.B. einen Compressor um hochzuladende Bilder gleich zu komprimieren (z.B. für ein Blog oder CMS). Ein weiteres tolles Plugin ist auch der Image Editor um dem Benutzer die Möglichkeit zu geben die hochzuladenden Bilder vorher zu bearbeiten. Beides habe ich jedoch nicht selber verwendet und kann daher keine Aussage über die Qualität der Plugins machen.

Schön ist auch, dass im Dashboard gleich ein Fortschrittsbalken integriert ist, der den Fortschritt des Uploads anzeigt.

Umsetzung in Next.js mit Supabase

In meinem Fall verwende ich Supabase als Backend und Next.js / React als Frontend. Daher zeige ich hier, wie ich Uppy in diesen Technologie-Stack integriert habe.

Eigentlich bin ich erst durch die Dokumentation von Supabase auf Uppy aufmerksam geworden. Diese erwähnt Uppy nämlich an folgender Stelle: Resumable Uploads - UppyJS example.

In der Dokumentation sind also eigentlich schon fast alle Infos vorhanden, um Uppy mit Supabase zum Laufen zu bringen. Ein paar Dinge sind aber in den offiziellen Beispielen nicht ersichtlich und diese möchte ich euch nicht vorenthalten.

Partitionierung

In der Uppy Dokumentation ist der Code um Uppy einzubinden recht überschaubar. Allerdings kann die Anzahl der Zeilen in einem echten Beispiel schnell anwachsen. Deshalb macht es Sinn, den Code für Uppy in verschiedene Teile aufzuteilen.

Eine Möglichkeit dies zu tun ist

  • useUppy() Hook Ein Hook der sich um das grundlegende Setup von Uppy kümmert.
  • FileUpload Komponente Die FileUpload Komponente beinhaltet das JSX um das Uppy Dashboard anzuzeigen, sowie weiteres EventHandling.

Im Prinzip könnte man den Code der FileUpload Component auch in den useUppy Hook integrieren. Ich habe das aber bewusst weggelassen, damit man den Hook auch einfach mit anderen Uppy UIs verwenden kann (z.B. Drag & Drop).

useUppy()

Der useUppy Hook ist relativ einfach. Deshalb kann ich ihn hier direkt anhängen.

import Uppy from "@uppy/core";
import Tus from "@uppy/tus";
import { useState } from "react";

export function useUppy() {
  const [uppy] = useState<Uppy>(() => {
    const supabaseStorageURL = `${process.env.NEXT_PUBLIC_SUPABASE_URL}/storage/v1/upload/resumable`;
    return new Uppy().use(Tus, {
      endpoint: supabaseStorageURL,
      uploadDataDuringCreation: true,
      chunkSize: 6 * 1024 * 1024,
      allowedMetaFields: [
        "bucketName",
        "objectName",
        "contentType",
        "cacheControl",
      ],
      removeFingerprintOnSuccess: true,
      onError: function (error) {
        console.log("Failed because: " + error);
      },
    });
  });

  return uppy;
}

Der Code ist relativ direkt aus dem Beispiel in der Supabase Doku. Das removeFingerprintOnSuccess wurde bereits im oberen Abschnitt erklärt.

Man kann sich entweder direkt bei der Instanzierung von Uppy für Events registrieren oder später mit dem von Uppy unterstützten Hook useUppyEvent(...). Uppy Events sind das eine, aber es gibt auch Events von Quellen und Plugins. In diesem Beispiel ist das die Quelle tus. Hier registriere ich mich direkt nach dem use(Tus, {... auf das onError: ... Event um Fehler erst mal auf die Console zu loggen.

Man sollte auch darauf achten, wo man useUppy aufruft, um nicht versehentlich das Hochladen von Dateien zu unterbrechen. In meinem Fall zum Beispiel durchläuft der Benutzer eine Art Wizard, der ihm Schritt für Schritt alle nötigen Informationen liefert, um ein Markdown File in ein PDF zu konvertieren. Wizard welcher aus vier Schritten besteht. Geht der Benutzer einen Schritt zurück, möchte man nicht die gesamte eingegebene File List verlieren. Bei geschachtelten Components (Parent/Child) kann es daher sinnvoll sein, useUppy im Parent aufzurufen und dann die Variable uppy an das Child weiterzureichen. (Hintergrund: React Docs - Verschiedene Komponenten an der gleichen Position zurücksetzen)

FileUpload Komponente

Die FileUpload Component hat die Aufgabe das Uppy Dashboard UI zum JSX hinzuzufügen, sowie einige weitere Hilfsfunktionen zu implementieren:

  • Setzen des Bearer Tokens auf das jeweils neueste (Supabase Auth) Access Token.
  • Registrierung auf gewünschte Events mittels useUppyEvent(...).

Da der Code etwas umfangreicher ist (und aus Gründen der Geheimhaltung), werde ich ihn hier nicht 1:1 einfügen, sondern nur Ausschnitte.

Im Folgenden verwenden wir einen useEffect, um das Bearer Token von Tus neu zu setzen, falls sich das Supabase Auth AccessToken ändert. Außerdem registrieren wir uns auf nützliche Events:

export function FileUpload({
  uppy,
}: {
  uppy: Uppy;
}) {
  const session = useSession();

  useEffect(() => {
    if (uppy) {
      if (!session?.access_token) {
        console.log("Access token is not set");
        return;
      }
      uppy.getPlugin("Tus")?.setOptions({
        headers: {
          authorization: `Bearer ${session?.access_token}`,
        },
      });
    }
  }, [session, uppy]);

  // Beispiel wie man sich direkt auf ein Uppy Event registriert:
  useUppyEvent(uppy, "file-added", (file) => { /* Mach was... */ });
  useUppyEvent(uppy, "complete", (file) => { /* Mach was... */ });
  return (
    <Dashboard uppy={uppy} theme="dark" proudlyDisplayPoweredByUppy={false} />
  );

Fazit

Uppy ist ein praktischer und ansprechender Datei-Uploader, der modular aufgebaut ist. Meine Erfahrungen mit dem Uppy NPM Paket und auch mit der Dokumentation waren sehr gut.

Ich hoffe, dass ihr mit den zusätzlichen Informationen in diesem Blogpost Uppy auch in eurem nächsten Projekt erfolgreich einsetzen könnt :)