Studium przypadku – Pobieranie w Chrome przez przeciąganie i upuszczanie

Wprowadzenie

Przeciąganie i upuszczanie to jedna z wielu wspaniałych funkcji HTML 5. Jest obsługiwana w Firefox 3.5, Safari, Chrome i IE. Niedawno wprowadziliśmy nową funkcję, która umożliwia użytkownikom Google Chrome przeciąganie i upuszczanie plików z przeglądarki na pulpit. Jest to bardzo przydatna funkcja, ale nie była powszechnie znana, dopóki Ryan Seddon nie opublikował artykułu o odwrotnym inżynierii tej nowej funkcji.

W firmie Box.net bardzo się cieszymy, że dzięki nowym możliwościom możemy ulepszać nasze rozwiązanie do zarządzania treściami w chmurze i więcej robić dla społeczności deweloperów. Z przyjemnością informujemy, że usługa DnD Download została zintegrowana z naszą usługą. Użytkownicy Boxa mogą teraz przeciągać pliki bezpośrednio z przeglądarki Chrome na pulpit, aby je pobrać i zapisać.

Chcę opowiedzieć, jak podczas tworzenia tej nowej funkcji przechodziłem przez kilka iteracji.

Sprawdzanie obsługi interfejsu API w przypadku funkcji Przeciągnij i upuść

Najpierw sprawdź, czy Twoja przeglądarka w pełni obsługuje przeciąganie i upuszczanie w HTML5. Najprostszym sposobem jest użycie biblioteki Modernizr do sprawdzenia określonej funkcji:

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

Iteracja 1

Najpierw spróbowałem podejścia, które Seddon znalazł w Gmailu. Do linków kotwiczych plików dodaliśmy nowy atrybut o nazwie „data-downloadurl”. Ten proces wykorzystuje niestandardowe atrybuty danych w HTML5. W parametrze data-downloadurl musisz podać typ MIME pliku, nazwę pliku docelowego (pożądaną nazwę pobranego pliku) i adres URL pliku do pobrania. W tym celu dodaj do szablonu HTML ten kod:

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

które wygeneruje dane wyjściowe podobne do tych:

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

Na podstawie plugin stworzonej przez von Schorscha, która opiera się na artykule Seddona, dodałem wtyczkę jQuery, która wykonuje wykrywanie funkcji przeglądarki. Wyróżnione są wiersze, które dodałem do wersji von Schorscha:

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

Zrobiłem to, ponieważ bez wcześniejszego wykrywania przeglądarki wywołanie addEventListener() do elementu HTML w IE spowoduje błąd JavaScriptu, ponieważ IE używa własnej metody attachEvent(). Zmienne e.dataTransfer jest nie zdefiniowane w IE (obecnie), e.dataTransfer.constructor zwraca DataTransfer w Firefoxie (Mozilla), a przeglądarki Webkit (Chrome i Safari) implementują konstruktor Clipboard. W Safari e.dataTransfer.setData('DownloadURL','http://www.box.net') zwraca wartość false, a Chrome zwraca true w przypadku tego wyrażenia. Po wykonaniu wszystkich wymienionych wyżej testów funkcja będzie dostępna tylko w Chrome. Możesz powiedzieć, że wystarczy:

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

Wolę wykrywanie funkcji niż wykrywanie przeglądarki, ale technicznie nie wykryje ono, że pobieranie w trybie Nie przeszkadzać będzie działać.

Problemy w wersji 1

1) Ponieważ obecnie mamy włączone przenoszenie/kopiowanie plików między folderami w trybie DnD na stronie, potrzebujemy sposobu na rozróżnienie pobierania w trybie DnD i przenoszenia w trybie DnD na stronie. Technicznie nie możemy połączyć tych dwóch działań. Nie możemy przewidzieć, czy użytkownik chce przenieść plik do innego folderu na koncie Box.net, czy też przeciągnąć go na pulpit. Te 2 działania są zupełnie inne. Poza tym nie ma łatwego sposobu na wykrycie, czy kursor znajduje się poza oknem przeglądarki. Możesz użyć window.onmouseout (IE) i document.onmouseout (inne przeglądarki), aby dołączyć do dokumentu zdarzenie mouseout i sprawdzić, czy e.relatedTarget.nodeName == "HTML" (e to zdarzenie mouseout lub window.event, w zależności od tego, które jest dostępne). Jest to jednak dość trudne ze względu na przenoszenie zdarzeń. Zdarzenie może być wywoływane losowo, gdy najedziesz kursorem na obraz lub warstwę, zwłaszcza w kompleksowej aplikacji internetowej, takiej jak Box.net.

2) chcemy, aby użytkownik musiał coś zrobić, aby nie mógł przez przypadek przeciągnąć czegoś na pulpit. Edytorzy folderu Box mogą przesłać plik wykonywalny, który powoduje niechciane działanie na komputerze osoby, która go pobierze. Chcemy, aby użytkownik wiedział dokładnie, kiedy plik zostanie pobrany na komputer.

Iteracja 2

Postanowiliśmy przetestować kombinację klawiszy Ctrl + przeciągnij (przeciąganie pliku przy wciśniętym klawiszu Ctrl w Windows). Ta czynność jest zgodna z tym, co użytkownicy mogą zrobić na komputerze z systemem Windows, aby zduplikować plik. Wymaga też od użytkownika dodatkowej pracy (ale nie dodatkowego kroku), aby zapobiec przypadkowemu pobieraniu plików.

W wersji 1 zrezygnowaliśmy z wtyczki jQuery, ponieważ musimy ściśle zintegrować DnD Download z wtyczką DnD na stronie. Dla zainteresowanych: używamy zmodyfikowanej wersji wtyczki Draggable z biblioteki jQuery UI. W zdarzeniu mousedown elementu docelowego umieszczamy ten kod:

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

Oprócz włączenia klawisza Ctrl dodaliśmy też małą podpowiedź, która pojawia się, gdy użytkownik przeciąga element na stronie. Informuje on użytkownika, że pliki można pobierać, przeciągając ikonę pliku na pulpit, gdy przytrzymana jest klawisz Ctrl.

Problemy w wersji 2

Ze względów bezpieczeństwa Box.net nie udostępnia stałych adresów URL do bezpośredniego dostępu do plików statycznych. Nie dotyczy to tylko Box.net. Żadna usługa przechowywania danych online nie powinna udostępniać stałych adresów URL bez dodatkowej warstwy zabezpieczeń, która sprawdza, czy plik jest publiczny i czy użytkownik, który poprosił o pobranie, ma odpowiednie uprawnienia.

Gdy klikniesz „adres URL do pobrania” (np. https://www.box.net/box_download_file?file_id=f_60466690) elementu, zwróci on kod stanu „302 Found” i przekieruje Cię do losowego adresu URL (np. https://www.box.net/dl/6045?a=1f1207a084&m=168299,11211&t=2&b=aca15820d924e3b), który jest tymczasowym „rzeczywistym adresem URL” pliku. Problem polega na tym, że klucz wygasa co kilka minut, więc umieszczanie go w wyjściowym kodzie HTML jest niepraktyczne. Gdy użytkownik spróbuje pobrać plik z linku w pliku wyjściowym HTML wygenerowanym kilka minut temu, może się wyświetlić błąd „404”.

DnD Download działa tylko w przypadku rzeczywistych adresów URL, które wskazują bezpośrednio na zasób. Jeśli występuje przekierowanie, nie jest ono wystarczająco inteligentne, aby podążać za łańcuchem (i z powodów bezpieczeństwa nigdy nie powinno podążać za łańcuchem). Dlatego, chociaż link https://www.box.net/box_download_file?file_id=f_60466690 z powyższego przykładu umożliwia pobranie pliku po wpisaniu go na pasku adresu przeglądarki, nie zadziała w przypadku DnD.

Aby lepiej zobrazować różnice między „rzeczywistym adresem URL” a „adresem URL przekierowania”, zapoznaj się z tymi zrzutami ekranu:

Adres URL przekierowania 302
Adres URL przekierowania 302
Rzeczywisty URL
Prawdziwy URL

Iteracja 3

Spróbujmy Ajaxa.

Zmieniliśmy nieco kod z poprzedniej iteracji i uzyskaliśmy następujący wynik:

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

To ma sens. Po rozpoczęciu przeciągania natychmiast wysyła ono do serwera wywołanie Ajax, aby pobrać najnowszy adres URL pliku do pobrania. Nie działa jednak.

Okazuje się, że musi to być wywołanie synchroniczne (lub jak ja to nazywam, Sjax). Wygląda na to, że metoda setData musi być wywołana w momencie, gdy jest dołączony detektor zdarzeń. Zgodnie z interfejsem jQuery API wyróżnione wiersze wyglądają tak:

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

Wszystko działało dobrze, dopóki nie odłączyłem połączenia z siecią. Ponieważ jest to wywołanie synchroniczne, przeglądarka zamarza, dopóki wywołanie nie zostanie wykonane. Jeśli wywołanie Ajaxa zakończy się niepowodzeniem (404 lub brak odpowiedzi), przeglądarka nie zostanie odmrożona, tak jakby uległa awarii.

Znacznie bezpieczniej jest wykonać te czynności:

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

Aby przetestować tę funkcję, prześlij plik statyczny na konto Box.net. Przeciągnij ikonę pliku na pulpit, przytrzymując klawisz Ctrl. Jeśli nie masz konta, jego utworzenie zajmie Ci mniej niż 30 sekund.

Dzięki tej funkcji możesz rozwijać swoją kreatywność i robić wiele różnych rzeczy. Przeciągnięcie obrazu do okna drukarki w Windows spowoduje jego natychmiastowe wydrukowanie. Możesz skopiować utwór z Boxa na dysk telefonu komórkowego, przeciągnąć plik z Boxa do klienta IM, aby przesłać go bezpośrednio do znajomego... To otwiera nieskończone możliwości zwiększenia produktywności.

przeciąganie pliku do drukarki,
Przeciąganie pliku do drukarki.
Przeciąganie pliku do klienta czatu
Przeciąganie pliku do klienta IM.

Uwagi i planowane ulepszenia

To wciąż nie jest idealne, ponieważ wywołanie synchroniczne może na chwilę zablokować przeglądarkę. Nie pomaga też Web Worker w HTML 5, ponieważ musi on działać asynchronicznie. Wygląda na to, że metoda setData musi być wykonywana w momencie, gdy detektor zdarzeń jest dołączony.

W rzeczywistości wydajność jest całkiem zadowalająca. Wywołanie synchronicznego Ajaxa (Sjax) po prostu pobiera ciąg znaków adresu URL, co powinno być dość szybkie. Ma to jednak duży wpływ na nagłówek HTTP, co można rozwiązać za pomocą WebSocket. Dopóki jednak nie zauważymy większego wykorzystania tej technologii, nie warto używać WebSockets do wysyłania do klienta każdej drobnej aktualizacji.

Mam też nadzieję, że w przyszłości do interfejsu API zostanie dodana możliwość pobierania wielu plików. W połączeniu z niestandardowymi polami wyboru, które umożliwiają wybór wielu plików w interfejsie użytkownika, byłoby to niesamowite. Byłoby też jeszcze lepiej, gdyby można było w ten sposób pobierać pliki utworzone przez klienta, na przykład pliki tekstowe wygenerowane na podstawie przesłanego formularza.

  • Tryb Nie przeszkadzać
  • Zmiana kolejności na liście
  • Tworzenie galerii obrazów
  • Eksportowanie obrazu na płótnie

Pliki referencyjne