Rails3のビューでブロックを伴なうHelperを使う時の注意

globalize3というgemを評価していて、Rails3のおかしな挙動に気がつきました。

ビュー内で、Globalize.with_locale というメソッド使うと、一部の内容がダブって出力されるというものです。

最初、globalize3の問題かと思いましたが、調べてみると、Rails3の問題のようです。ビュー内でブロックを使うHelperメソッドを使うと、そのブロックまでの内容が二重に出力されます。

以下の手順で再現できます。(Rails 3.0.3で確認)

新規アプリケーション作成

$ rails new just_yield
$ cd just_yield

コントローラを追加

$ rails g controller welcome index
$ rm public/index.html

config/routes.rbに以下を追加

root :to => "welcome#index"


app/helpers/application_helper.rb に、yieldを行なうだけのHelperメソッドを追加

module ApplicationHelper
  def just_yield
    yield
  end
end

ビューを以下のように変更

<h1>Welcome#index</h1>

A
<% just_yield do %>
B
<% end %>
C

サーバを起動して、http://localhost:3000/にアクセスすると、以下の出力になります。

<!DOCTYPE html>
<html>
<head>
  <title>JustYield</title>
  
  <script src="/javascripts/prototype.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/effects.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/dragdrop.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/controls.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/rails.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/application.js?1290752743" type="text/javascript"></script>
  <meta name="csrf-param" content="authenticity_token"/>
<meta name="csrf-token" content="Dow8TXbSPVQh9azlacoLXSTrlOTnfsdJIuJvgfJP8EQ="/>
</head>
<body>

<h1>Welcome#index</h1>

A
B
<h1>Welcome#index</h1>

A
B
C




</body>
</html>

つまり、ビューの先頭から、ブロックの終わりまでが二重に出力されます。

ビューを以下ように修正して、just_yieldの評価結果を変更すると正常に出力されます。

<h1>Welcome#index</h1>

A
<% just_yield do %>
B
<% 1; end %>
C

出力結果

<!DOCTYPE html>
<html>
<head>
  <title>JustYield</title>

  <script src="/javascripts/prototype.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/effects.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/dragdrop.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/controls.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/rails.js?1290752743" type="text/javascript"></script>
<script src="/javascripts/application.js?1290752743" type="text/javascript"></script>
  <meta name="csrf-param" content="authenticity_token"/>
<meta name="csrf-token" content="Dow8TXbSPVQh9azlacoLXSTrlOTnfsdJIuJvgfJP8EQ="/>
</head>
<body>

<h1>Welcome#index</h1>

A
B
C




</body>
</html>

つまり、ビュー内で使用するHelperメソッドが、yieldの結果をそのままリターンすると、二重の出力になるようです。

さらにこれの原因を調べてみると、do ... end を含むERBソースは、次のようにコンパイルされます。

@output_buffer.append_if_string=  just_yield do
  @output_buffer.safe_concat('B\n');
end

つまり、<%= ... %> ではなくて、<% ... %>を使っているのに、その式の評価結果を出力結果として使用するようです。このメソッドの実装は以下の所です。

gems/actionpack-3.0.3/lib/action_view/template/handlers/erb.rb

    def append_if_string=(value)
      if value.is_a?(String) && !value.is_a?(NonConcattingString)
        ActiveSupport::Deprecation.warn("<% %> style block helpers are deprecated. Please use <%= %>", caller)
        self << value
      end
    end

この Deprecation.warn は、確かに上記の問題が発生する時に、出力されています。

後方互換性のために、ブロックを伴う式の評価結果が文字列だと、それが出力されるようにしてある、ということのようです。

結論として、ブロックを伴う Helperメソッドの実装を変更できる場合には、

module ApplicationHelper
  def just_yield
    yield
    1 # 文字列を返却するとテンプレートに出力されてしまうので、文字列以外を返す
  end
end

とすればよくて、実装が変更しにくい場合はビューの方を下記のように変更すれば、良いと思います。

<h1>Welcome#index</h1>

A
<% just_yield do %>
B
<% 1 # ブロックが文字列を返却するとテンプレートに出力されてしまうので、文字列以外を返す
   end %>
C