Case study - Download tramite trascinamento in Chrome

David Tong
David Tong

Introduzione

Il trascinamento è una delle tante funzionalità di HTML 5 ed è supportato in Firefox 3.5, Safari, Chrome e IE. Di recente Google ha implementato una nuova funzionalità che consente agli utenti di Google Chrome di trascinare file dal browser al computer. Si tratta di una funzionalità estremamente comoda, ma non era molto nota finché Ryan Seddon non ha pubblicato un articolo sulle scoperte del suo reverse engineering su questa nuova funzionalità.

Noi di Box.net siamo molto entusiasti di come queste nuove funzionalità ci consentano di migliorare la nostra soluzione di gestione dei contenuti nel cloud e di dare un maggiore contributo alla community di sviluppatori. Sono felice di annunciare che DnD Download è stato integrato nel nostro prodotto. Ora gli utenti di Box possono trascinare i file direttamente da un browser Chrome al computer per scaricarli e salvarli.

Vorrei condividere come ho eseguito diverse iterazioni durante lo sviluppo di questa nuova funzionalità.

Verificare il supporto dell'API Drag and Drop

La prima cosa da fare è verificare che il browser supporti completamente il trascinamento HTML5. Un modo semplice per farlo è utilizzare una libreria chiamata Modernizr per verificare la presenza di una determinata funzionalità:

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

Iterazione 1

Ho provato per prima cosa l'approccio trovato da Seddon in Gmail. Ho aggiunto un nuovo attributo chiamato "data-downloadurl" per ancorare i link dei file. Questa procedura utilizza gli attributi dei dati personalizzati di HTML5. In data-downloadurl, devi includere il tipo MIME del file, il nome del file di destinazione (il nome del file scaricato che preferisci) e l'URL di download del file. Di conseguenza, questo viene aggiunto al modello HTML:

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

che genererà un output come il seguente:

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

In base a un plugin jQuery creato da von Schorsch, che si basa sull'articolo di Seddon, ho aggiunto un plug-in jQuery che esegue un po' di rilevamento delle funzionalità del browser. Le righe che ho aggiunto alla versione di von Schorsch sono evidenziate:

(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);

Ho fatto questo perché, senza il rilevamento del browser precedente, l'esecuzione di addEventListener() su un elemento HTML in IE crea un errore JavaScript perché IE utilizza il proprio metodo attachEvent(). e.dataTransfer non è definito in IE (al momento), e.dataTransfer.constructor restituisce DataTransfer in Firefox (Mozilla), mentre i browser Webkit (Chrome e Safari) implementano il costruttore Clipboard. In Safari, e.dataTransfer.setData('DownloadURL','http://www.box.net') restituisce false, mentre Chrome restituisce true per questa istruzione. Se esegui tutti i test sopra indicati, la funzionalità rimarrà disponibile solo per Chrome. Potresti obiettare che potrei semplicemente fare quanto segue:

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

Tuttavia, preferisco il rilevamento delle funzionalità al rilevamento del browser, anche se tecnicamente non rileva che il download di DnD funzionerà.

Problemi dell'iterazione 1

1) Poiché al momento abbiamo attivato il trascinamento della pagina per lo spostamento/la copia di file tra cartelle, abbiamo bisogno di un modo per distinguere il trascinamento della pagina per il download e il trascinamento della pagina. Tecnicamente, non possiamo combinare queste due azioni. Non possiamo prevedere se l'utente vuole spostare un file in un'altra cartella all'interno dell'account Box.net o trascinarlo sul desktop. Queste due azioni sono completamente diverse. Inoltre, non esiste un modo semplice per rilevare se il cursore si trova all'esterno della finestra del browser. Puoi utilizzare window.onmouseout (IE) e document.onmouseout (altri browser) per associare l'evento mouseout al documento e controllare se e.relatedTarget.nodeName == "HTML" (e è l'evento mouseout o window.event, a seconda di quale sia disponibile). Tuttavia, questo è piuttosto difficile a causa del bubbling degli eventi. L'evento potrebbe essere attivato in modo casuale quando passi sopra un'immagine o un livello, in particolare in un'app web complessa come Box.net.

2) Vogliamo che l'utente debba fare qualcosa in modo esplicito per impedirgli di trascinare qualcosa sul desktop per sbaglio. Potenzialmente, un editor di una cartella Box può caricare un file eseguibile che esegue un'azione indesiderata sul computer di chi lo scarica. Vogliamo che l'utente sappia esattamente quando un file verrà scaricato sul computer.

Iterazione 2

Abbiamo deciso di fare esperimenti con Ctrl + Trascina (trascinando un file con il tasto Ctrl di Windows premuto). Questa azione è coerente con ciò che gli utenti possono fare su un computer Windows per duplicare un file. Inoltre, richiede un lavoro aggiuntivo (ma non un passaggio aggiuntivo) da parte dell'utente per impedire il download dei file per errore.

Il plug-in jQuery nell'iterazione 1 è stato abbandonato perché dobbiamo integrare strettamente il download DnD con il DnD in-page. Per chi fosse interessato, utilizziamo una versione modificata del plug-in Draggable di jQuery UI. All'interno dell'evento mousedown di un elemento target, inseriamo il seguente codice:

// 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;
}
}

Oltre ad attivare il tasto Ctrl, abbiamo aggiunto anche una piccola descrizione comando, che viene visualizzata quando l'utente esegue un normale trascinamento nella pagina. Comunica all'utente che i file possono essere scaricati se l'icona del file viene trascinata sul desktop mantenendo premuto il tasto Ctrl.

Problemi dell'iterazione 2

Per motivi di sicurezza, Box.net non espone URL permanenti per accedere direttamente ai file statici. Questo non è un problema esclusivo di Box.net. Qualsiasi servizio di archiviazione online non deve esporre URL permanenti senza un ulteriore livello di sicurezza per verificare se il file è pubblico e se il download previsto è richiesto da un utente con le autorizzazioni appropriate.

Quando si segue l'"URL di download" (ad es. https://www.box.net/box_download_file?file_id=f_60466690) di un elemento, viene restituito un codice di stato "302 Found" e viene eseguito il reindirizzamento a un URL random (ad es. https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b) che è l'"URL effettivo" temporaneo del file. Il problema è che scade ogni pochi minuti, quindi non è pratico inserirlo nell'output HTML. Potrebbe restituire "404" quando l'utente tenta di scaricare il file tramite il link nell'output HTML generato diversi minuti fa.

Il download tramite DnD funziona solo con URL effettivi che rimandano direttamente a una risorsa. Se è coinvolto il reindirizzamento, al momento non è abbastanza intelligente da seguire la catena (e non dovrebbe mai farlo per motivi di sicurezza). Pertanto, anche se il link https://www.box.net/box_download_file?file_id=f_60466690 riportato sopra ti consente di scaricare il file quando lo inserisci nella barra degli indirizzi del browser, non funzionerebbe con il trascinamento.

Per illustrare meglio le differenze tra un "URL effettivo" e un "URL di reindirizzamento", guarda gli screenshot:

URL di reindirizzamento 302
URL di reindirizzamento 302
URL effettivo
URL effettivo

Iterazione 3

Proviamo con Ajax.

Abbiamo modificato leggermente il codice nell'iterazione precedente e abbiamo ottenuto quanto segue:

// 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;
}
}

Ha senso. Al momento del trascinamento, viene eseguita immediatamente una chiamata Ajax al server per recuperare l'URL di download più recente del file. Tuttavia, non funziona.

A quanto pare deve essere una chiamata sincrona (o, come mi piace chiamarla, Sjax). Sembra che setData debba essere eseguito quando l'ascoltatore di eventi è collegato. In base all'API di jQuery, le righe evidenziate diventano:

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

Funziona bene finché non scollego la connessione di rete. Poiché esegue una chiamata sincrona, il browser si blocca finché la chiamata non va a buon fine. Se la chiamata Ajax non va a buon fine (404 o se non risponde), il browser non si sblocca come se avesse avuto un arresto anomalo.

È molto più sicuro fare qualcosa di simile al seguente:

$.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
});

Per una demo di questa funzionalità, non esitare a caricare un file statico in un account Box.net. Trascina l'icona del file sul desktop tenendo premuto il tasto Ctrl. Se non hai un account, bastano meno di 30 secondi per crearne uno.

Con questa funzionalità puoi dare sfogo alla tua creatività e rendere possibili molte cose. Se trascini un'immagine in una finestra di dialogo della stampante di Windows, l'immagine verrà stampata immediatamente. Puoi copiare un brano da Box alla memoria del tuo smartphone, trascinare un file da Box al tuo client di messaggistica istantanea per trasferirlo direttamente a un amico e così via. Ti si aprono infinite possibilità per aumentare la tua produttività.

Stampa di un file sulla stampante
Trascinamento di un file nella stampante.
Trascinamento di un file nel client di messaggistica istantanea
Trascinamento di un file nel client di messaggistica istantanea.

Opinioni e miglioramenti futuri

Questo non è ancora ottimale, poiché una chiamata sincrona potrebbe bloccare il browser per un breve istante. Anche il web worker HTML 5 non è utile, in quanto deve essere asincrono. Sembra che setData debba essere eseguito al momento in cui viene collegato l'ascoltatore di eventi.

In realtà, il rendimento è abbastanza accettabile. La chiamata Ajax (Sjax) sincrona recupera semplicemente una stringa URL, che dovrebbe essere abbastanza veloce. Tuttavia, comporta un elevato overhead nell'intestazione HTTP, che potrebbe essere risolto da WebSocket. Tuttavia, finché non vedremo un maggiore utilizzo di questo tipo di tecnologia, non vale la pena utilizzare WebSocket per inviare ogni piccolo aggiornamento al client.

Mi auguro inoltre che la possibilità di scaricare più file venga aggiunta all'API in futuro. Se combinato con caselle di controllo personalizzate per selezionare più file nell'interfaccia dell'utente, sarebbe fantastico. Inoltre, sarebbe ancora più bello se i file generati dal cliente, come i file di testo generati dal risultato di un modulo inviato, potessero essere scaricati in questo modo.

  • Colonna dnd
  • Riordinare l'elenco
  • Creare una galleria di immagini
  • Esportazione di un'immagine su tela

Riferimenti