Fallstudie: Download per Drag-and-drop in Chrome

David Tong
David Tong

Einführung

Drag-and-drop (DnD) ist eine der vielen großartigen Funktionen von HTML 5 und wird in Firefox 3.5, Safari, Chrome und IE unterstützt. Google hat vor Kurzem eine neue Funktion eingeführt, mit der Google Chrome-Nutzer Dateien per Drag-and-drop aus dem Browser auf den Desktop ziehen können. Es ist eine äußerst praktische Funktion, die aber erst durch einen Artikel von Ryan Seddon bekannt wurde, in dem er seine Entdeckungen beim Reverse-Engineering dieser neuen Funktion beschrieb.

Wir bei Box.net freuen uns sehr, dass wir mit diesen neuen Funktionen unsere Lösung für die Cloud-Inhaltsverwaltung verbessern und einen größeren Beitrag zur Entwicklergemeinschaft leisten können. Ich freue mich, Ihnen mitteilen zu können, dass DnD Download in unser Produkt integriert wurde. Box-Nutzer können Dateien jetzt direkt aus einem Chrome-Browser auf ihren Desktop ziehen, um sie herunterzuladen und zu speichern.

Ich möchte Ihnen zeigen, wie ich während der Entwicklung dieser neuen Funktion mehrere Iterationen durchlaufen habe.

Unterstützung der Drag-and-drop API prüfen

Prüfen Sie zuerst, ob Ihr Browser HTML5-Drag-and-drop vollständig unterstützt. Eine einfache Möglichkeit dazu ist die Verwendung der Bibliothek Modernizr, um nach einer bestimmten Funktion zu suchen:

if (Modernizr.draganddrop) {
// Browser supports native HTML5 DnD.
} else {
// Fallback to a library solution.
}

Ausführung 1

Ich habe zuerst den Ansatz ausprobiert, den Seddon in Gmail gefunden hat. Ich habe ein neues Attribut namens „data-downloadurl“ hinzugefügt, um Links zu Dateien zu verankern. Dabei werden die benutzerdefinierten Datenattribute von HTML5 verwendet. In „data-downloadurl“ müssen Sie den MIME-Typ der Datei, den Namen der Zieldatei (den gewünschten Dateinamen der heruntergeladenen Datei) und die Download-URL der Datei angeben. Daher wird Folgendes in die HTML-Vorlage eingefügt:

<a href="#" class="dnd"
data-downloadurl="{$item.mime}:{$item.filename}:{$item.url}"></a>

Die Ausgabe sollte in etwa so aussehen:

<a href="#" class="dnd" data-downloadurl=
"image/jpeg:Penguins.jpg:https://www.box.net/box_download_file?file_id=f66690"></a>

Basierend auf einem plugin von Schorsch, das auf dem Artikel von Seddon basiert, habe ich ein jQuery-Plug-in hinzugefügt, das die Browserfunktionen erkennt. Hervorgehoben sind die Zeilen, die ich der Version von Schorsch hinzugefügt habe:

(function($) {

$.fn.extend({
dragout: function() {
var files = this;
if (files.length > 0) {
    $(files).each(function() {
    var url = (this.dataset && this.dataset.downloadurl) ||
                this.getAttribute("data-downloadurl");
    if (this.addEventListener) {
        this.addEventListener("dragstart", function(e) {
        if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
            e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
            e.dataTransfer.setData("DownloadURL", url);
        }
        },false);
    }
    });
}
}
});

})(jQuery);

Der Grund dafür ist, dass ohne vorherige Browsererkennung ein JavaScript-Fehler auftritt, wenn addEventListener() auf ein HTML-Element im IE angewendet wird, da der IE seine eigene attachEvent()-Methode verwendet. e.dataTransfer ist im IE derzeit nicht definiert. e.dataTransfer.constructor gibt in Firefox (Mozilla) DataTransfer zurück, während Webkit-Browser (Chrome und Safari) den Clipboard-Konstruktor implementieren. In Safari gibt e.dataTransfer.setData('DownloadURL','http://www.box.net') „falsch“ zurück, in Chrome „wahr“. Wenn Sie alle oben genannten Tests durchführen, ist die Funktion nur für Chrome verfügbar. Sie könnten argumentieren, dass ich einfach Folgendes tun könnte:

/chrome/.test( navigator.userAgent.toLowerCase() )

Ich bevorzuge jedoch die Funktionserkennung gegenüber der Browsererkennung, obwohl damit technisch nicht erkannt wird, ob der Download von Inhalten im Hintergrund funktioniert.

Probleme bei Iteration 1

1) Da wir derzeit das Drag-and-drop auf der Seite zum Verschieben/Kopieren von Dateien zwischen Ordnern aktiviert haben, benötigen wir eine Möglichkeit, zwischen Drag-and-drop-Download und Drag-and-drop auf der Seite zu unterscheiden. Technisch gesehen können wir diese beiden Aktionen nicht kombinieren. Wir können nicht vorhersagen, ob der Nutzer eine Datei in einen anderen Ordner innerhalb des Box.net-Kontos verschieben oder auf seinen Desktop ziehen möchte. Diese beiden Aktionen sind völlig unterschiedlich. Außerdem gibt es keine einfache Möglichkeit, zu erkennen, ob sich der Cursor außerhalb des Browserfensters befindet. Sie können window.onmouseout (IE) und document.onmouseout (andere Browser) verwenden, um dem Dokument das mouseout-Ereignis hinzuzufügen und zu prüfen, ob e.relatedTarget.nodeName == "HTML" (e ist das mouseout-Ereignis oder window.event, je nachdem, was verfügbar ist). Das ist aufgrund von Ereignis-Bubbling jedoch ziemlich schwierig. Das Ereignis wird möglicherweise zufällig ausgelöst, wenn Sie sich auf einem Bild oder einer Ebene befinden, insbesondere in einer komplexen Webanwendung wie Box.net.

2) Wir möchten, dass der Nutzer etwas Bestimmtes tun muss, um zu verhindern, dass er versehentlich etwas auf den Desktop zieht. Ein Mitbearbeiter eines Box-Ordners kann eine ausführbare Datei hochladen, die auf dem Computer desjenigen, der sie herunterlädt, unerwünschte Aktionen ausführt. Wir möchten, dass Nutzer genau wissen, wann eine Datei auf den Computer heruntergeladen wird.

Ausführung 2

Wir haben uns entschieden, mit der Tastenkombination „Strg + Ziehen“ zu experimentieren (eine Datei bei gedrückter Windows-Strg-Taste ziehen). Diese Aktion entspricht dem, was Nutzer auf einem Windows-Computer tun können, um eine Datei zu duplizieren. Außerdem ist zusätzliche Arbeit (aber kein zusätzlicher Schritt) erforderlich, um zu verhindern, dass Dateien versehentlich heruntergeladen werden.

Das jQuery-Plug-in in Iteration 1 wird nicht mehr verwendet, da wir die Download-Funktion für die Daten heruntergeladener Inhalte eng mit der On-Page-Funktion für die Daten heruntergeladener Inhalte verknüpfen müssen. Für Interessierte: Wir verwenden eine modifizierte Version des Draggable-Plug-ins von jQuery UI. Fügen Sie in das mousedown-Ereignis eines Zielelements den folgenden Code ein:

// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
    that[0].addEventListener("dragstart",function(e) {
        // e.dataTransfer in Firefox uses the DataTransfer constructor
        // instead of Clipboard
        // make sure it's Chrome and not Safari (both webkit-based).
        // setData on DownloadURL returns true on Chrome, and false on Safari
        if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
            e.dataTransfer.setData('DownloadURL','http://www.box.net')) {
        var url = (this.dataset && this.dataset.downloadurl) ||
                    this.getAttribute("data-downloadurl");
        e.dataTransfer.setData("DownloadURL", url);
        }
    }, false);
    return;
}
}

Neben der Aktivierung der Strg-Taste haben wir auch eine kleine Toaster-Tooltip-Funktion hinzugefügt, die angezeigt wird, wenn der Nutzer einen normalen Drag-and-drop-Vorgang auf der Seite ausführt. Der Nutzer wird darüber informiert, dass Dateien heruntergeladen werden können, wenn das Dateisymbol bei gedrückter Strg-Taste auf den Desktop gezogen wird.

Probleme bei Iteration 2

Aus Sicherheitsgründen stellt Box.net keine permanenten URLs für den direkten Zugriff auf statische Dateien bereit. Das ist nicht nur bei Box.net der Fall. Kein Onlinespeicherdienst sollte permanente URLs ohne eine zusätzliche Sicherheitsebene freigeben, um zu prüfen, ob die Datei öffentlich ist und ob der beabsichtigte Download von einem Nutzer mit entsprechenden Berechtigungen angefordert wird.

Wenn die „Download-URL“ (z.B. https://www.box.net/box_download_file?file_id=f_60466690) eines Elements aufgerufen wird, wird der Statuscode „302 Gefunden“ zurückgegeben und eine Weiterleitung zu einer zufälligen URL (z.B. https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b) erfolgt, die die temporäre „tatsächliche URL“ der Datei ist. Das Problem ist, dass er alle paar Minuten abläuft. Daher ist es nicht sinnvoll, ihn in die HTML-Ausgabe einzufügen. Es kann „404“ zurückgeben, wenn der Nutzer versucht, die Datei über den Link in der vor einigen Minuten generierten HTML-Ausgabe herunterzuladen.

Der DnD-Download funktioniert nur mit echten URLs, die direkt auf eine Ressource verweisen. Bei einer Weiterleitung ist der Algorithmus derzeit nicht intelligent genug, um der Kette zu folgen. Aus Sicherheitsgründen sollte er das auch gar nicht tun. Daher können Sie die Datei zwar über den Link https://www.box.net/box_download_file?file_id=f_60466690 herunterladen, wenn Sie ihn in die Browser-Adressleiste eingeben, er funktioniert aber nicht mit Drag-and-drop.

Die folgenden Screenshots veranschaulichen die Unterschiede zwischen einer „tatsächlichen URL“ und einer „Weiterleitungs-URL“:

302-Weiterleitungs-URL
302-Weiterleitungs-URL
Tatsächliche URL
Tatsächliche URL

Iteration 3

Probieren wir Ajax aus.

Wir haben den Code aus der vorherigen Iteration leicht geändert und sind zu folgendem Code gekommen:

// DnD to desktop when the Ctrl key is pressed while dragging
if (e.ctrlKey) {
var that = $(e.target);
// make sure it is not IE (attachEvent).
if (that[0].addEventListener) {
that[0].addEventListener("dragstart", function(e) {
    // e.dataTransfer in Firefox uses the DataTransfer constructor
    // instead of Clipboard
    // make sure it's Chrome and not Safari (both webkit-based).
    // setData on DownloadURL returns true on Chrome, and false on Safari
    if (e.dataTransfer && e.dataTransfer.constructor == Clipboard &&
        e.dataTransfer.setData('DownloadURL', 'http://www.box.net')) {
    var url = (this.dataset && this.dataset.downloadurl) ||
                this.getAttribute("data-downloadurl");
    $.ajax({
        complete: function(data) {
        e.dataTransfer.setData("DownloadURL", data.responseText);
        },
        type:'GET',
        url: url
    });
    }
}, false);
return;
}
}

Das ergibt Sinn. Beim Ziehen wird sofort ein Ajax-Aufruf an den Server gesendet, um die aktuelle Download-URL der Datei abzurufen. Das funktioniert jedoch nicht.

Es stellt sich heraus, dass es sich um einen synchronen Aufruf handeln muss (oder wie ich es gerne nenne, um einen Sjax-Aufruf). Anscheinend muss setData zum Zeitpunkt der Verknüpfung des Ereignis-Listeners erfolgen. Gemäß der jQuery-API werden die hervorgehobenen Zeilen so:

$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
type: 'GET',
url: url
});

Das funktioniert einwandfrei, bis ich die Netzwerkverbindung trenne. Da es sich um einen synchronen Aufruf handelt, hängt der Browser, bis der Aufruf erfolgreich war. Wenn der Ajax-Aufruf fehlschlägt (404 oder keine Antwort), wird der Browser nicht aufgetaut, als wäre er abgestürzt.

Es ist viel sicherer, so vorzugehen:

$.ajax({
async: false,
complete: function(data) {
e.dataTransfer.setData("DownloadURL", data.responseText);
},
error: function(xhr) {
if (xhr.status == 404) {
    xhr.abort();
}
},
type: 'GET',
timeout: 3000,
url: url
});

Wenn Sie sich eine Demo dieser Funktion ansehen möchten, laden Sie einfach eine statische Datei in ein Box.net-Konto hoch. Ziehen Sie das Dateisymbol bei gedrückter Strg-Taste auf den Desktop. Wenn Sie noch kein Konto haben, dauert es weniger als 30 Sekunden, eines zu erstellen.

Mit dieser Funktion können Sie kreativ sein und viele Dinge möglich machen. Wenn Sie ein Bild in ein Windows-Druckerdialogfeld ziehen, wird es sofort gedruckt. Sie können einen Song aus Box auf das Laufwerk Ihres Smartphones kopieren oder eine Datei aus Box per Drag-and-drop in Ihren IM-Client ziehen, um sie direkt an einen Freund zu senden. Die Möglichkeiten, Ihre Produktivität zu steigern, sind endlos.

Dateien per Drag-and-drop an den Drucker ziehen
Datei zum Drucker ziehen
Dateien per Drag-and-drop in den IM-Client ziehen
Datei in den IM-Client ziehen

Gedanken und zukünftige Verbesserungen

Das ist jedoch immer noch nicht ideal, da ein synchroner Aufruf den Browser für einen kurzen Moment blockieren kann. Der HTML 5-Webworker ist auch keine Lösung, da ein Webworker asynchron sein muss. Anscheinend muss setData ausgeführt werden, wenn der Ereignis-Listener angehängt wird.

In der Realität ist die Leistung ziemlich akzeptabel. Der synchrone Ajax-Aufruf (Sjax) ruft lediglich einen URL-String ab, was ziemlich schnell gehen sollte. Allerdings ist der Overhead im HTTP-Header sehr hoch, was mit WebSockets möglicherweise behoben werden kann. Bis diese Art von Technologie jedoch häufiger verwendet wird, lohnt es sich nicht, WebSockets zu verwenden, um jede Kleinigkeit an den Client zu senden.

Ich hoffe auch, dass die API in Zukunft die Möglichkeit zum Herunterladen mehrerer Dateien bietet. In Kombination mit benutzerdefinierten Kästchen zur Auswahl mehrerer Dateien auf der Benutzeroberfläche wäre das unglaublich. Außerdem wäre es noch besser, wenn clientseitig erstellte Dateien, z. B. Textdateien, die aus dem Ergebnis eines eingereichten Formulars generiert wurden, auf diese Weise heruntergeladen werden könnten.

  • Spalten-Drag-and-drop
  • Liste neu anordnen
  • Bildgalerie erstellen
  • Canvas-Bild exportieren

Verweise