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

Wstęp

Przeciąganie i upuszczanie (DnD) to jedna z wielu wspaniałych funkcji języka HTML5, obsługiwana przez przeglądarki Firefox 3.5, Safari, Chrome i IE. Niedawno firma Google wprowadziła nową funkcję, która pozwala użytkownikom Google Chrome przeciągać i upuszczać pliki z przeglądarki na pulpit. Jest to niezwykle przydatna funkcja, ale nie była powszechnie znana, dopóki Ryan Seddon nie opublikował artykułu na temat odkryć tej nowej funkcji za pomocą inżynierii wstecznej.

Jesteśmy zachwyceni tym, że te nowe funkcje pomagają nam ulepszać rozwiązanie do zarządzania treścią w chmurze, a także mieć większy wkład w rozwój społeczności programistów. Z przyjemnością informuję, że funkcja pobierania DnD została zintegrowana z naszą usługą. Użytkownicy Box mogą teraz pobierać i zapisywać pliki bezpośrednio z przeglądarki Chrome na komputer.

Chętnie opowiem, jak przeszedłem kilka iteracji w trakcie opracowywania nowej funkcji.

Sprawdzanie obsługi interfejsu API typu „przeciągnij i upuść”

Najpierw sprawdź, czy Twoja przeglądarka w pełni obsługuje przeciąganie i upuszczanie w HTML5. Możesz to zrobić w prosty sposób, korzystając z biblioteki o nazwie Modernizr, która pozwala sprawdzić konkretną funkcję:

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

Iteracja 1

Najpierw użyłem metody, którą Seddon wybrał w Gmailu. Dodałem nowy atrybut „data-downloadurl”, aby zakotwiczać linki do plików. W tym procesie używane są niestandardowe atrybuty danych HTML5. W polu data-downloadurl należy podać typ MIME pliku, nazwę pliku docelowego (pożądaną nazwę pobieranego pliku) i adres URL pobierania. W ten sposób do szablonu HTML zostaje dodana wartość:

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

co da wynik podobny do tego:

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

Utworzona na podstawie artykułu Seddona plugin jQuery utworzona przez von Schorscha, która nieco wykrywa funkcje przeglądarki. Wyróżnione są wiersze dodane przeze mnie 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);

Powodem jest to, że bez wcześniejszego wykrywania przeglądarki użycie funkcji addEventListener() w elemencie HTML w IE powoduje wystąpienie błędu JavaScript, ponieważ IE korzysta z własnej metody insertEvent(). W IE (obecnie) nie zdefiniowano metody e.dataTransfer, a funkcja e.dataTransfer.constructor zwraca w przeglądarce Firefox (Mozilla i Safari), natomiast przeglądarki Webkit (Chrome i Safari) implementują konstruktor Clipboard. W Safari funkcja e.dataTransfer.setData('DownloadURL','http://www.box.net') zwraca dla tej instrukcji wartość „false”, a Chrome zwraca wartość „true” (prawda). Po wykonaniu wszystkich wymienionych powyżej testów ta funkcja będzie dostępna tylko w Chrome. Pewnie uważam, że mogę po prostu zrobić to tak:

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

Jednak wolę wykrywanie funkcji niż wykrywanie przeglądarki, choć technicznie to nie wykrywa, czy pobieranie DnD będzie działać.

Problemy z iteracją 1

1) Ponieważ obecnie mamy włączoną funkcję DnD na stronie do przenoszenia/kopiowania plików między folderami, potrzebujemy sposobu na rozróżnienie DnD pobierania i DnD na stronie. Technicznie rzecz biorąc, 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 przeciągnąć go na komputer. Te 2 działania są całkowicie różne. Nie ma też łatwego sposobu na sprawdzenie, czy kursor znajduje się poza oknem przeglądarki. Możesz użyć parametrów window.onmouseout (IE) i document.onmouseout (w innych przeglądarkach), aby dołączyć do dokumentu zdarzenie wyjechania kursorem. Sprawdź też, czy e.relatedTarget.nodeName == "HTML" (e jest zdarzenie wyjechania kursorem lub window.event, zależnie od tego, które jest dostępne). Jest to jednak dosyć trudne ze względu na dymki wydarzeń. Zdarzenie może być uruchamiane losowo, gdy znajdujesz się na obrazie lub warstwie, zwłaszcza w złożonej aplikacji internetowej, takiej jak Box.net.

2) Warto, by użytkownik jawnie coś zrobił, co uniemożliwi mu przypadkowe przeciągnięcie czegoś na pulpit. Edytujący folder Box może przesłać plik wykonywalny, który wykonuje niepożądane działania na komputerze osoby pobierającej ten plik. Chcemy też, by użytkownik wiedział, kiedy plik zostanie pobrany na komputer.

Iteracja 2

Zdecydowaliśmy się poeksperymentować z wersją Ctrl + przeciąganie (przeciąganie pliku po naciśnięciu klawisza Ctrl w systemie Windows). Działa to tak samo jak na komputerze z systemem Windows, aby duplikować plik. Wymaga to też od użytkowników wykonania dodatkowych czynności, by zapobiec przypadkowemu pobraniu plików.

Wtyczka jQuery w iteracji 1 została porzucone, ponieważ musimy ściśle zintegrować funkcję pobierania DnD z DnD na stronie. Dla zainteresowanych osób korzystamy ze zmodyfikowanej wersji wtyczkiDraggable interfejsu jQuery. W zdarzeniu wskaźnika myszy w elemencie docelowym umieszczamy następujący 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łą etykietkę oznaczającą toster, która wyświetla się, gdy użytkownik wykonuje zwykłe przeciąganie strony na stronie. Informuje on użytkownika, że pliki można pobrać po przeciągnięciu ikony pliku na pulpit podczas przytrzymywania klawisza Ctrl.

Problemy z iteracją 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 wyłącznie Box.net. Żadna usługa przechowywania danych online nie powinna udostępniać stałych adresów URL bez dodatkowej warstwy zabezpieczeń w celu sprawdzenia, czy plik jest publiczny i czy zażądano pobrania pliku przez użytkownika z odpowiednimi uprawnieniami.

Po kliknięciu „adresu URL pobierania” (np. https://www.box.net/box_download_file?file_id=f_60466690) elementu zwracany jest kod stanu „302 znaleziono” i przekierowywanie użytkownika na losowy adres 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 wygasa on co kilka minut, więc umieszczenie go w danych wyjściowych HTML jest niepraktyczne. Gdy użytkownik spróbuje pobrać plik, korzystając z linku w wygenerowanym kilka minut temu, wyniku HTML, może zwrócić błąd 404.

Pobieranie DnD działa tylko w przypadku rzeczywistych adresów URL wskazujących bezpośrednio zasoby. Jeśli wykorzystywane jest przekierowanie, nie jest ono na tyle inteligentne, by śledzić łańcuch (a nigdy nie powinno przechodzić do łańcucha ze względów bezpieczeństwa). Dlatego chociaż powyższy link https://www.box.net/box_download_file?file_id=f_60466690 umożliwiałby pobranie pliku po wpisaniu go na pasku lokalizacji przeglądarki, nie będzie on działać w przypadku DnD.

Aby lepiej przedstawić różnice między „rzeczywistym adresem URL” a „adresem URL przekierowania”, zobacz zrzuty ekranu:

Przekierowanie 302
Przekierowanie 302
Rzeczywisty URL
Rzeczywisty adres URL

Iteracja 3

Wypróbujmy Ajax.

W poprzedniej iteracji nieco zmodyfikowaliśmy kod i opracowaliśmy następujące rozwiązanie:

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

Ma to sens. Po rozpoczęciu przeciągania natychmiast wywołuje ona serwer Ajax w celu pobrania najnowszego adresu URL pobierania pliku. Jednak to nie działa.

Okazuje się, że musi to być wywołanie synchroniczne (jak ja nazywam go Sjaxem). Wygląda na to, że parametr setData musi zostać wykonany, gdy podłączony jest detektor zdarzeń. Zgodnie z interfejsem API jQuery zaznaczone wiersze wyglądają tak:

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

Działa dobrze, dopóki nie odłączę połączenia sieciowego. Wykonuje ono wywołanie synchroniczne, więc przeglądarka blokuje się do czasu pomyślnego wywołania. Jeśli wywołanie Ajax nie powiedzie się (404 lub w ogóle nie odpowie), przeglądarka w ogóle nie odszyfruje, tak jak gdyby 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 zobaczyć, jak działa ta funkcja, prześlij statyczny plik na konto Box.net. Przeciągnij ikonę pliku na pulpit, przytrzymując klawisz Ctrl. Jeżeli nie masz konta, utworzenie go zajmie mniej niż 30 sekund.

Dzięki tej funkcji możesz wykazać się kreatywnością i stworzyć wiele możliwości. Przeciągnięcie obrazu do okna dialogowego drukarki w systemie Windows spowoduje natychmiastowe wydrukowanie go. Możesz skopiować piosenkę z Box na dysk swojego telefonu komórkowego, przeciągnąć plik z Box do klienta czatu, aby przesłać go bezpośrednio do znajomego... Otwiera to nieskończone możliwości zwiększania produktywności.

przeciąganie pliku do drukarki
Przeciągnij plik do drukarki.
Przeciąganie pliku do klienta czatu
Przeciągnij plik do klienta czatu.

Wnioski i przyszłe ulepszenia

Nie jest to jednak idealny wynik, ponieważ wywołanie synchroniczne może na chwilę zablokować przeglądarkę. Również zasób Web Worker HTML5 nie pomaga, ponieważ musi być asynchroniczny. Wygląda na to, że trzeba zrobić to w momencie, gdy podłączony jest detektor zdarzeń.

W rzeczywistości wyniki są dość akceptowalne. Synchroniczne wywołanie metody Ajax (Sjax) pobiera tylko ciąg adresu URL, co powinno być szybkie. Zawiera on duży nadmiar w nagłówku HTTP, który może zostać rozwiązany przez WebSockets. Jednak dopóki nie zauważymy większego wykorzystania tego rodzaju technologii, nie warto korzystać z WebSockets w celu wysyłania każdej aktualizacji do klienta.

Mam też nadzieję, że w przyszłości dodamy do API możliwość pobierania wielu plików. W połączeniu z niestandardowymi polami wyboru do wyboru wielu plików w interfejsie użytkownika jest to coś niesamowitego. Co więcej, jeszcze lepiej byłoby, gdyby można było w ten sposób pobrać pliki wygenerowane przez klienta, na przykład pliki tekstowe wygenerowane na podstawie przesłanego formularza.

  • Kolumna ndnd
  • Zmień kolejność na liście
  • Tworzenie galerii obrazów
  • Eksportowanie obrazu na płótnie

Źródła