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することはありません。

http://www2a.biglobe.ne.jp/%7eseki/ruby/erbd.jpg

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