Fallstudie: Download per Drag-and-drop in Chrome

David Tong
David Tong

Einleitung

Drag & 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 jedoch erst bekannt war, als Ryan Seddon einen Artikel über die Entdeckungen seines Reverse Engineering zu dieser neuen Funktion veröffentlichte.

Wir bei Box.net freuen uns sehr darüber, wie wir mithilfe dieser neuen Funktionen unsere Content-Management-Lösung in der Cloud verbessern und noch mehr zur Entwickler-Community beitragen können. Ich freue mich, dir mitteilen zu können, dass DnD Download jetzt in unser Produkt integriert wurde. Jetzt können Box-Nutzer Dateien direkt aus einem Chrome-Browser auf ihren Desktop ziehen, um sie herunterzuladen und zu speichern.

Ich möchte Ihnen gerne erzählen, wie ich bei der Entwicklung dieser neuen Funktion mehrere Iterationen durchlaufen habe.

Unterstützung für Drag-and-drop API prüfen

Überprüfen Sie zunächst, ob Ihr Browser HTML5 Drag & Drop vollständig unterstützt. Eine einfache Möglichkeit besteht darin, eine Bibliothek namens Modernizr zu verwenden, um nach einer bestimmten Funktion zu suchen:

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

Ausführung 1

Zuerst habe ich die Methode ausprobiert, die Seddon in Gmail gefunden hat. ein neues Attribut namens 'data-downloadurl' hinzugefügt, um Links von Dateien zu verankern. Bei diesem Vorgang werden die benutzerdefinierten Datenattribute von HTML5 verwendet. In data-downloadurl müssen Sie den MIME-Typ der Datei, den Namen der Zieldatei (der gewünschte Dateiname der heruntergeladenen Datei) und die Download-URL der Datei angeben. Daher wird dies der HTML-Vorlage hinzugefügt:

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

Die Ausgabe würde 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 von Schorsch erstellten jQuery-plugin, das auf Seddons Artikel basiert, habe ich ein jQuery-Plug-in hinzugefügt, das eine Teilerkennung von Browserfunktionen ermöglicht. Hervorgehoben sind die Zeilen, die ich zu von Schorschs Version 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 entsteht, wenn in IE „addEventListener()“ zu einem HTML-Element in Internet Explorer hinzugefügt wird, weil IE seine eigene Methode „attachEvent()“ verwendet. e.dataTransfer ist in IE derzeit nicht definiert, e.dataTransfer.constructor gibt DataTransfer in Firefox (Mozilla) zurück, während Webkit-Browser (Chrome und Safari) den Clipboard-Konstruktor implementieren. In Safari gibt e.dataTransfer.setData('DownloadURL','http://www.box.net') „false“ und Chrome für diese Anweisung „true“ zurück. 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 die Funktionserkennung der Browsererkennung, auch wenn dies technisch nicht ergibt, dass DnD-Downloads funktionieren werden.

Probleme bei Iteration 1

1) Da DnD auf der Seite derzeit zum Verschieben/Kopieren von Dateien zwischen Ordnern aktiviert ist, brauchen wir eine Möglichkeit, DnD-Download von DnD 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 den Desktop ziehen möchte. Diese beiden Aktionen sind völlig unterschiedlich. Außerdem lässt sich nicht einfach feststellen, ob sich der Cursor außerhalb des Browserfensters befindet. Sie können „window.onmouseout“ (IE) und „document.onmouseout“ (andere Browser) verwenden, um ein Mouseout-Ereignis an das Dokument anzuhängen und zu prüfen, ob e.relatedTarget.nodeName == "HTML" (e ist das „mouseout“- oder „window.event“, je nachdem, was verfügbar ist). Dies ist jedoch aufgrund des Bubbble-Effekts recht schwierig. Das Ereignis kann zufällig ausgelöst werden, wenn Sie sich über einem Bild oder einer Ebene befinden, insbesondere in einer komplexen Web-App wie Box.net.

2) Wir möchten, dass der Nutzer explizit etwas tut, um zu verhindern, dass er versehentlich etwas auf den Desktop zieht. Unter Umständen kann ein Bearbeiter eines Box-Ordners eine ausführbare Datei hochladen, die auf dem Computer des Nutzers, der sie herunterlädt, etwas unerwünschtes tut. Nutzende sollen genau wissen, wann eine Datei auf den Desktop heruntergeladen wird.

Ausführung 2

Wir beschlossen, mit Steuerung+Ziehen (Ziehen einer Datei, wenn die Windows-Strg-Taste gedrückt wird) zu experimentieren. Dies entspricht der Vorgehensweise zum Duplizieren einer Datei auf einem Windows-Desktop. Außerdem erfordert der Nutzer zusätzlichen Aufwand (aber keinen zusätzlichen Schritt), um den versehentlichen Download von Dateien zu verhindern.

Das jQuery-Plug-in in Iteration 1 wurde verworfen, da DnD Download eng in das DnD auf der Seite integriert werden muss. Für Interessierte verwenden wir eine modifizierte Version des Draggable-Plug-ins von jQuery-UI. Wir fügen den folgenden Code in das Mousedown-Ereignis eines Zielelements 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 kurze Kurzinfo hinzugefügt, die angezeigt wird, wenn Nutzer regelmäßig auf einer Seite ziehen. Sie teilt dem Nutzer mit, dass Dateien heruntergeladen werden können, wenn das Dateisymbol auf den Desktop gezogen wird, während die Strg-Taste gedrückt wird.

Probleme bei Iteration 2

Aus Sicherheitsgründen legt Box.net keine dauerhaften URLs für den direkten Zugriff auf statische Dateien offen. Dies gilt nicht nur für Box.net. Online-Speicherdienste sollten keine dauerhaften URLs ohne eine zusätzliche Sicherheitsebene offenlegen, um zu prüfen, ob die Datei öffentlich ist und ob der beabsichtigte Download von einem Nutzer mit den entsprechenden Berechtigungen angefordert wird.

Wird die Download-URL (z.B. https://www.box.net/box_download_file?file_id=f_60466690) eines Elements aufgerufen, wird der Statuscode "302 Found" zurückgegeben und der Nutzer an eine zufällige URL (z.B. https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b) weitergeleitet, die die temporäre "tatsächliche URL" der Datei ist. Die Herausforderung besteht darin, dass sie alle paar Minuten abläuft. Daher ist es nicht praktikabel, sie in die HTML-Ausgabe einzufügen. Sie könnte „404“ zurückgeben, wenn der Nutzer versucht, die Datei über den Link in der vor einigen Minuten generierten HTML-Ausgabe herunterzuladen.

„DnD-Download“ funktioniert nur bei tatsächlichen URLs, die direkt auf eine Ressource verweisen. Bei einer Umleitung ist diese derzeit nicht intelligent genug, um der Kette zu folgen (und aus Sicherheitsgründen sollte sie der Kette nie folgen). Obwohl der obige Link https://www.box.net/box_download_file?file_id=f_60466690 Sie die Datei herunterladen können, wenn Sie sie in die Adressleiste des Browsers eingeben, funktioniert DnD nicht.

Um die Unterschiede zwischen einer "tatsächlichen URL" und einer "Weiterleitungs-URL" besser zu veranschaulichen, sehen Sie sich die Screenshots an:

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

Ausführung 3

Versuchen wir es mit Ajax.

Wir haben den Code in der vorherigen Iteration geringfügig geändert und Folgendes erstellt:

// 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. Nach Dragstart wird sofort ein Ajax-Aufruf an den Server gesendet, um die neueste Download-URL der Datei abzurufen. Es funktioniert jedoch nicht.

Es stellt sich heraus, dass es ein synchroner Aufruf sein muss (oder, wie ich es gerne nenne, Sjax). Scheinbar muss setData beim Anhängen des Event-Listeners angewendet werden. Laut der API von jQuery werden die hervorgehobenen Zeilen so:

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

Und es funktioniert einwandfrei, bis ich die Netzwerkverbindung getrennt habe. Da der Aufruf synchron erfolgt, friert der Browser ein, bis der Aufruf erfolgreich ist. Wenn der Ajax-Aufruf fehlschlägt (404) oder nicht reagiert, wird der Browser überhaupt nicht aufgetaut, als wäre er abgestürzt.

Es ist viel sicherer, so etwas zu tun:

$.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 eine Demo dieser Funktion ansehen möchten, können Sie eine statische Datei in ein Box.net-Konto hochladen. Ziehen Sie das Dateisymbol mit gedrückter Strg-Taste auf Ihren Desktop. Wenn Sie kein Konto haben, dauert die Erstellung weniger als 30 Sekunden.

Mit dieser Funktion kannst du deiner Kreativität freien Lauf lassen und eine Menge Dinge erreichen. Wenn Sie ein Bild in ein Windows-Druckerdialogfeld ziehen, wird das Bild sofort gedruckt. Sie können einen Song von Box auf die Festplatte Ihres Mobiltelefons kopieren, eine Datei von Box auf Ihren IM-Client ziehen, um sie direkt an Ihren Freund zu übertragen... Dies eröffnet Ihnen endlose Möglichkeiten, Ihre Produktivität zu steigern.

Eine Datei auf dem Drucker senden
Datei auf den Drucker ziehen
Datei in den IM-Client ziehen
Datei in den IM-Client ziehen

Gedanken und zukünftige Verbesserungen

Dies ist immer noch nicht ideal, da ein synchroner Aufruf den Browser für einen kurzen Moment sperren könnte. Auch der HTML 5 Web Worker ist nicht hilfreich, da er asynchron sein muss. Anscheinend muss setData dann ausgeführt werden, wenn der Event-Listener angehängt wird.

In Wirklichkeit ist die Leistung jedoch ziemlich akzeptabel. Der synchrone Ajax-Aufruf (Sjax) ruft lediglich einen URL-String ab, was ziemlich schnell sein sollte. Der HTTP-Header ist mit einem großen Aufwand verbunden, der möglicherweise von WebSockets angegangen werden kann. Bis diese Art von Technologie jedoch häufiger genutzt wird, lohnt es sich nicht, mit WebSockets jedes kleine Update an den Client zu senden.

Ich hoffe außerdem, dass die API in Zukunft um die Möglichkeit zum Herunterladen mehrerer Dateien erweitert wird. In Kombination mit benutzerdefinierten Kästchen, mit denen mehrere Dateien auf der Benutzeroberfläche ausgewählt werden können, ist das eine tolle Sache. Noch schöner wäre es, wenn clientgenerierte Dateien, z. B. Textdateien, die aus dem Ergebnis eines gesendeten Formulars generiert wurden, auf diese Weise heruntergeladen werden können.

  • Spalte „dnd“
  • Liste neu anordnen
  • Bildergalerie erstellen
  • Leinwandbild exportieren

Verweise