Case study - Download tramite trascinamento in Chrome

David Tong
David Tong

Introduzione

Il trascinamento (DnD) è una delle tante fantastiche 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 desktop. Si tratta di una funzionalità estremamente utile, ma non è stata ampiamente nota fino a quando Ryan Seddon non ha pubblicato un articolo sulle scoperte della sua decompilazione su questa nuova funzionalità.

Noi di Box.net siamo entusiasti di come queste nuove funzionalità ci permettono di migliorare la nostra soluzione di gestione dei contenuti cloud, oltre a contribuire maggiormente alla community degli sviluppatori. Sono felice di annunciare che il download DND è stato integrato nel nostro prodotto. Ora gli utenti di Box possono trascinare i file direttamente da un browser Chrome al desktop per scaricare e salvare il file.

Vorrei spiegarmi come ho affrontato diverse iterazioni durante lo sviluppo di questa nuova funzionalità.

Verifica il supporto dell'API Drag and Drop

La prima cosa da fare è verificare che il tuo browser supporti completamente il trascinamento di HTML5. Per farlo, prova a 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 l'approccio che Seddon ha trovato in Gmail. Ho aggiunto un nuovo attributo chiamato "data-downloadurl" ai link di ancoraggio dei file. Questa procedura utilizza gli attributi di dati personalizzati di HTML5. In data-downloadurl, devi includere il tipo MIME del file, il nome del file di destinazione (il nome desiderato del file scaricato) e l'URL di download del file. Così, viene aggiunto al modello HTML:

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

il che creerà un output simile al 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 e basato sull'articolo di Seddon, ho aggiunto un plug-in jQuery che esegue un qualche rilevamento delle funzionalità del browser. Sono evidenziate le righe che ho aggiunto alla versione di von Schorsch:

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

Il motivo per cui l'ho fatto è che, senza un precedente rilevamento del browser, l'utilizzo di salva() su un elemento HTML in IE creerà un errore JavaScript, perché IE utilizza il proprio metodoattachEvent(). 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 degli appunti. In Safari, e.dataTransfer.setData('DownloadURL','http://www.box.net') restituisce false e Chrome restituisce true per questa istruzione. Se effettui tutti i test menzionati sopra, la funzionalità rimarrà disponibile solo per Chrome. Potresti sostenere che potrei semplicemente fare quanto segue:

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

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

Problemi con l'iterazione 1

1) Poiché al momento il file DND nella pagina è abilitato per lo spostamento/la copia di file da una cartella all'altra, abbiamo bisogno di un modo per distinguere il download DND e il file DND sulla 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 collegare l'evento di mouseout al documento e controllare se e.relatedTarget.nodeName == "HTML" (ovvero l'evento mouseout o window.event, a seconda di quale delle due opzioni è disponibile). Ma questo è abbastanza difficile a causa della bollitura degli eventi. L'evento può essere attivato in modo casuale quando ti trovi sopra un'immagine o un livello, soprattutto in un'app web complessa come Box.net.

2) Vogliamo che l'utente faccia esplicitamente qualcosa per impedirgli di trascinare per sbaglio qualcosa sul desktop. Potenzialmente, un editor di una cartella Box può caricare un file eseguibile che esegue un'azione indesiderata sul computer di chiunque lo scarichi. Vogliamo che l'utente sappia esattamente quando un file può essere scaricato sul desktop.

Iterazione 2

Abbiamo deciso di fare una prova con il tasto Ctrl + trascinamento (trascinando un file quando viene premuto il tasto Ctrl di Windows). Questa azione è coerente con le azioni che gli utenti possono eseguire su un desktop Windows per duplicare un file. Richiede inoltre un lavoro aggiuntivo (ma non un passaggio aggiuntivo) da parte dell'utente per impedire il download erroneo dei file.

Il plug-in jQuery nell'iterazione 1 viene abbandonato perché dobbiamo integrare strettamente il download DnD con il file DnD sulla pagina. Se ti interessa, utilizziamo una versione modificata del plug-in trascinabile dell'interfaccia utente di jQuery. All'interno dell'evento mousedown di un elemento target, inseriamo il codice seguente:

// 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 per il tostapane, che viene visualizzata quando l'utente esegue un normale trascinamento sulla pagina. Comunica all'utente che è possibile scaricare i file se l'icona del file viene trascinata sul desktop mentre si tiene premuto il tasto Ctrl.

Problemi con l'iterazione 2

Per motivi di sicurezza, Box.net non espone gli URL permanenti per accedere direttamente ai file statici. Non è esclusiva 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 segue l'"URL di download" (ad esempio https://www.box.net/box_download_file?file_id=f_60466690) di un elemento, viene restituito un codice di stato "302 trovato" e viene reindirizzato a un URL casuale (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 prova a scaricare il file al link nell'output HTML generato diversi minuti prima.

Il download DND funziona solo su URL effettivi che rimandano direttamente a una risorsa. Se prevede il reindirizzamento, al momento non è abbastanza intelligente da seguire la catena (e non dovrebbe mai seguire la catena 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 funziona con DnD.

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 Ajax.

Abbiamo leggermente modificato 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;
}
}

Questo ha senso. Dopo il trascinamento, il file effettua immediatamente una chiamata Ajax al server per recuperare l'URL di download più recente del file. ma non funziona.

Ho scoperto che deve essere una chiamata sincrona (o, come lo chiamo io, Sjax). Sembra che setData debba essere eseguito nel momento in cui è collegato il listener di eventi. Secondo l'API di jQuery, le righe evidenziate diventano:

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

Tutto funziona fino a quando non ho scollegato la connessione di rete. Poiché esegue una chiamata sincrona, il browser si blocca fino a quando la chiamata non va a buon fine. Se la chiamata Ajax non va a buon fine (404 o se non risponde del tutto), il browser non si sbrina affatto come se si fosse arrestato in modo anomalo.

È molto più sicuro eseguire azioni come le seguenti:

$.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 funzione, 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, crearne uno richiede letteralmente meno di 30 secondi.

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 Windows, l'immagine verrà stampata immediatamente. Puoi copiare un brano da Box nell'unità del tuo cellulare, trascinare un file da Box al tuo client di messaggistica immediata per trasferirlo direttamente al tuo amico... In questo modo avrai a disposizione infinite possibilità per aumentare la tua produttività.

raggirare un file alla stampante
Trascina un file sulla stampante.
Trascinamento di un file nel client IM
Trascina un file sul client IM.

Considerazioni e miglioramenti futuri

Si tratta comunque di una situazione non ottimale, poiché una chiamata sincrona potrebbe bloccare il browser per un breve momento. Anche il web worker HTML 5 non aiuta, perché un web worker deve essere asincrono. Sembra che setData debba essere eseguito nel momento in cui è collegato il listener di eventi.

In realtà, il rendimento è abbastanza accettabile. La chiamata sincrona Ajax (Sjax) recupera semplicemente una stringa URL, che dovrebbe essere piuttosto veloce. Ha un grande overhead nell'intestazione HTTP, che può essere gestito da WebSocket. Tuttavia, finché non notiamo un maggiore utilizzo di questo tipo di tecnologia, non vale la pena usare WebSocket per inviare ogni piccolo aggiornamento al client.

Spero inoltre che in futuro venga aggiunta all'API la funzionalità di download di più file. Combinato con caselle di controllo personalizzate per selezionare più file nell'interfaccia utente, è incredibile. Inoltre, sarebbe ancora più utile se in questo modo venissero scaricati i file generati dal client, ad esempio i file di testo generati dal risultato dell'invio di un modulo.

  • Dnd colonna
  • Riordina elenco
  • Creazione di una galleria immagini
  • Esportazione di un'immagine canvas

Riferimenti