-- Eine lange Geschichte vom Download von Dateien in d.velop cloud-Apps --

tl;dr: App-Builder, die in ihrer App einen Download von Dateien anbieten, sollten dapi.download verwenden und den Response-Header Content-Disposition: attachment nicht vergessen.



Möglichkeiten des Downloads

Wie kannst du einen Download einer Datei im Browser starten? Nun, da gibt es verschiedene Wege:

  1. Link auf die Download-URL (ohne target bzw. "self")
  2. Link auf die Download-URL mit target "_blank"
    • a) Zwischenseite mit Warteinformation / Link
  3. window.open auf die Download-URL
    • a) Zwischenseite mit Warteinformation / Link
  4. IFrame auf die Download-URL zeigen lassen
  5. File-API (heißer Scheiß)
  6. data-URL

Bei Möglichkeit 1-4 ist in jedem Fall wichtig und notwendig, dass die Download-URL mit dem Header Content-Disposition: attachment antwortet.

Vor- und Nachteile der Möglichkeiten im "normalen" Browser

zu 1:

Ein Link auf eine Download-URL ist einer der häufigsten Vertreter. Der Vorteil bei einem Link auf eine Download-URL ist Folgender: Während der Browser im ersten Moment versucht, die Download-URL anzunavigieren, bemerkt er dann aufgrund des Content-Disposition-Headers, dass er die Ressource nicht anzeigen, sondern herunterladen soll. In diesem Moment bricht der Browser die Navigation ab und verbleibt auf der Seite. Für den Anwender fühlt sich das gut an, da er die Navigation nicht mitbekommt, aber trotzdem durch den Progress Indicator und das Starten des Downloads (je Browser unterschiedlich) bemerkt, dass sich etwas tut. Der einzige Nachteil bei dieser Variante tritt dann auf, wenn die Download-URL auf einen Fehler stößt (z.B. der Browser mit "500 Internal Server Error" antwortet). Dann erhält der Browser keinen Content-Disposition-Header und versucht, die Fehlerseite darzustellen. Der Anwender verbleibt nicht mehr auf der ursprünglichen Seite und muss zurücknavigieren. Wenn die Anwendung den aktuellen Stand der Seite (State) nicht korrekt wiederherstellt (so wie es viele der Ressourcen nicht tun), muss der Anwender selbst dafür sorgen. Wenn ihr davon ausgehen könnt, dass die Download-URL "so gut wie immer erreichbar ist", dann ist das eine der besten Möglichkeiten.

zu 2:

Ein Link auf die Download-URL mit target "_blank" umgeht das Problem mit der Fehlerseite, indem die Download-URL innerhalb eines neuen Tabs oder Fensters aufgerufen wird. Antwortet der Server mit einer Fehlerseite, dann muss der Anwender nur den Tab schließen und kehrt zu seiner Anwendung zurück. Allerdings ist das Verhalten für den Anwender nicht so schön wie bei Möglichkeit 1, da beim Download immer erst ein neuer Tab oder ein neues Fenster aufgeht. Der Tab oder das Fenster schließt sich zwar, sobald die ersten Daten übertragen wurden, aber im ersten Moment "flackert" es. Wenn der Server etwas mehr Zeit benötigt, um die ersten Bytes zu übertragen, sieht der Anwender entsprechend auch lange einen neuen weißen Tab.

2a) Den weißen Tab könnt ihr umgehen, indem ihr nicht direkt zur Download-URL navigiert, sondern erst eine "Zwischenseite" anzeigt. Die Zwischenseite weist dann darauf hin, dass der Download in Kürze startet (mit clientseitigem Redirect zur Download-URL). Zusätzlich gebt ihr einen Link auf die Download-URL an. Der Link hilft, falls es Probleme beim Start des Downloads gibt (z.B. Popupblocker, Netzwerkprobleme etc.), sodass der Anwender den Download erneut starten kann. Diese Zwischenseite hat allerdings den Nachteil, dass beim Start des Downloads die Seite nicht automatisch geschlossen wird, was ohne Zwischenseite der Fall ist.

zu 3:

Der Download via window.open unterscheidet sich nur geringfügig von Möglichkeit 2. Wenn ihr window.open nutzt, startet lediglich der Download aus dem JavaScript heraus anstatt per Link (den der Anwender anklickt). Beim Download über JavaScript springt allerdings der Popupblocker schneller an. Ebenso lässt Safari unter iOS keinen Download via JavaScript bis iOS 13 zu. Ab iOS 13 ist das hingegen möglich, darauf gehe ich später noch ein.

3a) Auch in diesem Fall könnt ihr die Möglichkeit wie in 2a) verwenden, indem ihr Navigation auf eine Zwischenseite mit den gleichen Vor- und Nachteilen implementiert.

zu 4:

Wer hätte es gedacht, doch auch ein verstecktes IFrame (z.B. Top und Left auf "-1000" oder Height und Width auf "1px" setzen) könnt ihr dazu verwenden, einen Download zu starten. Ihr setzt einfach die src des IFrames auf die Download-URL und schon startet der Download. Anders als bei den Möglichkeiten 1-3 ist in diesem Fall allerdings besonders wichtig, dass ihr den Content-Disposition-Header auf attachment festlegt. Ist das nicht der Fall und der Browser entscheidet sich, den Content-Type darstellen zu können (z.B. bei PDF-Dateien), dann bekommt der Anwender keinerlei Reaktion, da der Browser dann den Inhalt der Download-URL in dem nicht sichtbaren IFrame darstellt. Das ist gleichzeitig auch der größte Nachteil im Fehlerfall, da ein "500 Internal Server Error" dann ebenfalls in diesem nicht sichtbaren IFrame dargestellt wird. Ansonsten ist diese Möglichkeit aus Anwendersicht die schönste Variante, die die Vorteile von Möglichkeit 1 und 2 vereint. Im Fehlerfall wird nicht wegnavigiert und der State der Anwendung bleibt erhalten, gleichzeitig gibt es kein Flackern durch sich öffnende und schließende Tabs oder Fenster.

zu 5:

HTML5 bietet viele neue schöne Dinge, darunter auch eine File-API. Zum Lesen von Dateien und verarbeiten und versenden vom Client-PC zum Browser funktioniert das auch bereits recht gut (nur in Englisch). Möchte der Anwender allerdings auf dem Client-PC etwas schreiben (also z.B. eine Datei herunterladen und auf der Festplatte speichern), dann sieht die Sache schon anders aus (nur in Englisch) und somit ist diese Möglichkeit schon nicht nutzbar.

zu 6:

data-URLs seien an dieser Stelle nur der Vollständigkeit halber erwähnt, denn sie dienen eher dazu, Content zu speichern, der lokal im Browser generiert wurde. Theoretisch könnt ihr auch data-URLs serverseitig generieren, das macht aber praktisch keinen Sinn, da somit der komplette Dateiinhalt in die data-URL hineingeneriert werden muss und somit die Datei bereits mit Ausliefern der Seite oder der Anwendung heruntergeladen wird. Wenn überhaupt, dann ist diese Möglichkeit höchstens für sehr kleine Dateien oder im Browser generierter Content geeignet. Übrigens: AG-Grid nutzt dieses Feature für den Excel-Export. Aber auch beim Excel-Export gibt es bis iOS 13 Probleme, da der Download via JavaScript gestartet wird (mehr dazu später).

Wie ihr schon seht, gibt es nicht die Lösung, sondern ihr müsst die Vor- und Nachteile abwägen und euch dann für eine der Möglichkeiten (1-4) entscheiden. Geht ihr davon aus, dass die Download-URL i.d.R. erreichbar ist, ist Möglichkeit 4 die beste Wahl. Solange eure Anwender nur einen "einfachen" Browser verwenden, gibt es keine großen Hindernisse (wenn ihr von iOS mal abseht).

Vor- und Nachteile der Möglichkeiten in den Integrationen

Eine der großen Vorteile von d.velop cloud-Apps ist es, dass ihr diese in verschiedene Anwendungen integrieren könnt. Ihr benötigt lediglich einen eingebetteten Browser oder verwendet mittels WebView einen vom Betriebssystem bereitgestellten Browser.

Microsoft Outlook

Die Integration der d.velop cloud-Apps in Outlook verwendet aktuell den Embedded IE und hat die wenigsten Probleme, denn ein Download ist mit den Möglichkeiten 1-4 und den beschriebenen Vor- und Nachteilen möglich. Allerdings wird bei einem neuem Tab oder Fenster ggf. ein neuer Prozess mit dem normalen Browser (und nicht Embedded IE) gestartet, der dann ggf. eine erneute Authentifizierung benötigt. Konfiguriert ihr das Outlook-Add-In so, dass beim Aufrufen eines neuen Tabs (window.open oder target "_blank") nicht ein Prozess des Standardbrowsers gestartet werden soll, sondern innerhalb von Outlook der Hauptbereich die Navigation übernehmen soll, dann schließt sich dieser auch automatisch, sobald er den Header Content-Disposition: attachment liest. Die Möglichkeit 4 ist aber in diesem Fall auch für den Anwender die Schönste.

IBM Notes

Und hier fangen die ersten Probleme an: Bei der Integration in IBM Notes wird aktuell auch der Embedded IE verwendet und ein Download ist ebenfalls mit dem Möglichkeiten 1-4 möglich, ebenfalls mit dem gleichen Problem bzgl. ggf. notwendiger erneuter Authentifizierung. Konfiguriert ihr das Notes-Widget allerdings so, dass beim Aufrufen eines neuen Tabs dieses innerhalb von Notes im Hauptbereich übernommen werden soll, dann bleibt nach Starten des Downloads ein weißer Tab (oder die Zwischenseite) stehen. Alle Bemühungen, diesen weißen Tab zu erkennen und automatisch vom Notes-Widget schließen zu lassen, sind gescheitert. Somit fallen die Möglichkeiten 2 und 3 raus, wenn ihr keine weiße Seite sehen möchtet.

Glücklicherweise funktioniert im Notes-Kontext aber der Download via verstecktem IFrame und somit könnt ihr einfach die Möglichkeit 4 wählen und seid sauber raus.

Microsoft Office

Bei Microsoft Office ist es so, dass ihr selbst kein vollständiges Add-In oder Widget wie bei Outlook und Notes schreibt, sondern Microsoft sieht von vorne herein einen Embedded Browser (den Embedded IE bzw. ganz aktuell wird in diesem Fall der Embedded Edge laufen (nur in Englisch)) selbst vor, sodass ihr "nur noch" eure URL angeben müsst. Allerdings hat Microsoft festgelegt (warum auch immer), dass keine GUI vom Browser eingeblendet werden darf. So ist z.B. window.alert abgeklemmt (zusätzlich ist window.alert in office.js überschrieben). Dieser Umstand führt aber auch dazu, dass selbst ein Speichern unter-Dialog, wie er vom IE im Normalfall beim Speichern angezeigt wird, in Office nicht angezeigt wird. Selbst eine Anfrage bei Stack Overflow (nur in Englisch), wie wir das Problem umgehen könnten, brachte keinen Erfolg. Somit fallen die Möglichkeiten 1 und 4 flach, sodass noch 2 und 3 übrig bleiben. 

"Feature-Detection" oder "Host-Detection" oder ...?

Wie ihr seht, schließen sich Notes und Office aus, wenn ihr mit einer Variante für den Anwender in allen Integrationen das schönste "Downloaderlebnis" haben möchtet. Wie lösen wir nun dieses Problemchen? Am besten wäre es, so wie wir es auch in anderen Bereichen machen, wenn wir mittels Feature-Detection erkennen könnten, welche Variante der gerade genutzte Browser unterstützt. Leider ist das Feature-Detection nicht möglich, da es hier um den Download geht, um wir außerhalb des Scopes von CSS, HTML und JavaScript sind. Bleibt noch die Host-Detection. Erkennt die Anwendung, dass sie sich in dem Host "Office" oder "Notes" befindet, könnte sie entsprechend reagieren. Abgesehen davon, dass das technisch schwierig, wenn nicht sogar unmöglich ist, ist es auch nicht erwünscht. Die Vergangenheit der Webentwicklung hat gezeigt, dass eine Host-Detection (was in diesem Fall einer Browser-Detection sehr nahe kommt) eine schlechte Idee ist, weshalb wir auf Feature-Detection gewechselt sind, was ja in diesem Fall nicht möglich ist.

"dapi.download" als Ausweg zur "Detection-Falle"?

Warum nicht den Host selbst entscheiden lassen? Der Host weiß ja, welche Variante möglich ist. Die Integrationen in Notes, Outlook und Office haben sowieso eine eigene Seite und laden die Shell in einem IFrame und haben somit die Möglichkeit, JavaScript Host-spezifisch auszuführen. 

In diesem Zuge ist die Methode download in der d.api entstanden. Der Methode übergebt ihr eine URL, die ihr herunterladen möchtet und alles Weitere übernimmt die Methode bzw. die Shell. Im Standard wählt die Shell die Möglichkeit 4 für den Download, bietet allerdings über die Methode useNewWindowForDownload in der Bridge-API die Möglichkeit, das Verhalten auf Möglichkeit 3 zu ändern. Somit kann der Host über die Bridge-API mitteilen, welche Downloadvariante er unterstützt und ihr als App-Builder braucht euch keine Gedanken machen, wie eine Datei herunterzuladen ist, ihr verwendet einfach dapi.download und seid fertig.

iOS spielt nicht mit

Bis hierher funktioniert alles soweit, gäbe es da nicht Safari unter iOS. Apple kennt nicht das Konzept des Downloads und stellt Inhalte direkt dar. Daher wird auch bei einem neuen Tab oder IFrame der Content-Disposition-Header ignoriert und Safari versucht, das Dokument direkt darzustellen. Ebenso stellt Apple den Anwender so weit in den Vordergrund, dass ein Download via JavaScript, der nicht im direkten Zusammenhang mit einem Klick gebracht werden kann, unterbunden wird. Dadurch ist ein Download mittels dapi.download nicht möglich.

iOS spielt doch mit (ab iOS 13, September 2019)

Glücklicherweise hat sich Apple mit iOS 13 den anderen Browsern bzw. dem Desktop etwas angenähert und bietet nun einen Download-Manager an (nur in Englisch). Gleichzeitig wird auch der Content-Disposition-Header berücksichtigt und ein Download via JS ist möglich. Bei diesem Prozess wird allerdings dem Anwender noch die Frage gestellt, ob er die Datei wirklich herunterladen möchte, was der User-Experience aber nicht widerspricht.

Ausblick

Für euch als App-Builder ist es nun das einfachste, wenn ihr dapi.download verwendet, da ihr euch um nichts weiter kümmern muss. Der Vorteil für die Zukunft liegt auch darin, dass in der Shell oder auch der Bridge die Möglichkeit besteht, Erweiterungen einzubauen. So wäre es denkbar, eine eigene Download-Methode in der Shell zu registrieren, mit der ihr euer eigenes Ziel definiert (z.B. "Download" speichert die Datei automatisch in Google Drive oder OneDrive o.ä.). Sollten andere Integrationen andere Probleme aufweisen, können wir eine Anpassung an zentraler Stelle vornehmen und das Problem beheben. Ihr selbst müsst euch nicht mit diesen Problemen und deren Lösung herumschlagen, sondern könnt euch darauf verlassen, dass der Download einfach funktioniert.

Check!!!