eRIZ’s weblog

PHP, webdesign, Linux, Windows i inne, bo nie samym chlebem człowiek żyje
Serdecznie zapraszam do udziału w ANKIECIE

JavaScript, jQuery i Uploadify - odłamkowy!

Przyznam, że już dawno nie miałem takiej zagwozdki, jak ta, która spotkała mnie przez ostatnie 3 dni.

Zaczął mnie - delikatnie mówiąc - irytować fakt, iż na maila lecą czasem załączniki 40 MiB, więc postanowiłem napisać na potrzeby teamu małą aplikację, która miała:

  • zautoryzować użytkownika (to nie *share, że wszyscy mają mieć dostęp)
  • ułatwić wybór i upload
  • przyjąć pliki na serwerze i dać znać, komu trzeba

Coś podobnego już działa, jednak to wybitna prowizorka - najprostszy formularz i jedno pole. Ot, filozofia. Ale potrzebne jest coś, co działa trochę lepiej i nie zraża interfejsem. ;)

Teoria fajna?

nieco więcej założeń

Wszystko fajnie, drugie podejście, bo kiedyś nie było to aż tak potrzebne, więc sobie leżało gdzieś na poligonie, w oczekiwaniu na lepsze czasy. Aż do tego tygodnia, gdy udało mi się w ciągu 5 minut naskrobać szkic całej logiki (kartka papieru i długopis rządzą; trzeba sobie tylko pewne wytyczne stworzyć, żeby było łatwo i przyjemnie ;)). Ok, back-end nie jest problemem, schody zaczęły się, gdy przyszło do front-endu.

Pierwszym błędem, który zrobiłem po zaprojektowaniu pierwszych prototypów, było skupienie na IE. Ech, człowiek był młody i głupi (teraz tylko głupi ;)), teraz przyszła pora na skupienie się na właściwej aplikacji.

Teraz, gdy balast odrzucony, można było się skupić na właściwym problemie i jego rozwiązaniach:

  • podstawowy uploader, który działa bazując na zwykłym formularzu
  • uploader rozszerzony, pozwalający na wyświetlenie rozmiaru poszczególnych plików kolejki i ew. ostrzeżenie, gdy delikwent spróbuje wrzucić więcej niż wynosi przydzielony limit
  • uploader „wypasiony” pokazujący postęp uploadu w czasie rzeczywistym

Największy problem, to stworzenie ujednoliconego GUI. Ostatnia opcja jednak mogła wymusić na mnie kompromis w postaci dwóch osobnych interfejsów.

Pierwsze dwie wersje było stosunkowo łatwo zrealizować – zwykłe elementy formularza, tworzona dynamicznie ukryta ramka (gdyż upload AJAX-owy plików nie istnieje), formularz gdzieś na dnie DOM, z ustawionym celem (target) na tę ramkę. Największy problem stanowiło tutaj odpowiednie skopiowanie pola z wybranym plikiem, gdyż JS – ze względów bezpieczeństwa – nie ma możliwości manipulacji wartością. Ale wystarczy sklonować węzeł DOM (cloneNode()) do wirtualnego formularza i go posłać. Problem z głowy.

Wyciągnięcie rozmiarów pliku, bądź wielu pól, również nie jest aż takie problematyczne – przeglądarki działające na silniku WebKit oraz Gecko, dla każdego pola wyboru pliku, ustawiają własność file, która zawiera tablicę wybranych przez użytkownika plików, jest tam również podklucz size. Nie będę się teraz rozwodził nad możliwością wyboru wielu plików naraz, bo nie jest to tematem notki.

Teraz najtwardszy orzech do zgryzienia, mianiowicie – uploader flashowy.

Który wybrać?

W Sieci jest dostępnych kilka uploaderów działających jako animacja flash (statystycznie, najczęściej odpalana wtyczka w przeglądarkach; częściej niż Java, z tego co się zorientowałem). Niestety, PHP bez niestandardowych rozszerzeń nie ma możliwości raportowania o postępie uploadu do klienta. Pozostają albo inne języki (widziałem gdzieś back-end w Perlu do tego celu), albo prosta aplikacja kliencka, która będzie zawierała odpowiednie metody ciągnące za właściwe sznurki w skrypcie. ;)

Z tej racji, że uploader jest projektem wewnętrznym, poszukiwałem jakiegoś darmowego narzędzia z API pozwalającym na zaadaptowanie do własnej aplikacji. Poszperałem na Sieci, w miarę godne uwagi okazały się dwa SWFUploader i Uploadify. Reszta, to były

  • rozwiązania wyspecjalizowane (np. głównie upload zdjęć)
  • płatne
  • pozwalające na wysłanie pliku wyłącznie we własnym interfejsie
  • nie pozwalające na jakąkolwiek kontrolę procesu wysyłki/wybierania plików z poziomu skryptu

Chciałem ujednolicić GUI całej aplikacji, więc potrzebowałem:

  • API pozwalającego na pobranie informacji o dodanym pliku
  • Możliwości utworzenia i zarządzania własną kolejką
  • Wyzwalacza powiadamiającego mój skrypt o postępie wysyłki

SWFUpload wziąłem na tapetę jako pierwszy, ze względu na przyzwoitą dokumentację oraz popularność. Niestety, jeśli chodziło o własną kolejkę, to pojawiły się problemy (albo po prostu nie znalazłem odpowiedniego opisu/metod w API). Za to Uploadify okazało się pod tym względem dość sympatyczne. Wystarczyło – właściwie – uruchomić istniejące przykłady, poprzeciągać trochę po stronie kursorem, żeby zauważyć, że cała kolejka była zbudowana z elementów HTML-owych.

Zapowiadało się łatwo i przyjemnie. Właśnie, zapowiadało, bo zgodnie z prawami Murphy’ego: Jeżeli coś może się nie udać — nie uda się na pewno.

Po trzech dniach poświęconych na rozwalanie kłód pod nogami, siekiera jednak dopięła swego.

Uploadify?

Starcie#1 – przykłady

Pierwszą kłodą były wspomniane przeze mnie przykłady. Jeśli odpalimy je samodzielnie, wszystko jest fajnie. Naprawdę fajnie. Działa, buczy, czasem też i skwierczy.

Jednak po ściągnięciu najnowszej wersji uploadera i podpięciu pod demo – coś nie działa. Ooops? Ta, okazało się, że w starszej wersji jest inna konwencja nazewnictwa metod. Ot, po co w dokumentacji to pisać? No fakt, jest PDF, jest changelog na stronie. Ale tylko w tym ostatnim była wzmianka, że zmieniono nazwy metod API. Tylko w załączonym archiwum z przykładami już nie ma tego napisanego, że działa to tylko ze starszym. Słodko. Dopiero gdy Firebug wrzeszczy, że coś jest nie tak, to człowiek dochodzi do wniosku, że nie ma metod w obiekcie. Ok, krótkie poszukiwania i wiadomo, co jest nie tak. Szkoda, że ni w ząb nie jest o tym wspomniane w paczce. O ile w większości bibliotek jest dołączony changelog, to w tej nie było.

Może nie jest to aż takie ciężkie do przetrawienia, ale potrafi niepotrzebnie wprawić w zakłopotanie. Ot taka rada dla innych, gdyby ktoś zaczął przygodę z Uploadify od tego tekstu. ;)

Starcie#2 – instalacja

Za to dość dziwną jest filozofia podłączania Uploadify pod nasze skrypty. Mianowicie, wyszukujemy przez standardową funkcję jQuery element z odpowiednim selektorem, wywołujemy na niej metodę uploadify z pożądanymi opcjami. I tu zaczynają się schody – element musi posiadać atrybut ID, inaczej będzie się sypać. No bez przesady, przecież dało się to zrealizować w prosty i elegancki sposób, jak jest pisana reszta pluginów – byleby był to obiekt widziany przez metodę pieniężną ($ ;)). Fakt, Uploadify musi w jakiś sposób się odwoływać do animacji. Ale można było to załatwić wewnętrznymi wywołaniami między obiektami, a nie w taki brudny sposób…

Jeśli chodzi o podczepianie uploadera jest jeszcze inna dziwna sprawa. Mianowicie, gdy już wywołamy metodę konstrukcji Uploadify, to:

  • Obiekt macierzysty, dla którego została wywołana, jest ukrywany
  • Wstawiana jest animacja poprzez stworzenie OBJECT/EMBED o identyfikatorze pierwotnyIDUploader
  • A właściwe zdarzenia są podrzucane do pierwszego, ukrytego obiektu.

Co komu szkodziło podmienić wybrany obiekt na animację flash bądź dodane jej jako dziecko w DOM…? A tak, to trzeba kombinować z CSS, pozycjonowaniem całości, do tego śmietnik w DOM

A i jeszcze jedna rzecz – nauczony tym, że w dokumentacji prawie nic nie ma (oprócz opisu opcji konfiguracyjnych), w życiu nie pomyślałem o tym, żeby wysyłać zdarzenia do obiektu macierzystego, który został ukryty. Wtf…?

Starcie#3 – sposób obsługi zdarzeń

Całe jQuery posiada nieźle zorganizowany system obsługi zdarzeń – nie dość, że można łatwo i szybko obsłużyć standardowe, to dopisanie własnych nie jest żadnym problemem. Tak samo z konwencją nazewnictwa – wszędzie porzucono przedrostki on i skupiono się na samych zdarzeniach. Za to twórca Uploadify poszedł trochę dziwną drogą – zdarzenia wyzwalane przez jQuery przeznaczył raczej na cele wewnętrzne biblioteki (bo tak są przez animację flash wyzwalane), a użytkownikowi pozostawił metody definiowane przy konstrukcji uploadera, np. onComplete, onProgress. Ok, ale wewnątrz przecież i tak się wykonują uploadifyComplete, uploadifyProgress.

I żeby wygodnie z tego korzystać, należałoby najpierw zdjąć obsługę wszystkich tych zdarzeń ze standardowej biblioteki API. Na usprawiedliwienie decyzji autorów - w Uploadify jest zintegrowana jakaś prowizoryczna kolejka. Fakt, działa, ale – moim zdaniem – programiści i tak samodzielnie zaimplementują własną, a fakt wrzucenia tego do całości przez autorów do jednego wora tylko niepotrzebnie zwiększył rozmiar pliku wynikowego. Z drugiej strony, pozwala to na łatwe dorzucenie uploadu przez początkujących, zapewnienie funkcjonalności Out of The Box.

Jednak wcale bym się nie obraził, gdyby było to ujednolicone, i np. w przykładach można by było elegancką kolejkę zaimplementować jako ustawienia przy konstrukcji. I wtedy nie byłoby nadmiarowego kodu, a każdy miałby skrojone na miarę – czyste, fajne API.

Starcie#4 – fochy wtyczki Flash

Przyznam, że już dawno miałem taką chochliczą zagadkę, która sprawiła, że spędziłem mnóstwo czasu na testowaniu różnych kombinacji uniemożliwiających działanie skryptu. Miałem zagwozdkę, dlaczego przy wybraniu paru plików, Uploadify w ogóle nie wyzwalało zdarzenia onComplete, pomimo że pierwszy plik był wysyłany na serwer. Za to onProgress było – 100% postępu w wysyłce.

Winowajca…? Nadawanie display: none dla warstwy zawierającej uploader. Na forach Uploadify wyczytałem, że jeżeli taka sytuacja ma miejsce, to wtyczka Adobe zamraża działanie animacji. Zresztą – nie wiedzieć czemu – czasem wynikają dziwne problemy, które – pomimo książkowego zapisu kodu – biorą się znikąd. Zatem – na czas uploadu trzeba zadbać, aby w naszym kreatorze obiekt z uploaderem był cały czas widoczny, dla kodu. Co nie znaczy, że nie możemy go schować ujemnym z-indexem, czy zerową przezroczystością (gdy zastosujemy tylko to ostatnie, to w Operze tak średnio przechodzi…).

Starcie#5 – JS + Flash + Firebug = WTF?

Przyznam, że mnie to trochę zaskoczyło – w każdym zdarzeniu wyzwalanym przez Uploadify, użycie funkcji, które zatrzymują działanie GUI (np. alert()), powoduje zamrożenie działania Firefoksa. Nie wiem, czym jest to spowodowane (gdyż ten sam kod na np. Chromium działa bez problemów), ale pomaga stary dobry kod. Zamiast alert:

  1. setTimeout(function(){
  2.         alert('asd');
  3. }, 1);

Jest to jedna z rzeczy, która stanowi dla mnie zagadkę. I chyba nią pozostanie. Huh…

Czysty kod

Ok, 3 strony A4 rozważań teoretycznych, teraz przejdźmy do implementacji w praktyce. Moim zdaniem, dość czytelnej, w której panuje porządek i łatwo jest to dostosować. :)

Zacznijmy od szkieletu.

HTML

  1. <head>
  2. <script type="text/javascript" src="jquery.js"></script>
  3. <script type="text/javascript" src="swfobject.js"></script>
  4. <script type="text/javascript" src="jquery.uploadify.js"></script>
  5.  
  6. <script type="text/javascript" src="script.js"></script>
  7.  
  8. <style>
  9. #queue
  10.     { list-style-type: none; padding: 0; margin: 0; }
  11.    
  12.     #queue li
  13.         { overflow: hidden; }
  14.        
  15.     #queue li span.size, #queue li a
  16.         { float: right }
  17.        
  18.     #queue li div.progress
  19.         { height: 20px; border: 1px solid #000; overflow: hidden; }
  20.        
  21.         #queue li div.progress span
  22.             { float: left; height: 20px; background: #000; }
  23.    
  24. </style>
  25.  
  26.  
  27.  
  28. </head>
  29. <body>
  30.     <div id="add">asd</div>
  31. </body>

No zbyt wiele nie ma tu do tłumaczenia – parę reguł CSS, element będący pojemnikiem na uploader, wszystko.

JS

  1. $(function(){
  2.     // jakiś uchwyt do listy ;)
  3.     var list = $('<ul id="queue" />').appendTo('body');
  4.  
  5.     $("#add")
  6.         .uploadify({
  7.             'uploader': 'uploadify.swf',
  8.             'script': 'upload.php',
  9.             'multi': true,
  10.             'displayData': 'speed',
  11.         });
  12.        
  13.     $('#add')   // uploadify nie zwraca metody łańcuchowej, niestety...
  14.         .unbind('uploadifySelect uploadifyComplete uploadifyCancel uploadifyProgress')  // pozbywamy się domyślnych handlerów
  15.         .bind('uploadifySelect', function(event, ID, data){ // zdarzenie wyzwalane przy dodaniu pliku, każdego z osobna
  16.             var item = $(
  17.                 '<li>'+
  18.                 '<span class="size" />'+
  19.                 '<div class="progress"><span /></div>'+
  20.                 '<a>usuń</a>'+
  21.                 '</li>'); // tworzymy element listy - nazwa w <li />, rozmiar, postęp i kasowanie
  22.                
  23.             item.attr('rel', ID);    // ID pliku zwracane przez Uploadify
  24.            
  25.             item.find('span.size').html(data.size); // etykietka z rozmiarem
  26.             item.append(data.name); // nazwa ;)
  27.            
  28.             list.append(item)// dopisanie do kolejki
  29.         })
  30.         .bind('uploadifyComplete uploadifyCancel', function(event, ID, data){   // ukończenie uploadu pliku bądź jego anulowania
  31.             list.find('li[rel='+ID+']').remove();   // przy zakończeniu/anulowaniu pliku - usuń z listy
  32.         })
  33.         .bind('uploadifyProgress', function(event, ID, data, progress){
  34.             list.find('li[rel='+ID+'] div.progress span').width(progress.percentage+'%');
  35.         });
  36.        
  37.     // żeby się dało skasować ;)
  38.     $('li a', list).live('click', function(e){  // pilnujemy, aby każdy nowy element miał dodaną odpowiednią funkcję
  39.         e.preventDefault();
  40.         $('#add').uploadifyCancel(
  41.             $(this).parent().attr('rel')
  42.         );
  43.     });
  44.    
  45.     // właściwy upload
  46.     $('<a>wyślij</a>').click(function(e){
  47.         e.preventDefault();
  48.         $('#add').uploadifyUpload();
  49.     }).appendTo('body');
  50. });

Co się dało, opatrzyłem komentarzem. A teraz pora na resztę wyjaśnień. ;) Najważniejsze jest pozbycie się domyślnej funkcji obsługującej wybrane zdarzenia. Jeśli tego nie zrobimy, Uploadify wrzuci obsługę swojej, wbudowanej kolejki.

Pewnie ktoś mnie zapyta, dlaczego użyłem dwa razy $(‘#add’). Cóż, developerzy Uploadify nie zadbali o to, aby ich metody były łańcuchowe. Trzeba się pomęczyć.

Do pełni szczęscia potrzebujemy obsłużyć zaledwie 4 metody:

  • uploadifySelect

    metoda ta jest wyzwalana przez Flash dla każdego dodanego przez nas pliku. Pozwala obsłużyć dodawanie do własnej kolejki, jako argument podaje m.in. ID, który jest istotny ze względu na to, aby identyfikować konkretny plik w wewnętrznej kolejce uploadera (nasza lista nią nie jest, gwoli ścisłości). Można, dzięki temu, kasować wybrany obiekt z kolejki, uploadować tylko jeden wybrany plik, czy też zidentyfikować wysyłany plik. Zachowałem go w atrybucie rel, aby później można było przy pomocy selektora pobrać odpowiedni wiersz

  • uploadifyComplete/uploadifyCancel

    te dwie możemy obsłużyć jedną funkcją – pozwala na wywalenie z własnej kolejki już wysłanego, czy anulowanego pliku

  • uploadProgress

    pozwala na pokazanie własnego paska postępu. Nie skupiałem się na takich szczegółach, jak prędkość (co jest do zrobienia). Przy każdym wywołaniu otrzymujemy ID, po którym identyfikujemy, który plik jest wysyłany (jest to istotne, gdyż Uploadify pozwala na wysyłanie kilku naraz)

Pozostało jeszcze przypilnować, aby dało się wykasować pliki z kolejki. Po to metoda live() operująca na każdym nowo utworzonym linku w kolejce. Przy kliknięciu nakazuje ona uploaderowi anulowanie konkretnego pliku. Aby pobrać parametr – skaczemy w hierarchi wyżej i pobieramy ID z atrybutu rel.

Skrypt-cel uploadu nie jest wielką filozofią, wystarczy przeklepać książkowy przykład.

Działa? Sprawdź. ;)

Podsumowanie

Uploadify jest naprawdę fajnym narzędziem, tylko z trochę kiepskim front-endem jeśli chodzi o JavaScript. Niestety, odbieganie od standardów, jakimi są obsługa zdarzeń oraz brak chainingu, powoduje, że trzeba się jednak trochę napocić, żeby praca z uploaderem była naprawdę przyjemna. Tekst napisałem na bazie moich parodniowych doświadczeń, mam nadzieję, że się przyda. ;)

O czym trzeba pamiętać:

  • uważaj przy tworzeniu kreatorów z Uploadify – warstwa z uploaderem musi być cały czas widoczna w DOM
  • nie krępuj się z kompletnym wyłączaniem standardowych zdarzeń – zaimplementuj je zgodnie z modelem jQuery
  • pilnuj się z przyzwyczajeniami – chaining z metodami API Uploadify spowoduje tylko siwienie Twoich włosów

A czemu nie SWFUpload…? Cóż:

  • cięższe niż Uploadify
  • wygodne wykorzystanie z jQuery wymagałoby dołączenia dodatkowej biblioteki
  • Uploadify posiada wszystkie niezbędne funkcje ;)

Choć jedno muszę przyznać – wtyczka SWFUpload do jQuery została jednak lepiej przemyślana pod kątem obsługi zdarzeń.

5 komentarzy

dopisz swój :: trackback :: RSS z komentarzami

RSS z komentarzami :: trackback

Skomentuj

Możesz używać znaczników XHTML. Dozwolone są następujące tagi: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>

Wszystkie komentarze przechodzą przez moderację oraz filtry antyspamowe. Nie zostanie opublikowany komentarz, jeśli:

  • Jego treść obraża kogokolwiek.
  • W treści znajdują się wulgaryzmy i słownictwo ogólnie uznane za nieprzyzwoite.
  • Mam wątpliwości co do autora wpisu (Wszelkie anonimy są kasowane - niezależnie od zawartości - wpisz prawdziwy e-mail. Jeśli usunąłem, Twoim zdaniem, komentarz niesłusznie - daj znać). Zdarza się, iż sprawdzam kim jest komentujący.
  • Zawiera jakąkolwiek formę reklamy.

Szufladka