the erb way
このページではERBをライブラリとして使用する、ERBらしい応用を紹介します。 はじめに部品を説明し、サンプルを示します。
binding
ERBはeval()を利用してeRubyスクリプトを実行します。 eval()には実行環境を示すbindingを指定できますが、 ERBでも同様にbindingを指定することができます。
bindingを指定することで、任意のスコープでeRubyスクリプトを実行でき、 eRubyの部品化を容易にします。
bindingを利用した例を示します。
require 'erb' class Foo SCRIPT = <<EOS <h1><%= @name %></h1> <ul> <% ary.each do |x|%> <li><%= x %></li> <% end %> </ul> EOS def initialize(name) @name = name @erb = ERB.new(SCRIPT) end def foo(ary) @erb.result(binding) end end it = Foo.new('foo') puts it.foo([1,2,'<dia>'])
SCRIPTに定義されているeRubyスクリプトでは、 インスタンス変数@nameや、変数aryを参照しています。 TOPLEVEL_BINDINGで実行してはエラーになってしまうでしょう。
fooメソッドはbindingを指定して、Fooのインスタンスのスコープで eRubyスクリプトを実行します。
また、eRubyスクリプトからERBの生成のコストを節約するため、 fooメソッドで毎回行なうのではなく、 initializeメソッドで一度だけ行なうようにしています。
ERB::Util
ERB::Utilはhtmlの生成などで便利な二つのメソッドを定義してあるモジュールです。 includeして使用します。
ERB::Util.html_escape(s)
ERB::Util.h(s)
-
HTMLの &"<> をエスケープする。
ERB::Util.url_encode(s)
ERB::Util.u(s)
-
文字列をURLエンコードする。
先ほどのスクリプトには問題があって、@nameやaryの内容に 注意を払っていません。HTMLエスケープをしながら出力するように 変更してみましょう。
require 'erb' class Foo include ERB::Util SCRIPT = <<EOS <h1><%=h @name %></h1> <ul> <% ary.each do |x|%> <li><%=h x %></li> <% end %> </ul> EOS def initialize(name) @name = name @erb = ERB.new(SCRIPT) end def foo(ary) @erb.result(binding) end end it = Foo.new('foo') puts it.foo([1,2,'<dia>'])
<%=h @name %>や<%=h x %>がHTMLエスケープしている部分です。
<%=h ... %>
と書くと一見eRubyの拡張のように見えますが、よく見るとただの メソッド呼び出しであることがわかります。 上記はつまり次のようなスクリプトなのです。
<%= h(...) %>
def_method
eRubyスクリプトをeval()するのではなく、メソッドとして定義することもできます。
ERBを繰り返し実行する場合など、eval()の行なうスクリプトのパーズ処理の コストを節約できるかもしれません。
ERB#def_method(mod, methodname, fname='(ERB)')
-
変換したRubyスクリプトをメソッドとして定義する。 定義先のモジュールはmodで指定し、メソッド名はmethodnameで指定する。 fnameはスクリプトを定義する際のファイル名である。主にエラー時に活躍する。
erb = ERB.new(script) erb.def_method(MyClass, 'foo(bar)', 'foo.erb')
ERB::DefMethod.def_erb_method(methodname, erb)
-
selfにerbのスクリプトをメソッドてして定義する。メソッド名はmethodnameで指定する。 selfが文字列の時、そのファイルを読み込みERBで変換したのち、メソッドとして定義する。
class Writer extend ERB::DefMethod def_erb_method('to_html', 'writer.erb') ... end ... puts writer.to_html
先ほどのスクリプトをdef_methodを使って書き直してみましょう。
require 'erb' class Foo include ERB::Util SCRIPT = <<EOS <h1><%=h @name %></h1> <ul> <% ary.each do |x|%> <li><%=h x %></li> <% end %> </ul> EOS ERB.new(SCRIPT).def_method(self, 'foo(ary)') def initialize(name) @name = name end end it = Foo.new('foo') puts it.foo([1,2,'<dia>'])
メソッドfooの定義なくなり、代わりにdef_method(self, 'foo(ary)')となりました。
さらにRubyスクリプトとeRubyスクリプトのファイルを分割してみます。
まずeRubyスクリプト(foo.erb)を示します。 定数Foo::SCRIPTがそのまま入っています。
<h1><%=h @name %></h1> <ul> <% ary.each do |x|%> <li><%=h x %></li> <% end %> </ul>
次にRubyスクリプトを示します。
require 'erb' class Foo include ERB::Util extend ERB::DefMethod def_erb_method('foo(ary)', 'foo.erb') def initialize(name) @name = name end end it = Foo.new('foo') puts it.foo([1,2,'<dia>'])
はじめに、ERB::DefMethodをextendしてFooクラスでdef_erb_method できるようにします。
extend ERB::DefMethod
つづいて def_erb_method でメソッドを定義します。 最初の引数はメソッド名と仮引数です。 二番目のメソッドはeRubyスクリプトのファイル名です。
ロジックとなるRubyスクリプトとビューであるeRubyスクリプトが 分離されているのがわかるでしょうか?
Play with dRuby
dRubyを使ったサーバとCGIの構成でERbを使ってみましょう。
- ごく簡単なWebチャット風なもの
- 発言と一緒にクライアントのIPアドレスと時刻を表示する
- 120秒たった発言は消えてしまう
- ファイルに保存しない
- 漢字コードは気にしない
- dRubyによるサーバknock_s.rbとCGIインターフェイスknock.rbで構成する
まず、主処理であるknock_s.rbを見てみます。 KnockPageという定数でeRubyスクリプトを定義しています。 KnockWriterがページを生成するクラスで、KnockPageから次のようにメソッドを定義しています。
ERB.new(KnockPage).def_method(self, "to_html")
メソッド化されているので、実行時にevalすることはありません。
knock_s.rbの全体の流れはよく見かけるdRubyのプログラムなので解説は省略します。
CGIを実行する前に、knock_s.rbを起動しておかなくてはなりません。
% cat knock_s.rb require 'drb/drb' require 'erb' require 'monitor' class Knock def initialize(host, str) @host = host @str = str @time = Time.now end attr_reader :host, :str, :time end class KnockHistory include MonitorMixin def initialize(expire=120) super() @history = [] @expire = expire @keeper = make_keeper end def add(host, str) synchronize do @history.push(Knock.new(host, str)) end true end def each(&block) synchronize do @history.each(&block) end end private def forget time = Time.now - @expire synchronize do while (knock = @history[0]) return if knock.time > time @history.shift end end end def make_keeper Thread.new do loop do sleep 5 forget end end end end KnockPage = <<EOP <ul> <% @history.each do |k| %> <li> <%=h k.host %>(<%= k.time.strftime("%H:%M:%S") %>): <%=h k.str %> </li> <% end %> </ul> <form action="knock.rb" method="post"> <input type="text" name="knock" /> <input type="submit" name="send" value="send" /> </form> EOP class KnockWriter include DRbUndumped include ERB::Util def initialize(history) @history = history end def add(host, str) @history.add(host, str) end ERB.new(KnockPage).def_method(self, "to_html") end knock = KnockHistory.new writer = KnockWriter.new(knock) DRb.start_service('druby://localhost:8411', writer) gets
次はCGIインターフェイスのknock.rbです。 肝心な処理をknock_s.rbのプロセスに任せているので 仕事があまりありません。
CGIのリクエストからREMOTE_HOSTと発言を取り出して knockサーバへ送ります。 その後、knock.to_htmlによってページ本体のbodyを取得してページを組み立てます。
% cat knock.rb #!/usr/local/bin/ruby require 'drb/drb' require 'cgi' cgi = CGI.new('html3') DRb.start_service knock = DRbObject.new(nil, 'druby://localhost:8411') str ,= cgi['knock'] if str && str.size > 0 host = cgi.remote_host || cgi.remote_addr || 'unknown' knock.add(host, str) end cgi.out { cgi.html() { cgi.head { cgi.title { 'Knock' } + '<meta http-equiv="refresh" content="15; url=knock.rb" />' } + cgi.body { knock.to_html } } }