Nieinwazyjny javascript razem z Ruby on Rails

Posted by Piotr Sarnacki Tue, 25 Dec 2007 20:15:00 GMT

JakiÅ› czas temu Riddle napisaÅ‚ o tym dlaczego nie można zakÅ‚adać, że ktoÅ› ma włączony javascript. Temat byÅ‚ już wczeÅ›niej wiele razy poruszany, mi bardzo podobaÅ‚ siÄ™ artykuÅ‚ Chrisa Heilmanna The seven rules of Unobtrusive JavaScript. ZachÄ™cam do zapoznania siÄ™ z tymi dwoma tekstami – pomogÄ… zrozumieć dlaczego “tracić” czas na rozwiÄ…zywanie problemów, o których napiszÄ™ poniżej. StreszczajÄ…c krótko artykuÅ‚y mogÄ™ napisać, że jeżeli to możliwe, należy pisać aplikacjÄ™ tak, żeby dziaÅ‚aÅ‚a bez włączonego javascriptu. Skrypty mogÄ… nie dziaÅ‚ać z kilku powodów:

  • użytkownik ma wyłączony javascript w przeglÄ…darce
  • wystÄ…pi błąd w kodzie (nie możemy sprawdzić kodu na wszystkich urzÄ…dzeniach)
  • strona nie zaÅ‚aduje siÄ™ do koÅ„ca
  • boty wyszukiwarek nie używajÄ… javascriptu, wiÄ™c w skrajnym wypadku możemy uniemożliwić zindeksowanie naszej strony

Jak to wyglÄ…da w Railsach?

Railsy są wyposażone w zestaw helperów generujących różnego rodzaju kawałki kodu javascript. Pomysł jest z pozoru bardzo fajny. Początkujący mogą szybko zacząć używać javascriptu razem z dobrodziejstwami, które daje nam Ajax bez znajomości samego języka. Jest jednak sporo minusów używania helperów:

  • generujÄ… one dodatkowy niepotrzebny kod javascript. jeżeli na stronie mamy kilkadziesiÄ…t linków, a każdy z nich ma wklejony kod `onclick=”new Ajax.Request(’/controller/action?n=33’, {asynchronous:true, evalScripts:true, onComplete:function(request){undoRequestCompleted(request)}}); return false;”` strona bÄ™dzie ważyć dużo wiÄ™cej.
  • javascript w nich użyty jest “inwazyjny”. Jeżeli javascript nie bÄ™dzie dziaÅ‚aÅ‚, to element wygenerowany w taki sposób również nie zadziaÅ‚a1
  • ciężko jest siÄ™ przy nich trzymać zasady DRY. MajÄ…c 5 linków wygenerowanych metodÄ… link_to_remote z takimi samymi opcjami, za każdym razem gdy coÅ› musimy zmienić, zmieniamy to w 5 miejscach. Powinno siÄ™ oczywiÅ›cie napisać helpera, który wygeneruje link z danymi opcjami. Tylko chyba nie tÄ™dy droga – w dalszej części artykuÅ‚u postaram siÄ™ pokazać dlaczego nieinwazyjny javascript jest lepszy w tego typu zadaniach.
  • z mojego doÅ›wiadczenia wynika, że bardzo czÄ™sto, gdy ilość kodu siÄ™ powiÄ™ksza i javascript generowany przez railsy miesza siÄ™ z tym z plików js można Å‚atwo siÄ™ pogubić. Tyczy siÄ™ to także różnego rodzaju api – na przykÅ‚ad google maps. NajÅ‚atwiej dziaÅ‚ać, gdy javascript jest odseparowany od kodu railsów
  • jesteÅ›my zwiÄ…zani z jednÄ… bibliotekÄ… (w tym wypadku prototype+script.aculo.us) – jeżeli chcemy zamienić jÄ… na coÅ› innego (ja ostatnio przesiadÅ‚em siÄ™ na jQuery, zastanawiaÅ‚em siÄ™ też nad YUI) helpery przestanÄ… dziaÅ‚ać – można je oczywiÅ›cie przepisać, ale komu by siÄ™ chciaÅ‚o. DHH nie zamierza nic w tej kwestii zmieniać, wiÄ™c na zmianÄ™ tego w Railsach nie ma co czekać.

Jakie sÄ… minusy? Trzeba lepiej poznać javascript (wÅ‚aÅ›ciwie dla mnie to nie jest minus, ale dla niektórych być może tak). Nie jest to jednak przeszkoda nie do pokonania dla poczÄ…tkujÄ…cych. Javascript, który jest potrzebny do zadaÅ„ możliwych do wykonania z użyciem samych helperów nie jest z reguÅ‚y przesadnie trudny do nauczenia. Ratunkiem dla osób, które nadal chcÄ… korzystać z helperów jest plugin UJS. Jeżeli bardzo nie chcesz pisać wszystkiego w czystym javascripcie, to jest to bardzo fajne połączenie prostoty helperów i zalet nieinwazyjnego javascriptu. Jest ona jednak pisana dla Prototype’a, wiÄ™c tak jak w ostatnim punkcie z powyższej listy można o niej zapomnieć, jeżeli używana jest jakakolwiek inna biblioteka.

W komentarzach apohllo zauważył, że plugin UJS nie jest już rozwijany. Używacie na własną odpowiedzialność. :)

Przejdę do przykładów, bo przecież nie samą teorią człowiek żyje.

PrzerzuciÅ‚em siÄ™ ostatnio na jQuery i chyba przy niej zostanÄ™. Rozumiem jednak, że wiÄ™kszość użytkowników railsów jest zwiÄ…zana z Prototype’em, wiÄ™c kod bÄ™dÄ™ podawaÅ‚ w dwóch wersjach, dla Prototype’a i jQuery.

Na poczÄ…tek wprowadzenie. Co zrobić, żeby wyrzucić z htmla (i railsów) wstawki Javascript? Wszystko wstawiamy do aplikacji używajÄ…c zdarzeÅ„. Przypuśćmy, że mamy linka o id=”someLink”. Zamiast dopisania onclick:


    <a href="#" onclick="alert('Klik!'); return false;">Link</a>
  
należy użyć:

Prototype:

    Event.observe($('someLink'), 'click', function(event) {
      alert('Klik!');
      Event.stop(event);
    }
  
jQuery:

    $('#someLink').click(function (){
      alert('Klik!');
      return false;
    });
  

Oba przykłady dodają zdarzenie uaktywniane kliknięciem w linka. Ostatnia linijka w obu funkcjach, które są wykonywane po kliknięciu (nazywane są z reguły handlerami) jest wstawiona po to, żeby kliknięcie linka nie przeładowało strony.

Kod taki w aplikacji Rails można wrzucić do pliku application.js, lub jakiegoÅ› specyficznego pliku js Å‚adowanego na danej stronie. Należy też go zaÅ‚adować dopiero po wczytaniu siÄ™ caÅ‚ego dokumentu. Normalnie coÅ› takiego uzyskiwaÅ‚o siÄ™ wpisujÄ…c w body: `onload=”jakasFunkcjaJavascript();”`, ale takie dodawanie jest passe, wiÄ™c:

Prototype:

    Event.observe(window, 'load', function() {
      //kod który wykona się po załadowaniu strony
    }

    // lub zdefiniowana wcześniej funkcja, która wykona się po załadowaniu strony
    Event.observe(window, 'load', jakasFunkcjaJavascript());
  
jQuery:

    $(function() {
      //kod który wykona się po załadowaniu strony
    });

    // lub zdefiniowana wcześniej funkcja, która wykona się po załadowaniu strony
    $(jakasFunkcjaJavascript);
    //powyższe przykłady, to skrócone wersje document.ready:
    $(document).ready(function () {});
  

Dzięki tym konstrukcjom mamy pewność, że kod wykona się dopiero gdy załaduje się cały dokument, a nie w momencie, gdy dołączony jest plik js.

Teraz przykład prostego zapytania ajax (przykład z dokumentacji railsów):

  link_to_remote 'hello', :url => { :action => "action" }, 
    404 => "alert('Not found...? Wrong URL...?')",
    :failure => "alert('HTTP Error ' + request.status + '!')" 
  # Wygeneruje: <a href="#" onclick="new Ajax.Request('/testing/action', {asynchronous:true, evalScripts:true,
  #            on404:function(request){alert('Not found...? Wrong URL...?')},
  #            onFailure:function(request){alert('HTTP Error ' + request.status + '!')}}); return false;">hello</a>
Jak widać powyżej wygenerowanego kodu jest całkiem sporo. Jeżeli będzie trzeba wstawić taki link w paru miejscach dobrze by było napisać swojego własnego helpera, który automatycznie będzie wklejał komunikaty o błędach. Jak można to zrobić lepiej? Na początek wystarczy stworzyć zwykłego linka z jakąś klasą, lub id:

  link_to 'hello', { :action => 'action' }, :class => 'ajax'
Teraz trzeba użyć trochę javascriptu :

        $$('a.ajax').each(function (element) {
          Event.observe(element, 'click', function(event) {
            new Ajax.Request(this.readAttribute('href'), {asynchronous:true, evalScripts:true, 
              on404:function(request){alert('Not found...? Wrong URL...?')}, 
              onFailure:function(request){alert('HTTP Error ' + request.status + '!')}}); 
            Event.stop(event);
          });
        });

Na poczÄ…tku pobieramy wszystkie linki z klasÄ… ajax i dla każdego z nich wywoÅ‚ujemy funkcjÄ™ `Event.observe(element, ‘click’....`. Dalszy kod wykona siÄ™ wiÄ™c po klikniÄ™ciu w danego linka. W tym wypadku wykonujemy zapytanie ajaxowe (`new Ajax.Request`). Pierwszy argument to atrybut href linka (uwaga, kod ten nie zadziaÅ‚a w starszych wersjach prototype’a, które niepoprawnie obsÅ‚ugiwaÅ‚y this w tego typu funkcji). Reszta kodu to standardowe opcje, po wiÄ™cej odsyÅ‚am do dokumentacji Prototype.

A w jQuery wyglądać to będzie tak:

    $('a.ajax').click(function (){
      $.ajax({
        url: this.href,
        dataType: "script",
        beforeSend: function(xhr) {xhr.setRequestHeader("Accept", "text/javascript");},
        error: function(){
          alert( "Error loading page");
        }
      });     
      return false;
    });

Kod zasadniczo robi to samo, co poprzedni przykÅ‚ad. Można przy okazji porównać prostotÄ™ jQuery i porównać jÄ… z Prototype’em (ostatnio dużo siÄ™ w tej bibliotece pozmieniaÅ‚o, a ja nie jestem na bieżąco, wiÄ™c jeżeli ktoÅ› zna lepszy sposób na napisanie czegoÅ› takiego, to proszÄ™ o komentarz). SkomentujÄ™ tylko atrybuty dataType i beforeSend w funkcji ajax(). UstawiajÄ…c je w taki sposób przekazujemy serwerowi, że chcemy dostać odpowiedź jako skrypt i akceptujemy typ MIME “text/javascript”. Należy te 2 rzeczy dodać, ponieważ inaczej nie bÄ™dzie renderować siÄ™ plik RJS. WiÄ™cej na ten temat w artykule jQuery Ajax + Rails

Rozwiązanie proste i efektywne. Żeby link wykonał javascript wystarczy dodać do niego klasę ajax. Jeżeli strona i kody javascript nie wczytają się, link dalej będzie działał poprawnie. Przy założeniu, że poprawnie obsłużymy wszystko w kontrolerze. Służy do tego metoda `respond_to`


  respond_to do |format|
    format.js # jeżeli to zapytanie wykonane ajaxem uruchomi się plik RJS
    format.html # w przeciwnym wypadku wyrenderowany zostanie template rhtml
  end

Więcej o takim sposobie renderowania templatów pisał na przykład Jamis Buck.

Można też w podobny sposób zamienić zwykłą formę na taką wysyłaną ajaxem:

Prototype:

       $$('form.ajax').each(function (element) {
          Event.observe(element, 'submit', function(event) {
            new Ajax.Request(this.readAttribute('action'), {
              parameters: Form.serialize(this),
              asynchronous:true, 
              evalScripts:true
              }); 
            Event.stop(event);
          });
        });  
  

Powyższy kod jest bardzo podobny do poprzedniego przykÅ‚adu. Różnica polega na tym, że zdarzeniem nie jest ‘click’ tylko ‘submit’ i jako parametry podajmy wynik funkcji `Form.serialize(this)` – zbiera ona wartoÅ›ci pól i zwraca string typu: “pole1=wartosc1&pole2=wartosc2”

jQuery:

    $("form.ajax").ajaxForm({
      dataType: 'script',
      beforeSend: function(xhr) {xhr.setRequestHeader("Accept", "text/javascript");},
      resetForm: true
    });
  

W jQuery najÅ‚atwiej skorzystać z pluginu jQuery Form – zaÅ‚atwia on za nas wszelkie formalnoÅ›ci ;-)

W ten sposób zmiana jakiegoś linka lub formy na jego ajaxową formę to kwestia dodania jednej klasy. Można oczywiście napisac wiele takich funkcji dla różnych przypadków, dowiązanych do tagów z innymi klasami, lub z konkretnym id.

Na koniec krótkie podsumowanie.

Kod javascript dodajemy do aplikacji tak, żeby nie zablokować dostÄ™pu w wypadku braku jego wykonania. Zapomnieć można o wszelakich “onclick” i innych tego typu sprawach. Wszystko powinno być dołączone jako zdarzenia. DziÄ™ki temu zmniejsza i upraszcza siÄ™ kod railsów i ten przez nie generowany.

Warto obejrzeć również plugin MinusMOR, który zmienia trochę podejście do javascriptu. Zamiast plików rjs, w których używamy rubiego zamienianego później na javascript, mamy pliki ejs, w których wpisujemy kod javascript z możliwością wstawiania kodu rubiego. Tak samo jak w rhtmlu poprzez <% %>.

W komentarzach apohllo zauważyÅ‚, że plugin MinusMOR nie jest już rozwijany. Trzeba o tym pamiÄ™tać zaczynajÄ…c go używać. Z drugiej strony widziaÅ‚em kod pluginu i rejestruje on tylko nowe rozszerzenie “ejs”. Na poczÄ…tku szukane bÄ™dÄ… pliki z tym rozszerzeniem, a jeżeli ich nie bÄ™dzie Railsy wyrenderujÄ… RJS. W każdym razie zaczynacie używać na wÅ‚asnÄ… odpowiedzialność. :)

Zapraszam do komentowania – prosiÅ‚bym o opinie dotyczÄ…ce tego typu artykułów. Czy sÄ… zrozumiaÅ‚e? Czy przydajÄ… siÄ™ wam? Konstruktywna krytyka mile widziana. :)


  1. W tym miejscu trzeba zaznaczyć, że jest możliwość wygenerowania linku, czy formy, która bÄ™dzie dziaÅ‚aÅ‚a przy wyłączonym javascripcie, ale niewiele osób o tym wie i z tego korzysta. I trzeba dopisać 2 url, który z reguÅ‚y jest taki sam – Å‚amana jest zasada DRY

Posted in  | Tags , , ,  | 3 comments | 1 trackback

Comments

  1. Avatar apohllo said about 3 hours later:

    Uwaga!

    Wspomniany w tekście plugin UJS NIE JEST JUŻ ROZWIJANY! Osoby nim zainteresowane odsyłam pod adres http://jlaine.net/2007/8/3/from-rails-ajax-helpers-to-low-pro-part-i

  2. Avatar Drogomir said 4 days later:

    Dzięki za uwagę, nie sprawdzałem tego pluginu jeszcze.

    Tylko z drugiej strony co to tak naprawdę zmienia? Plugin jest na tyle prosty i mało zmienia, że nie wiem co miałoby być jeszcze rozwijane. W ostateczności jeżeli ktoś sam nie będzie umiał czegoś zmienić, zawsze można poprosić o pomoc kogoś kto będzie wiedział o co chodzi.

  3. Avatar Drogomir said 16 days later:

    Y… szukaÅ‚em wÅ‚aÅ›nie czegoÅ› w tym artykule i zczaiÅ‚em, że pisaÅ‚eÅ› o UJS, a nie o MinusMOR…

Trackbacks

Use the following link to trackback from your own site:
http://blog.drogomir.com/articles/trackback/34

  1. From kamillo21
    Soczewki
    Jeœli uzywacie soczewki kontaktowe to polecam firme Easyvision, oferuja tani e i dobre soczewki a do tego maj¹ siec salonow optycznych w ktorych mo¿na je bez problemow je nabyc. Maj¹ niestety tylko misieczne soczewki , hmm a ale przecie¿ one w³aœ...

(leave url/email »)

   Comment Markup Help Preview comment

Clicky Web Analytics