ぐらめぬ・ぜぷつぇんのはてダ(2007 to 2011)

2007年~2011年ごろまで はてなダイアリー に書いてた記事を引っ越してきました。

script/generateで作られるビューファイルの拡張子が、Rails2.0では".html.erb"になる件

会社の同僚が・・・というか自分も含めて、今回のお仕事ではRailsを使うことになったわけだが。
本を読んでまじめに勉強している同僚が、「ビューの拡張子が.html.erbとかになってるけど、本とかWebの記事だと.rhtmlになってる。なんで?」
とか言ってきたので調べてみる。

結論としては以下のURLにあるとおり、Rails2.0以降では複数のビューエンジンに対応できるようにする為、拡張子でもってテンプレートエンジンを切り替えられる構造にしている。で、現行のデフォルトがerb。
ともあれ、そのためにscaffoldやcontrollerでの生成でデフォルトが".html.erb"に切り替えられたようである。

第4回 テンプレートファイルの拡張子 - Ruby on Rails 2.0 日記 - Ruby on Rails with OIAX

参考:
ActionView::Base

以下は、なんか説明だけじゃ満足できなかったのでソースを追った課程。
まず今日のエントリにもあるとおり、自分はローカルの環境にRailsの1.2.6を入れている。ということで、1.2.6の方のViewがどうなっているかあたってみた。
2.0.2の方もあるのでかなり端折ります。
actionpack-1.13.6/lib/action_view/base.rb:

require 'erb' # ←eRubyのPureRuby実装であるerbを使用している。

module ActionView #:nodoc:
#...
  class Base # ←ActionView::Base
    include ERB::Util # ←やっぱりerbだ!
#...
    def render(...)
      #...
          render_template(options[:type] || :rhtml, options[:inline], nil, options[:locals] || {})
          # ↑options[:type]のデフォルトが:rhtmlになっている。恐らくここから始まる。
      #...
    end

    def render_template(template_extension, ...)
      #...
        # ↓render_templateでoptions[:type]で指定されたのがtemplate_extentsion.
        compile_and_render_template(template_extension, template, file_path, local_assigns)
      #...
    end
    def compile_and_render_template(extension, ...)
      #...
        compile_template(extension, template, file_path, local_assigns)
        # ↑まだまだ続く・・・
      #...
    end
#...
    private
#...
      def create_template_source(extension, template, render_symbol, locals)
        # ↓ようやく発見
        if template_requires_setup?(extension)
          body = case extension.to_sym
            when :rxml
              "controller.response.content_type ||= 'application/xml'\n" +
              "xml ||= Builder::XmlMarkup.new(:indent => 2)\n" +
              template +
              "\nxml.target!\n"
            when :rjs
              "controller.response.content_type ||= 'text/javascript'\n" +
              "update_page do |page|\n#{template}\nend"
          end
        else
          body = ERB.new(template, nil, @@erb_trim_mode).src
        end
        #...
      end

・・・という具合に、結局ファイルの拡張子で2種類+それ以外はERBと処理を振り分けているのが分かった。

  • .rxml → Builder::XmlMarkupが処理する。
  • .rjs → うー・・・よく分からないんだけど、JavaScript
  • それ以外(含.rhtml) → ERBに任せる。

ちなみに、ではscript/generatorをcontroller+action名付で実行された時に、rails-1.2.6でaction名.rhtmlを生成しているのはどこかというと
rails-1.2.6/lib/rails_generator/generators/components/controller/controller_generator.rb:

class ControllerGenerator < Rails::Generator::NamedBase
  def manifest
    record do |m|
      #...
      # View template for each action.
      actions.each do |action|
        path = File.join('app/views', class_path, file_name, "#{action}.rhtml")
        m.template 'view.rhtml', path,
          :assigns => { :action => action, :path => path }
      end
    end
  end
end

という形で、".rhtml"というのを生成している箇所を突き止めることができた。

続いてrails-2.0.2の方を行きます。こちらも同様に、ActionView::Baseクラスから始まります。
rails-2.0.2/vendor/rails/actionpack/lib/action_view/base.rb:

# base.rbにはERBのrequireは見あたらない・・・。
module ActionView #:nodoc:
#...
  class Base # ←ActionView::Base
#...
    def render(...)
      #...
          render_template(options[:type], options[:inline], nil, options[:locals])
      #...
    end
    def render_template(template_extension, template, file_path = nil, local_assigns = {}) #:nodoc:
      handler = self.class.handler_for_extension(template_extension)

      if template_handler_is_compilable?(handler)
        compile_and_render_template(handler, template, file_path, local_assigns)
      else
        template ||= read_template_file(file_path, template_extension) # Make sure that a lazyily-read template is loaded.
        delegate_render(handler, template, local_assigns)
      end
    end

ここで一旦止まり、handler入れる値のhandler_for_extensionを見てみます。引数はテンプレートの拡張子っぽいです。
同じクラス内に定義されています。

    def self.handler_for_extension(extension)
      (extension && @@template_handlers[extension.to_sym]) || @@default_template_handlers
    end

クラス変数のtemplate_handlersのハッシュに、拡張子をキーで引き当てています。無ければdefault_template_handlersクラス変数が返されています。
すると、このdefのすぐ下に見つかります。

    register_default_template_handler :erb, TemplateHandlers::ERB
    register_template_handler :rjs, TemplateHandlers::RJS
    register_template_handler :builder, TemplateHandlers::Builder

    # TODO: Depreciate old template extensions
    register_template_handler :rhtml, TemplateHandlers::ERB
    register_template_handler :rxml, TemplateHandlers::Builder

見つけました。.erbや.rhtmlなどに応じたTemplateHandlersのクラスが返されています。少し戻って、render_templateメソッドでこれによりTemplateHandlersのクラス名がセットされたhandler変数は、この後長い旅をしてActionView::Base#delegate_compileメソッドでようやく実行されます。

      def delegate_compile(handler, template)
        handler.new(self).compile(template)
      end

さて、ではTemplateHandlers::ERBはどこかというと
rails-2.0.2/vendor/rails/actionpack/lib/action_view/template_handlers/erb.rb:

require 'erb'

class ERB
  module Util
    HTML_ESCAPE = { '&' => '&amp;', '"' => '&quot;', '>' => '&gt;', '<' => '&lt;' }

    def html_escape(s)
      s.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }
    end
  end
end

module ActionView
  module TemplateHandlers
    class ERB < TemplateHandler
      def compile(template)
        ::ERB.new(template, nil, @view.erb_trim_mode).src
      end
    end
  end
end

という質素な作りになっています。ここでようやくピンポイントでerbを使うようになりました。

そしてscript/generatorの方も・・・

#rails-2.0.2/vendor/rails/railties/lib/rails_generator/
#    generators/components/controller/controller_generator.rb
#

class ControllerGenerator < Rails::Generator::NamedBase
#...
      actions.each do |action|
        path = File.join('app/views', class_path, file_name, "#{action}.html.erb")
#...
end

ようやく見つけました。rails-1.2.6と同じ箇所で、単純に拡張子を".html.erb"にしています。

長い旅でしたが、ようやくRailsの1.2系と2.0系でのViewの変更点の一部、デフォルトの拡張子の変更箇所を特定できました。

( ´ー`)フゥー...