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 }
}
}



