Ruby on Rails: Renderování

24. 2. 2006
Doba čtení: 7 minut

Sdílet

Seriál Ruby on Rails se nám pomalu chýlí ke konci. V dnešním předposledním díle našeho seriálu se podíváme, jak Rails řeší session, cookies a obecně výstup dat.

Renderováním je myšlen proces, při němž jsou vytvořena data k zobrazení v prohlížeči. Data vzniknou nějakým způsobem v kontroléru, defaultně použitím šablony, jejíž název odpovídá názvu kontroléru a akci. Máme-li tedy v kontroléru prázdnou akci index,

class BlogsController < ApplicationController
  def index
  end

end 

bude nejspíš zpracována šablona views/blogs/in­dex.rhtml. Je to totéž, jako by na konci byla volána metoda render() bez parametrů.

class BlogsController < ApplicationController
  def index

    render()
  end
end 

Nyní se podíváme na parametry metody render. Požadujeme-li renderovat šablonu, která náleží jiné akci, specifikujeme ji parametrem :action. V následujícím příkladě bude akce show simulovat akci index. Kdybychom nezavolali render sami, defaultně by se renderovala šablona views/blog/show­.rhtml.

class BlogsController < ApplicationController


  def index
    @list = [1,2,3]
  end

  def show
    index
    render( :action => :index )
  end

end 

Jak ale renderovat libovolné šablony? Slouží k tomu parametr :template.

class BlogsController < ApplicationController

  def show

    render( :template => 'guestbook/show' )
  end

end 

Kromě toho lze specifikovat i jiný layout než je ten, jenž byl nastaven globálně v kontroléru, proč ne třeba zelený.

class BlogsController < ApplicationController

  layout 'cerveny'
  def index
    if rand( 2 ) == 0
      render( :layout => 'zeleny' )
    end
  end
end 

Ovlivňováním toho, jaké šablony se budou renderovat, výčet užití metody render() nekončí. Co když nechceme použít šablonu vůbec? Samozřejmě je možné poslat prohlížeči data přímo jako řetězec. Nelze tak ovšem, naštěstí, napodobovat echo v PHP. Vícenásobné volání render totiž není možné. Kontrolér by se měl z principu starat pouze o funkčnost.

Zkusme si nadefinovat metodu s názvem „method_missing“. Ruby zavolá tuto metodu v případě, kdy bylo přistupováno k jiné, neexistující metodě téhož objektu. Jelikož akce je metodou objektu kontroléru, method_missing bude zavolána tehdy, budeme-li volat neexistující akci. V method_missing zobrazíme ladicí informace, právě pomocí render( :text ).

class BlogsController < ApplicationController
  def method_missing( name, *args )

    render( :text => "<html><body><h1>Neexistujici akce: #{name}</h1>Parametry: #{ params.to_yaml }</body></html>" )
  end
end 

A nakonec ještě nastavíme status hlavičku HTTP na jinou hodnotu než je defaultní „200 OK“.

class BlogsController < ApplicationController
  def index
    render( :status => '666 Very very bad', :text=>'' )
  end

end 

HTTP Hlavičky

Posílané HTTP hlavičky můžeme ovlivnit i jinak. S hlavičkami se pracuje jako s hashem (myšleno asociovaným polem). V příkladu budeme posílat prohlížeči náhodný obrázek (uvádím pouze pro demonstraci, používat render tímto způsobem není vhodné, viz dále).

class BlogsController < ApplicationController


  def show_random_picture
    num = rand( 20 )
    data = File.open( RAILS_ROOT + "/public/images/random/pic#{num}.jpg" ).read
    render( :text => data )
    headers['Content-Type'] = 'image/jpeg'
    headers['Content-Disposition'] = 'inline'
  end

end 

Posílání dat

Ne ke všemu se hodí render. Obecná (binární) data je lepší posílat pomocí send_data. Následující ukázka řeší zobrazení náhodného obrázku bez render(). Parametr :disposition způsobí, že se obrázek v případě akce show_random_picture zobrazí, kdežto download_random_pic­ture vyvolá dialog k jeho stažení do souboru random.jpg.

class BlogsController < ApplicationController

  def show_random_picture

    num = rand( 20 )
    data = File.open( RAILS_ROOT + "/public/images/random/pic#{num}.jpg" ).read
    send_data( data, :type => 'image/jpeg', :disposition => 'inline' )
  end

  def download_random_picture
    num = rand( 20 )
    data = File.open( RAILS_ROOT + "/public/images/random/pic#{num}.jpg" ).read
    send_data( data, :type => 'image/jpeg', :disposition => 'attachment', :filename => 'random.jpg' )

  end

end 

Je-li však soubor velký, není vhodné ho celý načítat do paměti. Metoda send_file je proto vhodnější, parametrem :streaming docílíme, že se soubor nenačte do paměti celý, ale bude se posílat po částech o maximálních velikostech :buffer_size. Parametr :disposition je defaultně attachment (stažení do souboru), takže ho neuvádím.

class BlogsController < ApplicationController


  def download_big_file
    send_file( RAILS_ROOT + "/public/big_file.dat", :filename=>'big_file.dat', :type=>'application/octet-stream', :streaming=>true, :buffer_size=>4096 )
  end

end 

Cookies

Obdobně jako s HTTP hlavičkami se pracuje i s cookies. Zde v jedné akci cookie nastavíme a v druhé ji přečteme.

class BlogsController < ApplicationController

  def set_cookie
    cookies[:mycookie] = Time.now.to_s
    render( :text => "cookie byla nastavena." )

  end

  def show_cookie
    render( :text => "cookie ma hodnotu: " + cookies[:mycookie] )
  end

end 

Chceme-li nastavit cookies přesněji (specifikovat expirační dobu či cestu), předáme parametry jako hash (asociované pole) namísto řetězce. Cookie s expirační dobou 0 by měla být aktivní do doby, než uživatel zavře okno prohlížeče.

class BlogsController < ApplicationController

  def set_cookie
    cookies[:mycookie] = {
      :value=> Time.now.to_s
      :path=>'/',

      :expire=>0
    }
    render( :text => "cookie byla nastavena." )
  end

end 

Session

Cookies nejsou určeny k ukládání objemných dat, jednak je velikost jedné cookie omezena, jednak by bylo neefektivní, kdyby velká data prohlížeč posílal s každým požadavkem. Objemnější data se dobře udržují v session. Cookies pak obsahují pouze identifikátor konkrétní session, defaultně se session cookie nazývá _session_id.

V session můžeme udržovat nejen řetězce, nýbrž celé objekty. Před uložením jsou objekty serializovány do řetězců a před čtením opět deserializovány. Samozřejmě nelze serializovat každý objekt (například objekty tříd IO či Thread), ale jen ty objekty, které umí serializovat funkce Marshal::dump().

Do session můžeme uložit celý model. Vyhneme se tak zbytečným databázovým dotazům, pokud se databáze nemění. V takovém případě je ale nutné serializovaný model specifikovat v každém kontroléru, v němž bude používán, poněvadž třída modelu musí být načtena ještě před deserializací modelu.

class BlogsController < ApplicationController


  model :blog
  
  ...

end 

Také nedoporučuji do session ukládat příliš objemná data, protože by byla načítána při každém použití kontroléru, ačkoli by v něm nemusela být použita. To samé platí o ukládání důležitých dat do session. Smazáním cookie se ztratí přístup k takové session, tudíž ztratíme i data. Ukládáme-li do session celý model, deserializace nemusí proběhnout, jestliže třídu modelu mezitím výrazně změníme. Nakonec, ne každý uživatel má cookies zapnuty, přesto by stránky měly fungovat alespoň v základní podobě.

Objekty v session jsou defaultně serializovány a ukládány do souborů, existuje přesto více možností, kam s nimi. Lze je uchovávat v databázi, v souborech bez serializace, v paměti, či pomocí DRb (vzdáleného server-klient uložiště). Obvykle ale defaultní způsob (PStore) stačí.

Na rozdíl od session v PHP Rails neprovádějí odmazávání starých souborů ani záznamů v databázi. Je třeba je mazat cronem.

Způsob ukládání session se nastavuje v souboru config/environ­ment.rb; v uváděném příkladě se používá PStore, soubory budou mít prefix sess_ a budou se nacházet v adresáři /tmp/sessions. Ostatní parametry ovlivňují nastavení session cookie.

ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:database_manager] = CGI::Session::PStore
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:session_path] = '/'
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:session_domain] = 'rolldance.cz'
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:session_key] = 'RAILSESSID'
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:session_expires] = Time.now.to_i + 3600
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:tmpdir] = '/tmp/sessions'
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS[:prefix] = 'sess_' 

Chcete-li přesto používat databázi, v souboru config/environ­ment.rb stačí odkomentovat jeden řádek a pomocí příkazu rake vytvořit databázovou tabulku.

  # Use the database for sessions instead of the file system
  # (create the session table with 'rake create_sessions_table')
  # config.action_controller.session_store = :active_record_store 

Více informací najdete v Rails API.

Flash

Flash nemá nic společného s programem na tvorbu animací. Jedná se o hash (asociované pole), který je používán ke komunikaci mezi dvěma stránkami (akcemi). Hash sám je uchováván v session, chová se jako session, avšak na rozdíl od session jeho obsah expiruje na následující stránce (v následující akci).

Bude-li zavolána akce save, nastaví se v ní hláška „msg“. Akce show hlášku zobrazí, ale jen při prvním volání. Po reloadu se hláška již neobjeví. Můžete si všimnout, že flash neexpiruje několikanásobným čtením v téže akci, ale jen v akci následující.

class BlogsController < ApplicationController

  def save
    flash[:msg] = 'Nejde to ulozit!'
  end

  def show
    msg = flash[:msg]

    msg = flash[:msg] # neexpiruje nekolikanasobnym ctenim
    render( :text => msg )
  end

  def show2
    msg = flash[:msg]
    render( :text => msg )
  end

end 

Pomocí flash.keep lze expiraci zamezit, čímž se hodnota flashe „podrží“ do další akce. V předchozím případě by v akci show2 „msg“ již neexistovala, v následujícím existovat bude.

class BlogsController < ApplicationController

  def save

    flash[:msg] = 'Nejde to ulozit!'
  end

  def show
    msg = flash[:msg]
    msg = flash[:msg]
    flash.keep( :msg )
    render( :text => msg )
  end

  def show2

    msg = flash[:msg]
    render( :text => msg )
  end

end 

Flash lze použít i ke komunikaci mezi kontrolérem a šablonou. V tom případě není nutné, aby se obsah distribuoval do příští akce. Expirovat bude po skončení současné akce, a to díky flash.now.

bitcoin_skoleni

class BlogsController < ApplicationController


  def save
    flash.now[:zobrazit_menu] = true
  end

end 

Flash slouží nejen k posílání dočasných hlášek, ale obecně jakýchkoli dat, která potřebujeme udržovat mezi dvěma akcemi. Flash můžeme v kombinaci s flash.keep používat místo session. Nemusíme pak přemýšlet, kdy a kde nepotřebná data ze session odmazávat; naopak budeme řešit, kde je udržovat, což je myslím jednodušší, neboť to bývá obvykle ta samá stránka, na níž jsou data naposledy používána.

Závěr

Příště seriál dokončíme popisem migrací, filtrů, kešování a použití některých pluginů.

Seriál: Ruby on Rails