Modularyzacja RoR - komponenty

Posted by Jarosław Zabiełło Wed, 19 Apr 2006 21:11:00 GMT

RoR pozwala na modularyzację projektu za pomocą wtyczek, zagnieżdżonych miniaplikacji (rails engines) oraz komponentów.

Używany tutaj blog jest oparty na Typo, aplikacji napisanej w RoR. Nie znalazłem w niej jednak gotowego, wbudowanego mechanizmu aby dodać galerię fotek. Są co prawda linki do zewnętrznego serwisu flickr.com, ale w wersji darmowej można umieścić stosunkowo niewiele fotek.

Aplikacja bloga chodzi na lighttpd i fastcgi. Wszelkie modyfikacje kodu wymagają restartu serwera. Mało to raczej wygodne. Postanowiłem więc stworzyć sobie oddzielny projekt RoR. A zatem standardowo: rails fotki; cd fotki. Potem modyfikacja config/database.yml (ustawilem polaczenie do dowolnej bazy, i tak nie bede uzywal). Do pracy roboczej wystarczy mi webrick. A zatem ruby scripts/serwer i pod adresem http://localhost:3000 dziala nowy projekt.

Starając się trzymać zasady DRY (czyli unikać powtarzania kodu). Działający kod zamieniłem następnie na komponent. Czyli procedura była taka: wpierw tworzę sobie aplikację w izolowanym projekcie RoR. Na stępnie przenoszę ją do komponentu (nadal w tym samym projekcie). Testuję. Jak działa, to łączę komponent z aplikacją docelową. Dzięki użyciu komponentu, unikam zaśmiecania kodu aplikacji docelowej, ograniczając do minimum ilość zmian jakie muszę dodać.

Komponenty

Komponenty w RoR tworzy się (podobnie jak inne rzeczy) stosunkowo łatwo i przyjemnie. Kazda świeża instalacja RoR tworzy domyślnie pusty folder components. Należy tam przejść i stworzyć swój podfolder, np. fotki.

Następnie przenosimy tam plik z kontrolerem. W tym wypadku było to plik miniaturki_controlle.rb. Wiadomo gdzie był: apps/controllers/. Przenosimy go zatem do components/fotki/. Musimy także dokonać w nim paru zmian. Przede wszystkim klasa kontrolera nie może dziedziczyć bezpośrednio po ApplicationController. Musimy uwzględnić to, że kontroler leży w innym miejscu.

Trzeba także zmienić definicje klasy i dodać informację, że szablony będą leżeć w innym miejscu niż domyślnie app/views/. Kontroler więc wyglądać może tak:

class Fotki::MiniaturkiController < ApplicationController
  uses_component_template_root

  def mysio_lista
    filenames(File.dirname(__FILE__)+'/../../public/fotki/mysio', '*.jpg')
    @subfolder = 'mysio/'
    render :template => 'fotki/miniaturki/lista'
  end

  def mysio_fotka
    filenames(File.dirname(__FILE__)+'/../../public/fotki/mysio', '*.jpg')
    @subfolder = 'mysio/'
    render :template => 'fotki/miniaturki/fotka'
  end

  protected

  def filenames(path, filter)
    @filenames = []
    Dir.glob(path+File::SEPARATOR+filter).each do |f|
      @filenames << File.basename(f).sub(/.*^\D+(\d+).+$/, '\1')
    end
    @filenames.sort!
  end
end

Metoda filenames wciaga liste plików i przypisuje do zmiennej instancji @filenames (zmienne tego typu są widziane w szablonach).

Przenieśliśmy kontroler, pora na szablony. Tworzymy zatem kolejny podfolder components/fotki/miniaturki/ i przenosimy tam wszystkie nasze szablony z app/views/miniaturki/. Szablon lista.rhtml służy do wyrzucenia na ekran miniaturek z obrazkami:

<% for f in @filenames %>
  <% if @subfolder%>
    <%= link_to(image_tag("/fotki/#{@subfolder}thumbnails/prev_pict#{f}.jpg",:border => "1"), "/#{@subfolder}#{f}") %>
  <% else %>
    <%= link_to(image_tag("/fotki/#{@subfolder}thumbnails/prev_pict#{f}.jpg",:border => "1"), "/fotka/#{f}") %>
  <% end %>
<% end %>

Natomiast szablon fotka.rhtml wyświetla jedno zdjęcie w pełnym formacie:

<div align="center">
  <div> 
    <%= render :partial => 'menu' %>
    <%= image_tag "/fotki/#{@subfolder}pict#{params[:id]}.jpg" %>
    <%= render :partial => 'menu' %>
</div>

Dobrze byłoby także, aby można było nawigować między obrazkami. Dodałem więc proste menu u góry i na dole. Aby się nie powtarzać, szablon z treścią menu wyniosłem do szablonu cząstkowego (partial). Plik z takim szablonem musi mieć obowiązkowo znak podkreślnika na początku nazywy. Szablon _menu.rhtml wygląda tak:

  <div style="margin-top:10px; margin-bottom: 10px"> 
    [ 
      <% if @filenames.first != params[:id] %>
        <% if @subfolder %>
          <a href="/<%= @subfolder %><%= @filenames[@filenames.index(@params[:id]) - 1] %>">&lt;&lt;</a>
        <% else %>
          <a href="/fotka/<%= @filenames[@filenames.index(@params[:id]) - 1] %>">&lt;&lt;</a>
        <% end %>
        |
      <% end %>
      <% if @subfolder %>
        <a href="/<%= @subfolder %>">miniaturki</a> 
      <% else %>
        <a href="/fotki">miniaturki</a> 
      <% end %>
      <% if @filenames.last != params[:id] %>
        |
        <% if @subfolder %>
          <a href="/<%= @subfolder %><%= @filenames[@filenames.index(@params[:id]) + 1] %>">&gt;&gt;</a>
        <% else %>
          <a href="/fotka/<%= @filenames[@filenames.index(@params[:id]) + 1] %>">&gt;&gt;</a>
        <% end %>
      <% end %>
    ]
  </div>

Jest troszkę rozbudowany z dwóch powodów. Chciałem aby przejścia do następnej lub wcześniejszej fotki były dostępne tylko wtedy kiedy to ma sens. Poza tym, kod uwzględnia tworzenie podfolderów ze zdjęciami. Stąd we wszystkich szablonach dodatkowa zmienna @subfolder.

Gdybym chciał umieścić wywołanie komponentu w szablonie aplikacji, to musiałbym użyć funkcji render_component. Jednak, w tym wypadku nie chciałem grzebać w szablonach Typo. Zatem ostatnią rzeczą jaka została, to zdefiniwanie odpowiedniej reguły dla resolvera adresów. Chcę aby lista pokazała się pod adresem /mysio a konkretne fotki pod adresem /mysio/nrfotki. Reguły rozwiązywania adresów URL znajdują się w pliku config/routes.rb:

ActionController::Routing::Routes.draw do |map|
  # reszta kodu
  map.connect 'mysio', 
              :controller => 'fotki/miniaturki',
              :action     => 'mysio_lista'              
  map.connect 'mysio/:id',
              :controller => 'fotki/miniaturki',
              :action     => 'mysio_fotka',
              :id         => /\d+/
  # blok kończy domyślna reguła:
  map.connect ':controller/:action/:id'
end

To prawie wszystko. Prawie, bo musiałem także napisać kod do przeskalowania obrazków (domyślnie były w rozdzielczości 1280×1024) oraz wygenerowania miniaturek. W związku z tym, że chciałem wszystko już napisac w Ruby, zabrałem się za pisanie skryptu w tym języku. Przeskalowanie fotek do rozdzielczości 1024×768:

#!/usr/bin/ruby
require 'RMagick'
default_path = '/home/rubyonrails/typo/public/fotki'
path = $1 || default_path
Dir.chdir(path)
Dir.glob('*.jpg').each do |f|    
  img = Magick::Image::read(f).first
  if img.columns > 1024
    print f
    img.change_geometry("1024x768") { |cols, rows, im| im.resize!(cols, rows) }
    img.write f
    puts " przeskalowane"
  end
end

Oraz generowanie miniaturek:

#!/usr/bin/ruby
require 'RMagick'
default_path = '/home/rubyonrails/typo/public/fotki'
path = $1 || default_path
outpath = path + File::SEPARATOR + 'thumbnails'
Dir.mkdir(outpath) if not File.exists?(outpath)
Dir.chdir(path)
Dir.glob('*.JPG').each do |f|  
  if f != f.downcase
    File.rename(f, f.downcase)
    f.downcase!
  end
  img = Magick::Image::read(f).first
  img.change_geometry("128x128") { |cols, rows, im| im.resize!(cols, rows) }
  img.write "#{outpath}#{File::SEPARATOR}prev_#{f}"
  p f
end

Niestety, tu spotkała mnie przykra niespodzianka. Biblioteka RMagic wywalała się z komunikatem o braku pamięci. Kod jest poprawny. To jest błąd w tej bibliotece. Chcąc, nie chcąc, przepisałem kod do Pythona:

#!/usr/bin/env python2.4
'''
Skaluje obrazki do 1020x768 oraz generuje miniatiurki
Operuje na plikach JPEG.
'''
import glob, Image, os, sys
scale = (1024,768)

try:
    path, pattern, prefix = sys.argv[1], sys.argv[2], sys.argv[3]
except:
    print "Skladnia: sciezka wzorzec prefix"
    sys.exit(1)
os.chdir(path)
thumbnails_path = path + os.sep + 'thumbnails'
if not os.path.exists(thumbnails_path):
    os.makedirs(thumbnails_path)
for f in glob.glob(pattern):
  print f,
  img = Image.open(f)
  size = img.size
  if max(size) > 1024:
    print "%s (%sx%s)" % (f, size[0], size[1]),
    img = img.resize(scale, Image.ANTIALIAS)
    img.save(f, 'JPEG')
    size = img.size
    print ' przeskalowane do %sx%s' % (size[0], size[1]),
  print " przeskalowywanie do 128x128",
  img.thumbnail((128, 128), Image.ANTIALIAS)
  img.save('thumbnails/%s_%s' % (prefix, f), 'JPEG')
  print

Tym razem wszystko przeszło gładko i szybko. Kod Pythona wykorzystujący biblioteke PIL był poza tym jakieś 2x szybszy. Sprawdza się zatem to, że współcześni programiści nie mogą ograniczać się tylko do jednego języka. Czasami warto mieć w zapasie drugi. Python jest doskonałym uzupełnieniem dla Rubiego. Ruby ma lepszy framework, ale Python ma dojrzalszą bibliotekę graficzna.

Jedyną rzeczą którą nie udało mi się rozwiązać, to umieszczenie treści komponentu w ramach aplikacji Typo (tak jak widać np,. ten artykuł). Typo jest dosyć skomplikowaną aplikacją i nie mogłem się połapac w jego plikach z layoutem. Niektórzy żartują że Typo to przykład kodowania sztucznej inteligencji. :) Przykład działających fotek z moim ulubionym futrzakiem można zobaczyć: tutaj.

Posted in  | Tags  | 3 comments

Comments

  1. Avatar http://blog.temp.rsc.pl said about 8 hours later:

    Jestes pewien ze blad o braku pamieci dotyczyl RMagicka a nie ImageMagicka ? Ja w swojej instalacji uzylem RMagicka+GraphicsMagick i nie mialem takich problemów.

  2. Avatar rofro said 5 days later:

    a moze uzyc minimagick? http://journal.gleepglop.com/articles/2005/12/04/imagemagick-the-ruby-way

  3. Avatar kml said over 2 years later:

    ImageScience też wydaje się fajny http://seattlerb.rubyforge.org/ImageScience.html

(leave url/email »)

   Comment Markup Help Preview comment