LoginSignup
127

More than 5 years have passed since last update.

Railsはどのようにテンプレートを見つけているか

Last updated at Posted at 2015-12-31

これは何?

構成

この後の構成は以下のとおりです。

  • はじめに
  • 1章 オプションのノーマライズ
  • 2章 オプションよりレンダー
  • 3章 レンダラーによるレンダー
  • 4章 オプションよりテンプレートパスを見つける
  • まとめ

はじめに

Railsアプリケーションでコントローラアクションにリクエストがあった時、Railsがどのようにレンダーするテンプレートを見つけているか疑問に思ったことはありませんか?例えばArticlesControllerindexアクションにリクエストがあった時、通常テンプレートapp/views/articles/index.html.erbが選ばれ、レンダーされます。最近、Railsのコードを読んでいてますが、今回、皆さんと一緒にActionPackActionViewのソースコードを見ていきたいと思います。

まず最初の章ではrenderがどのように機能しているのかを見ていきます。Railsのバージョンは4.2を想定していることに留意してください。もし違うバージョンを見ている場合多少実装が異なるかもしれません。

1章 オプションのノーマライズ

レンダーのエントリーポイントはAbstractController::Rendering#renderメソッドです。AbstractControllerActionControllerActionMailerにおいて共通で取り込まれているモジュールです。その2つのモジュールは多くの機能性が共通しているため、それら共通の機能性をAbstractControllerとして切り出しています。

rails/actionpack/lib/abstract_controller/rendering.rb
module AbstractController

  module Rendering

    # Normalize arguments, options and then delegates render_to_body and
    # sticks the result in self.response_body.
    # :api: public
    def render(*args, &block)
      options = _normalize_render(*args, &block)
      self.response_body = render_to_body(options)
      _process_format(rendered_format, options) if rendered_format
      self.response_body
    end
  end

end

コントローラでは、レンダーメソッドを直接呼ぶことができます。例えば、render 'new'でテンプレートnew.html.erbをレンダーすることができます。または、明示的にrenderメソッドを呼ばない場合、デフォルトレンダラーとしてActionController::ImplicitRenderが呼ばれます。

rails/actionpack/lib/action_controller/metal/implicit_renderer.rb
module ActionController
  module ImplicitRender
    def send_action(method, *args)
      ret = super
      default_render unless performed?
      ret
    end

    def default_render(*args)
      render(*args)
    end
  end
end

send_actionメソッドはコントローラアクションがトリガーとなって呼ばれます。まず最初にsuperを呼び、次にもしアクションで何もレンダーされなかった場合にperformed?メソッドがfalseを返します。そして見ていただけるとわかる通りdefault_renderメソッドが呼ばれます。default_renderが呼ばれると、単にrenderメソッドが呼ばれます。

AbstractController::Rendering#renderメソッドでは_normalize_renderメソッドが呼ばれ、次にrender_to_bodyメソッドが呼ばれます。_normalize_renderはオプションHashが返ってきます。この章では_normalize_renderメソッドがどのように生成されるかを確認していきます。

_normalize_renderがどのように実装されているかを見ていきましょう。

rails/actionpack/lib/abstract_controller/rendering.rb
module AbstractController

  module Rendering

    private

    # Normalize args and options.
    # :api: private
    def _normalize_render(*args, &block)
      options = _normalize_args(*args, &block)
      #TODO: remove defined? when we restore AP <=> AV dependency
      if defined?(request) && request && request.variant.present?
        options[:variant] = request.variant
      end
      _normalize_options(options)
      options
    end

  end

end

_normalize_args_normalize_optionsが呼ばれていることが分かります。_normalize_args_normalize_optionsは違った目的があります。

_normalize_args

_normalize_argsはすべての引数をオプションハッシュに変換します。例えば、renderメソッドを以下のように呼ぶことができます。

render 'new', status: :ok

ここで第一引数であるnewが文字列であり、_normalize_argsはオプションハッシュにこの第一引数を取り込み、適切なキーをあてるという責務があります。

それでは実装を見ていきましょう。

rails/actionpack/lib/abstract_controller/rendering.rb
module AbstractController

  module Rendering

    # Normalize args by converting render "foo" to render :action => "foo" and
    # render "foo/bar" to render :file => "foo/bar".
    # :api: plugin
    def _normalize_args(action=nil, options={})
      if action.is_a? Hash
        action
      else
        options
      end
    end

  end

end

通常、このメソッドは全く何もしないということが分かります。もしactionがハッシュであればactionを返し、もしそうでなく例えばactionが文字列であればオプションハッシュである第二引数を返します。

ApplicationControllerはこのメソッドをオーバーライドするいくつかのモジュールもインクルードすることに注意してください。その点については後ほど見ていきます。

_normalize_options

_normalize_optionsメソッドはモジュールが他のオプションを取り込むためにあります。Railsアプリケーションでは、ApplicationControllerActionController::Baseを継承し、ActionController::Baseは多くのモジュールをインクルードし、各モジュールはこのメソッドをオーバーライドして他のオプションを追加しています。

まずはじめにAbstractController::Renderingでどのようにこのメソッドが実装されているのかを確認していきましょう。

rails/actionpack/lib/abstract_controller/rendering.rb
module AbstractController

  module Rendering

    # Normalize options.
    # :api: plugin
    def _normalize_options(options)
      options
    end

  end

end

通常このメソッドは何もしませんが、他のモジュールでオーバーライドされます。

Override _normalize_args

Railsのソースコードは複雑で、その一つの理由として多くのモジュールが他のモジュールのメソッドをオーバーライドすることにあります。例としてArticlesControllerの先祖を見ていきましょう。

$ rails c
Loading development environment (Rails 4.2.0)
2.0.0-p353 :001 > puts ArticlesController.ancestors
ArticlesController
#<Module:0x007f8f0a971800>
ApplicationController
#<Module:0x007f8f0a8f93c8>
#<Module:0x007f8f05465118>
#<Module:0x007f8f05465140>
ActionController::Base
WebConsole::ControllerHelpers
Turbolinks::XHRHeaders
Turbolinks::Cookies
Turbolinks::XDomainBlocker
Turbolinks::Redirection
ActiveRecord::Railties::ControllerRuntime
ActionDispatch::Routing::RouteSet::MountedHelpers
ActionController::ParamsWrapper
ActionController::Instrumentation
ActionController::Rescue
ActionController::HttpAuthentication::Token::ControllerMethods
ActionController::HttpAuthentication::Digest::ControllerMethods
ActionController::HttpAuthentication::Basic::ControllerMethods
ActionController::DataStreaming
ActionController::Streaming
ActionController::ForceSSL
ActionController::RequestForgeryProtection
ActionController::Flash
ActionController::Cookies
ActionController::StrongParameters
ActiveSupport::Rescuable
ActionController::ImplicitRender
ActionController::MimeResponds
ActionController::Caching
ActionController::Caching::Fragments
ActionController::Caching::ConfigMethods
AbstractController::Callbacks
ActiveSupport::Callbacks
ActionController::EtagWithTemplateDigest
ActionController::ConditionalGet
ActionController::Head
ActionController::Renderers::All
ActionController::Renderers
ActionController::Rendering
ActionView::Layouts
ActionView::Rendering
ActionController::Redirecting
ActionController::RackDelegation
ActiveSupport::Benchmarkable
AbstractController::Logger
ActionController::UrlFor
AbstractController::UrlFor
ActionDispatch::Routing::UrlFor
ActionDispatch::Routing::PolymorphicRoutes
ActionController::ModelNaming
ActionController::HideActions
ActionController::Helpers
AbstractController::Helpers
AbstractController::AssetPaths
AbstractController::Translation
AbstractController::Rendering
ActionView::ViewPaths
ActionController::Metal
AbstractController::Base
ActiveSupport::Configurable
Object
ActiveSupport::Dependencies::Loadable
PP::ObjectMixin
JSON::Ext::Generator::GeneratorMethods::Object
Kernel
BasicObject

よくあるコントローラを見てみるとわかる通り、多くの先祖を持っていて、さらにほとんどがモジュールであることが分かります。AbstractController::Rendering以降のすべてのモジュールがこのメソッドをオーバーライドする可能性があります。そこでメソッドを実装している先祖を確認するgistを作成しました。1

class Module
  def ancestors_that_implement_instance_method(instance_method)
    ancestors.find_all do |ancestor|
      (ancestor.instance_methods(false) + ancestor.private_instance_methods(false)).include?(instance_method)
    end
  end
end

上記コードをrailsコンソールで実行するとClassName.ancestors_that_implement_instance_methodを呼ぶことでメソッドが実装された先祖を確認することができます。

それでは_normalize_argsメソッドをオーバーライドしている先祖を見ていきましょう。

$ rails c
Loading development environment (Rails 4.2.0)
2.0.0-p353 :013 >   ArticlesController.ancestors_that_implement_instance_method(:_normalize_args)
 => [ActionController::Rendering, ActionView::Rendering, AbstractController::Rendering]

ActionView::RenderingActionController::Renderingの2つのモジュールがインスタンスメソッドをオーバーライドしています。上から下の順に確認していきましょう。

はじめにActionView::Renderingを見ていきます。

actionview/lib/action_view/rendering.rb
module ActionView
  module Rendering
    # Normalize args by converting render "foo" to render :action => "foo" and
    # render "foo/bar" to render :template => "foo/bar".
    # :api: private
    def _normalize_args(action=nil, options={})
      options = super(action, options)
      case action
      when NilClass
      when Hash
        options = action
      when String, Symbol
        action = action.to_s
        key = action.include?(?/) ? :template : :action
        options[key] = action
      else
        options[:partial] = action
      end

      options
    end
  end
end

第一引数actionに注目すると、/を含む文字列の場合にactionに対してのキーは:templateとなり、/を含まない文字列の場合にactionに対してのキーは:actionになります。

そのため、render 'new'と呼ぶときオプションは{ action: ‘new’ }となり、render 'articles/new'と呼ぶときオプションは{ template: ‘articles/new’ }となります。

さて次にこのメソッドをオーバーライドするActionController::Renderingを見ていきます。

actionpack/lib/action_controller/metal/rendering.rb
module ActionController
  module Rendering
    # Normalize arguments by catching blocks and setting them on :update.
    def _normalize_args(action=nil, options={}, &blk) #:nodoc:
      options = super
      options[:update] = blk if block_given?
      options
    end
  end
end

このオーバーライドではブロックが渡ってくるとき、そのブロックをoptionss[:update]にセットしていることがわかります。

Override _normalize_options

_normalize_argsのように_normalize_optionsをオーバーライドするモジュールを調べていきましょう。オーバーライドするモジュールが[ActionController::Rendering, ActionView::Layouts, ActionView::Rendering, AbstractController::Rendering]であることがわかります。

はじめにActionView::Renderingを見ていきましょう。

actionpack/lib/action_controller/metal/rendering.rb
module ActionView
  module Rendering

    # Normalize options.
    # :api: private
    def _normalize_options(options)
      options = super(options)
      if options[:partial] == true
        options[:partial] = action_name
      end

      if (options.keys & [:partial, :file, :template]).empty?
        options[:prefixes] ||= _prefixes
      end

      options[:template] ||= (options[:action] || action_name).to_s
      options
    end
  end
end

通常、3つのオプションを追加していることが分かります。

options[:partial]がtrueであった場合、options[:partial]action_nameがセットされます。action_nameメソッドはトリガーになったアクションの名前が返ります。
例えばArticlesControllerindexアクションでトリガーした場合には、action_nameindexになります。
もし:partial:file:templateオプションがどれも指定されていない場合、options[:prefixes]がセットされます。この:prefixesに何がセットされるかは後で確認することにしましょう。次にoptions[:template]がセットされます。これは引数から渡されるかまたはaction_nameが使われます。
それではoptions[:prefixes]の中身が何か、_prefixesメソッドが実装を確認しましょう。

AbstractController::RenderingモジュールはActionView::ViewPathsモジュールをインクルードしており、_prefixesメソッドはそこで実装されています。

ActionView::ViewPathsは後ほどでも詳細を見ていく重要なモジュールです。このモジュールはコントローラでビューパスを管理するためのものです。例えば、通常Railsはビューパス#{Rails.root}app/viewsを追加することによって、アプリケーションはその特定のビューパスでテンプレートを検索するということを知ります。

それでは_prefixesメソッドに注目していきましょう。

rails/actionview/lib/action_view/view_paths.rb
module ActionView

  module ViewPaths

    # The prefixes used in render "foo" shortcuts.
    def _prefixes # :nodoc:
      self.class._prefixes
    end

  end

end

_prefixesメソッドはクラスメソッドの_prefixesを呼んでいるだけです。

rails/actionview/lib/action_view/view_paths.rb
module ActionView

  module ViewPaths
    module ClassMethods
      def _prefixes # :nodoc:
        @_prefixes ||= begin
          return local_prefixes if superclass.abstract?

          local_prefixes + superclass._prefixes
        end
      end

      private

      # Override this method in your controller if you want to change paths prefixes for finding views.
      # Prefixes defined here will still be added to parents' <tt>._prefixes</tt>.
      def local_prefixes
        [controller_path]
      end

    end
  end

end

Rails 4.2ではこのメソッドが実装されており、非推奨の親クラスのプレフィックスを処理していることに注意してください。上記のコードでは分かりやすくするためにその処理を除外しています。

このActionView::ViewPaths._prefixesは再帰的に呼ばれます。これは親クラスの_prefixeslocal_prefixesを追加しています。local_prefixesは唯一の要素controller_pathを持つ配列を返します。

controller_pathメソッドはとても単純で、AbstractController::Baseで実装されています。例えばArticlesControllerにおいてcontroller_patharticlesを返し、Articles::CommentsControllerにおいてcontroller_patharticles/commentsを返します。

つまり、_prefixesメソッドは最初に親のプレフィックスを取得して、次に現在のcontroller_pathを先頭に追加します。

例えば、アプリケーションにArticlesControllerがある場合、以下のコードで_prefixesを表示できます。

$ rails c
Loading development environment (Rails 4.2.0)
2.0.0-p353 :001 > ArticlesController.new.send(:_prefixes)
 => ["articles", "application"]

プレフィックスとしてarticlesapplicationの2つがあることが分かります。ApplicationControllerActionController::Baseを継承しており、ActionController::Baseabstractです。

なので、ArticlesController#indexアクションで引数なしでレンダーした場合にオプションは次の通りになります。

:prefixes : array [“articles”, “application”]
:template : string “index”

次にActionView::Layoutsがどのように_normalize_optionsメソッドをオーバーライドしているかを見ていきましょう。

rails/actionview/lib/action_view/layouts.rb
module ActionView
  module Layouts

    def _normalize_options(options) # :nodoc:
      super

      if _include_layout?(options)
        layout = options.delete(:layout) { :default }
        options[:layout] = _layout_for_option(layout)
      end
    end

  end
end

options[:layout]がセットされていない場合デフォルトのlayout:defaultです。そして_layout_for_optionの戻り値がoptions[:layout]にセットされます。

もし興味があれば_layout_for_optionsの実装を確認しても良いでしょう。このモジュールがレイアウトを探索するとき、最初に“app/views/layouts/#{class_name.underscore}.rb”を探索し、見つからない場合に親クラスを探索します。railsアプリケーションが作成されたときapplication.html.erbapp/views/layoutsディレクトリに作成されますが、デフォルトで全てのコントローラの親がApplicationControllerであるためにこのレイアウトがデフォルトで使用されるのです。

最後にActionController::Renderingがどのように_normalize_optionsをオーバーライドしているかを確認していきましょう。

rails/actionpack/lib/action_controller/metal/rendering.rb
module ActionController
  module Rendering

    # Normalize both text and status options.
    def _normalize_options(options) #:nodoc:
      _normalize_text(options)

      if options[:html]
        options[:html] = ERB::Util.html_escape(options[:html])
      end

      if options.delete(:nothing)
        options[:body] = nil
      end

      if options[:status]
        options[:status] = Rack::Utils.status_code(options[:status])
      end

      super
    end

  end
end

このメソッドは:html:nothing:statusオプションを処理する簡単なものです。

そして最後にArticlesController#indexを引数なしで呼んだ場合のオプションが以下の値になることがわかります。

:prefixes : array [“articles”, “application”]
:template : string “index”
:layout : proc (呼んだ時に“app/views/layouts/application.html.erb”が返る)

さて、オプションがどのようにノーマライズされるかを見てきました。Railsはどのテンプレートをレンダーするべきかを特定する時にオプションから詳細を抽出します。後の章ではRailsがどのようにテンプレートを特定しているかを調べていきます。

2章 オプションよりレンダー

前章ではRailsがテンプレートを探索するにあったって_normalize_renderメソッドによりオプションハッシュをつくるところまでを紹介してきました。そしてrenderメソッドではrender_to_bodyへ作成したオプションハッシュを渡します。

rails/actionpack/lib/abstract_controller/rendering.rb
module AbstractController

  module Rendering

    # Normalize arguments, options and then delegates render_to_body and
    # sticks the result in self.response_body.
    # :api: public
    def render(*args, &block)
      options = _normalize_render(*args, &block)
      self.response_body = render_to_body(options)
      _process_format(rendered_format, options) if rendered_format
      self.response_body
    end
  end

end

render_to_bodyはオプションハッシュの値をもとにテンプレートを選択します。AbstractController::Rendering#render_to_bodyを見てみると何もありません。例のごとく他のモジュールがオーバーライドしているのです。

rails/actionpack/lib/abstract_controller/rendering.rb
module AbstractController

  module Rendering

    # Performs the actual template rendering.
    # :api: public
    def render_to_body(options = {})
    end

  end

end

前章の通りApplicationController.ancestors_that_implement_instance_methodを実行してメソッドが実装されているクラスまたはモジュールを見つけていきましょう。

2.0.0-p353 :008 > ApplicationController.ancestors_that_implement_instance_method(:render_to_body)
 => [ActionController::Renderers, ActionController::Rendering, ActionView::Rendering, AbstractController::Rendering]

ActionController::RenderersActionController::RenderingActionView::Renderingで実装されていることが分かります。一つずつ見ていきましょう。

ActionController::Renderers#render_to_body

ActionController::Renderers#render_to_bodyではレンダラーを登録しており、オプションがレンダラーのキーを持っていればレンダラーを呼びます。もしレンダラーが見つからない場合は、superを呼びます。

rails/actionpack/lib/action_controller/metal/renderers.rb
module ActionController

  module Renderers

    def render_to_body(options)
      _render_to_body_with_renderer(options) || super
    end

    def _render_to_body_with_renderer(options)
      _renderers.each do |name|
        if options.key?(name)
          _process_options(options)
          method_name = Renderers._render_with_renderer_method_name(name)
          return send(method_name, options.delete(name), options)
        end
      end
      nil
    end

  end

end

これは主にrenderに:json:xmlのようなパラメータを渡す場合のためのもので、例えば以下のようなコードです。

class ArticlesController < ApplicationController

  def index
    @articles = Articles.all
    render json: @articles
  end
end

:jsonActionController::Renderersで登録されたレンダラーとなるので、このレンダーが呼ばれます。ActionController::Renderers.addでオリジナルのレンダラーを追加することもできます。

ActionController::Rendering#render_to_body

ActionController::Renderers#render_to_bodyでレンダラーが見つからない場合superが呼ばれますが、これはActionController::Rendering#render_to_bodyです。このメソッドで何をするかを確認していきましょう。

rails/actionpack/lib/action_controller/metal/rendering.rb
module ActionController

  module Rendering

    RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html]

    def render_to_body(options = {})
      super || _render_in_priorities(options) || ' '
    end

    private

    def _render_in_priorities(options)
      RENDER_FORMATS_IN_PRIORITY.each do |format|
        return options[format] if options.key?(format)
      end

      nil
    end

  end

end

このメソッドはsuperを先に呼びますが、superが何も返さない場合のみ_render_in_prioritiesが呼ばれることに注目してください。

_render_in_prioritiesではRENDER_FORMATS_IN_PRIORITYを一つずつ探していき、フォーマットを見つけた場合にそのオプション値を返します。

このモジュールでsuperを呼ぶとActionView::Rendering#render_to_bodyが呼ばれます。見ていきましょう。

ActionView::Rendering#render_to_body

rails/actionview/lib/action_view/rendering.rb
module ActionView

  module Rendering

    def render_to_body(options = {})
      _process_options(options)
      _render_template(options)
    end

    # Returns an object that is able to render templates.
    # :api: private
    def view_renderer
      @_view_renderer ||= ActionView::Renderer.new(lookup_context)
    end

    private

      # Find and render a template based on the options given.
      # :api: private
      def _render_template(options) #:nodoc:
        variant = options[:variant]

        lookup_context.rendered_format = nil if options[:formats]
        lookup_context.variants = variant if variant

        view_renderer.render(view_context, options)
      end

  end

end

これは探していた肝心な部分です。render_to_body_render_templateを呼び、_render_templateview_renderer.render(view_context, options)を呼びます。

view_rendererActionView::Rendererのインスタンスで、初期化時にActionView::LookupContextのインスタンスであるlookup_contextを渡します。ActionView::LookupContextはオプションに基づきテンプレートを探すための情報全てを保持します。さて次章ではこのクラスを詳しく見ていき、LookupContextViewPathsPathSetがどのように組み合わさってテンプレートを探すのかを調べていきましょう。

3章 レンダラーによるレンダー

前章ではテンプレートはActionView::Renderer#renderメソッドでレンダーされることを見てきました。そしてこの章ではこのクラスを詳しく見ていきましょう。

それでは見ていきましょう。

ActionView::Renderer

rails/actionview/lib/action_view/renderer/renderer.rb
module ActionView

  # This is the main entry point for rendering. It basically delegates
  # to other objects like TemplateRenderer and PartialRenderer which
  # actually renders the template.
  #
  # The Renderer will parse the options from the +render+ or +render_body+
  # method and render a partial or a template based on the options. The
  # +TemplateRenderer+ and +PartialRenderer+ objects are wrappers which do all
  # the setup and logic necessary to render a view and a new object is created
  # each time +render+ is called.
  class Renderer
    attr_accessor :lookup_context

    def initialize(lookup_context)
      @lookup_context = lookup_context
    end

    # Main render entry point shared by AV and AC.
    def render(context, options)
      if options.key?(:partial)
        render_partial(context, options)
      else
        render_template(context, options)
      end
    end

    # Direct accessor to template rendering.
    def render_template(context, options) #:nodoc:
      TemplateRenderer.new(@lookup_context).render(context, options)
    end
  end
end

このクラスはActionView::LookupContextクラスのlookup_contextを引数にとって初期化されます。renderメソッドが呼ばれた時に通常のテンプレートではオプションに:partialキーを持たないためrender_templateが呼ばれます。render_templateメソッドはTemplateRendererクラスのインスタンスを作成しrenderメソッドを作成します。

それではTemplateRendererクラスを見てみましょう。

rails/actionview/lib/action_view/renderer/template_renderer.rb
module ActionView

  class TemplateRenderer < AbstractRenderer

    def render(context, options)
      @view    = context
      @details = extract_details(options)
      template = determine_template(options)

      prepend_formats(template.formats)

      @lookup_context.rendered_format ||= (template.formats.first || formats.first)

      render_template(template, options[:layout], options[:locals])
    end

    private

    # Determine the template to be rendered using the given options.
    def determine_template(options)
      keys = options.has_key?(:locals) ? options[:locals].keys : []

      if options.key?(:body)
        Template::Text.new(options[:body])
      elsif options.key?(:text)
        Template::Text.new(options[:text], formats.first)
      elsif options.key?(:plain)
        Template::Text.new(options[:plain])
      elsif options.key?(:html)
        Template::HTML.new(options[:html], formats.first)
      elsif options.key?(:file)
        with_fallbacks { find_template(options[:file], nil, false, keys, @details) }
      elsif options.key?(:inline)
        handler = Template.handler_for_extension(options[:type] || "erb")
        Template.new(options[:inline], "inline template", handler, :locals => keys)
      elsif options.key?(:template)
        if options[:template].respond_to?(:render)
          options[:template]
        else
          find_template(options[:template], options[:prefixes], false, keys, @details)
        end
      else
        raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :text or :body option."
      end
    end
  end
end

このクラスはActionView::AbstractRendererクラスを継承しており、テンプレートはdeterminate_templateメソッドによって決定され、このメソッドでは例えば:body:textといったキーがオプションハッシュにあるかどうかを検証します。第1章で学んだ通り:templateキーがない場合はoptions[:template]にアクション名が入ります。そしてoptions[:prefixes]は配列になります。例えば、ArticlesControllerindexアクションにアクセスがあった場合に:template及び:prefixesは以下のようになります。

:prefixes : array [“articles”, “application”]
:template : string “index”

そしてテンプレートはfind_templateメソッドによって見つけられます。このメソッドは親クラスのActionView::AbstractRendererで定義されていますので、このクラスを見ていきましょう。

ActionView::AbstractRenderer

rails/actionview/lib/action_view/renderer/abstract_renderer.rb
module ActionView

  class AbstractRenderer

    delegate :find_template, :template_exists?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context


    def initialize(lookup_context)
      @lookup_context = lookup_context
    end

    def render
      raise NotImplementedError
    end

    protected

    def extract_details(options)
      @lookup_context.registered_details.each_with_object({}) do |key, details|
        value = options[key]

        details[key] = Array(value) if value
      end
    end
  end

end

find_templateメソッドは実際にはlookup_contextに委任されており、このメソッドは後で確認しますが、find_templateメソッドが呼ばれた時にextract_detailsメソッドの戻り値であるdetailsオブジェクトが渡されることを覚えておいてください。

上記extract_detailsメソッドを確認すると、このメソッドがハッシュを返すことが分かります。lookup_contextからregistered_detailsを取得し、オプションがキーを持っているかを確認し、もしあればその組を持つハッシュを返します。(バリューは配列に変換されます)

それではActionView::LookupContext#registered_detailsの中身はどうなっているでしょうか。見ていきましょう。

ActionView::LookupContext#registered_details

rails/actionview/lib/action_view/lookup_context.rb
module ActionView

  # LookupContext is the object responsible to hold all information required to lookup
  # templates, i.e. view paths and details. The LookupContext is also responsible to
  # generate a key, given to view paths, used in the resolver cache lookup. Since
  # this key is generated just once during the request, it speeds up all cache accesses.
  class LookupContext #:nodoc:

    mattr_accessor :registered_details
    self.registered_details = []

    def self.register_detail(name, options = {}, &block)
      self.registered_details << name
      initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" }

      Accessors.send :define_method, :"default_#{name}", &block
      Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1
        def #{name}
          @details.fetch(:#{name}, [])
        end

        def #{name}=(value)
          value = value.present? ? Array(value) : default_#{name}
          _set_detail(:#{name}, value) if value != @details[:#{name}]
        end

        remove_possible_method :initialize_details
        def initialize_details(details)
          #{initialize.join("\n")}
        end
      METHOD
    end

    # Holds accessors for the registered details.
    module Accessors #:nodoc:
    end

    register_detail(:locale) do
      locales = [I18n.locale]
      locales.concat(I18n.fallbacks[I18n.locale]) if I18n.respond_to? :fallbacks
      locales << I18n.default_locale
      locales.uniq!
      locales
    end

    register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css,  :xml, :json] }
    register_detail(:variants) { [] }
    register_detail(:handlers){ Template::Handlers.extensions }
  end

end

detail nameの配列であるモジュール属性registered_detailsを持っていることがわかります。

例えば、railsコンソールでActionView::LookupContext.registered_detailsを呼ぶことでデフォルトのdetailsキーを見ることができます。

$ rails c
Loading development environment (Rails 4.2.0)
2.0.0-p353 :001 > ActionView::LookupContext.registered_details
 => [:locale, :formats, :variants, :handlers]

デフォルトでは:locale:formats:variants:handlersの4つのdetailsが登録されており、上記にあるようにregister_detailが4回呼ばれ、detailsキーが1つずつ渡されます。

同時にこのクラスはハッシュであるインスタンス変数@detailsも管理します。このハッシュはキーにdetailキーを取り、initialize_detailsによって初期化されたデフォルト値である配列をバリューに取ります。(initialize_detailsについては後で言及します)

register_detailが呼ばれるたびに幾つかのインスタンスメソッドがクラスに追加されます。例えば、:localeが渡ってきたとき、以下のメソッドを定義します。

default_locale : 渡されたブロックから返る結果であるデフォルトlocaleを返す
locale: インスタンスメソッド@detailsに保持しているlocale値を返す
locale=: @detailsにlocale値をセットする

そしてregistered_detailが呼ばれるたびにinitialize_detailsが再定義されます。例えば、初めてregister_detail(:locale)が呼ばれるときinitialize_detailsは以下のようになります。

module ActionView

  class LookupContext #:nodoc:

    def initialize_details(details)
      @details[:locale] = details[:locale] || default_locale
    end
  end

end

そしてregister_detail(:formats)が呼ばれると、initialize_detailsは以下のように再定義します。

module ActionView

  class LookupContext #:nodoc:

    def initialize_details(details)
      @details[:locale] = details[:locale] || default_locale
      @details[:formats] = details[:formats] || default_formats
    end
  end

end

initialize_detailsは登録されたdetailキーそれぞれに対してのdetailバリューを取得し、もしキーが見つからなければデフォルトのdetailバリューがセットされます。

ActionView::LookupContext#find_template

ActionView::LookupContext#find_templateメソッドを確認してみましょう。

rails/actionview/lib/action_view/lookup_context.rb
module ActionView

  class LookupContext

    include ViewPaths

    # Helpers related to template lookup using the lookup context information.
    module ViewPaths
      attr_reader :view_paths, :html_fallback_for_js

      # Whenever setting view paths, makes a copy so that we can manipulate them in
      # instance objects as we wish.
      def view_paths=(paths)
        @view_paths = ActionView::PathSet.new(Array(paths))
      end

      def find(name, prefixes = [], partial = false, keys = [], options = {})
        @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options))
      end
      alias :find_template :find

      def find_all(name, prefixes = [], partial = false, keys = [], options = {})
        @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
      end

      def exists?(name, prefixes = [], partial = false, keys = [], options = {})
        @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options))
      end
      alias :template_exists? :exists?

      # Adds fallbacks to the view paths. Useful in cases when you are rendering
      # a :file.
      def with_fallbacks
        added_resolvers = 0
        self.class.fallbacks.each do |resolver|
          next if view_paths.include?(resolver)
          view_paths.push(resolver)
          added_resolvers += 1
        end
        yield
      ensure
        added_resolvers.times { view_paths.pop }
      end

    protected

      def args_for_lookup(name, prefixes, partial, keys, details_options) #:nodoc:
        name, prefixes = normalize_name(name, prefixes)
        details, details_key = detail_args_for(details_options)
        [name, prefixes, partial || false, details, details_key, keys]
      end

      # Compute details hash and key according to user options (e.g. passed from #render).
      def detail_args_for(options)
        return @details, details_key if options.empty? # most common path.
        user_details = @details.merge(options)

        if @cache
          details_key = DetailsKey.get(user_details)
        else
          details_key = nil
        end

        [user_details, details_key]
      end

      # Support legacy foo.erb names even though we now ignore .erb
      # as well as incorrectly putting part of the path in the template
      # name instead of the prefix.
      def normalize_name(name, prefixes) #:nodoc:
        prefixes = prefixes.presence
        parts    = name.to_s.split('/')
        parts.shift if parts.first.empty?
        name     = parts.pop

        return name, prefixes || [""] if parts.empty?

        parts    = parts.join('/')
        prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]

        return name, prefixes
      end
    end
  end

end

find_templateはこのモジュールにインクルードしているモジュールActionView::LookupContext::ViewPathsActionView::LookupContextで実装されています。find_templatefindメソッドのエイリアスであることが分かります。findメソッドはActionView::PathSetのインスタンスである@view_pathsに委任します。そのためActionView::PathSet#findが何をしているのかを理解する必要があります。実際にはActionView::PathSet#findPathResolverの組みに対して探索を委任していますが、この点について次章で確認していきます。

4章 オプションよりテンプレートパスを見つける

前章ではActionView::LookupContext#find_templateは単にActionView::PathSet#findに委任していることがわかりました。それではActionView::PathSet#findが何をしているのかを確認しましょう。

ActionView::PathSet

PathSetというクラス名より、このクラスがテンプレートが保持されているパスの組みを管理するであろうことが分かります。さてこのクラスの定義を見ていきましょう。

rails/actionview/lib/action_view/path_set.rb
module ActionView #:nodoc:
  # = Action View PathSet
  #
  # This class is used to store and access paths in Action View. A number of
  # operations are defined so that you can search among the paths in this
  # set and also perform operations on other +PathSet+ objects.
  #
  # A +LookupContext+ will use a +PathSet+ to store the paths in its context.
  class PathSet #:nodoc:
    include Enumerable

    attr_reader :paths

    delegate :[], :include?, :pop, :size, :each, to: :paths

    def initialize(paths = [])
      @paths = typecast paths
    end

    def find(*args)
      find_all(*args).first || raise(MissingTemplate.new(self, *args))
    end

    def find_all(path, prefixes = [], *args)
      prefixes = [prefixes] if String === prefixes
      prefixes.each do |prefix|
        paths.each do |resolver|
          templates = resolver.find_all(path, prefix, *args)
          return templates unless templates.empty?
        end
      end
      []
    end

    def exists?(path, prefixes, *args)
      find_all(path, prefixes, *args).any?
    end

    private

    def typecast(paths)
      paths.map do |path|
        case path
        when Pathname, String
          OptimizedFileSystemResolver.new path.to_s
        else
          path
        end
      end
    end
  end
end

initializeメソッドは文字列配列であるpaths引数を受け取ることが分かります。そしてinitializeメソッドでは、typecastメソッドにpathsを渡し、pathsの各要素において型変換した値をインスタンス変数@pathsに保持します。typecastメソッドはOptimizedFileSystemResolverのインスタンスに変換します。
find_allメソッドはpathsの中の各resolverインスタンス対して繰り返しを実施し、空でない最初の値を返します。
findメソッドはfind_allメソッドに委任しているだけで、もしテンプレートが見つからない場合は例外MissingTemplateを発生させます。
LookupContextクラスでは、ActionView::PathSetがどのように生成されているのかを確認することができます。

rails/actionview/lib/action_view/lookup_context.rb
module ActionView
  class LookupContext

    # Helpers related to template lookup using the lookup context information.
    module ViewPaths
      attr_reader :view_paths

      # Whenever setting view paths, makes a copy so that we can manipulate them in
      # instance objects as we wish.
      def view_paths=(paths)
        @view_paths = ActionView::PathSet.new(Array(paths))
      end
    end

    include ViewPaths
  end
end

view_paths=メソッドはActionView::LookupContext::ViewPathsで定義されており、ActionView::LookupContextはこのモジュールをインクルードしています。view_paths=メソッドが呼ばれるとActionView::PathSetのインスタンスが生成され、ActionView::LookupContextのインスタンス変数@view_pathsにセットされます。

さて、どのようにview_paths=メソッドが呼ばれるのでしょうか?それはActionView::LookupContext#initializeメソッドの中にあります。

rails/actionview/lib/action_view/lookup_context.rb
module ActionView
  class LookupContext

    def initialize(view_paths, details = {}, prefixes = [])
      @details, @details_key = {}, nil
      @skip_default_locale = false
      @cache = true
      @prefixes = prefixes
      @rendered_format = nil

      self.view_paths = view_paths
      initialize_details(details)
    end

  end
end

view_pathsActionView::LookupContextインスタンスの初期化で渡されます。そして、ActionView::ViewPathsActionView::LookupContextを初期化します。

rails/actionview/lib/action_view/view_paths.rb
module ActionView
  module ViewPaths
    extend ActiveSupport::Concern

    included do
      class_attribute :_view_paths
      self._view_paths = ActionView::PathSet.new
      self._view_paths.freeze
    end

    # LookupContext is the object responsible to hold all information required to lookup
    # templates, i.e. view paths and details. Check ActionView::LookupContext for more
    # information.
    def lookup_context
      @_lookup_context ||=
        ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes)
    end

    def append_view_path(path)
      lookup_context.view_paths.push(*path)
    end

    def prepend_view_path(path)
      lookup_context.view_paths.unshift(*path)
    end

    module ClassMethods
      # Append a path to the list of view paths for this controller.
      #
      # ==== Parameters
      # * <tt>path</tt> - If a String is provided, it gets converted into
      #   the default view path. You may also provide a custom view path
      #   (see ActionView::PathSet for more information)
      def append_view_path(path)
        self._view_paths = view_paths + Array(path)
      end

      # Prepend a path to the list of view paths for this controller.
      #
      # ==== Parameters
      # * <tt>path</tt> - If a String is provided, it gets converted into
      #   the default view path. You may also provide a custom view path
      #   (see ActionView::PathSet for more information)
      def prepend_view_path(path)
        self._view_paths = ActionView::PathSet.new(Array(path) + view_paths)
      end

      # A list of all of the default view paths for this controller.
      def view_paths
        _view_paths
      end

  end
end

このActionView::ViewPathsはコントローラクラスにもインクルードされます。デフォルトではview_pathsは空配列です。しかし、append_view_pathprepend_view_pathがビューパスをビューパス配列の最初または最後に追加します。このprepend_view_path
append_view_pathはクラスメソッドとしてもインスタンスメソッドとしても定義されていることに注意してください。

ではビューパスはどこで初期化されているでしょうか?実はRails::Engineクラスなのです。

rails/railties/lib/rails/engine.rb
module Rails

  class Engine

    initializer :add_view_paths do
      views = paths["app/views"].existent
      unless views.empty?
        ActiveSupport.on_load(:action_controller){ prepend_view_path(views) if respond_to?(:prepend_view_path) }
        ActiveSupport.on_load(:action_mailer){ prepend_view_path(views) }
      end
    end

  end

end

イニシャライザーはアプリケーション開始時に呼び出されるブロックとして定義されています。:action_controllerが読み込まれた時にprepend_view_pathが呼ばれ、view_pathsはただ1つの要素であるapp/viewsディレクトリを含むことになります。

OptimizedFileSystemResolver

さて、ActionView::PathSetがどのように初期化されるのかを理解しましたが、実際のところresolverにfindメソッドを委任していることも知りました。なのでresolverでどのようにメソッドが実装されているのかを見ていきましょう。

resolver.png

Resolverは渡ってくるdetailsを受け、テンプレートを解決するためにあります。クラスの関係は上記図のとおりです。

通常、resolverはActionView::PathSetで使われ、ActionView::OptimizedFileSystemResolverです。このクラスはActionView::FileSystemResolverを継承し、さらにこのクラスはActionView::PathResolverを継承し、さらにこのクラスはActionView::Resolverを継承しており、このクラスはすべてのresolverの基底となっています。

We can see that by default the resolver used in ActionView::PathSet, which is ActionView::OptimizedFileSystemResolver, extends from ActionView::FileSystemResolver, which in terms extends from ActionView::PathResolver, which extends from ActionView::Resolver, which is the base class for all resolvers.

基底クラスのActionView::Resolverはすべてのサブクラスのresolverで使われるキャッシュのようないくつかの共通な機能性を提供しています。しかしここでは、ActionView::PathResolverにおいて、パスの中でテンプレートをどのように見つけるのかというロジックがどのように実装されているかについて注目していきましょう。

rails/actionview/lib/action_view/template/resolver.rb
module ActionView

  class PathResolver < Resolver

    EXTENSIONS = { :locale => ".", :formats => ".", :variants => "+", :handlers => "." }
    DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"

    def initialize(pattern=nil)
      @pattern = pattern || DEFAULT_PATTERN
      super()
    end

    private

    def find_templates(name, prefix, partial, details)
      path = Path.build(name, prefix, partial)
      query(path, details, details[:formats])
    end

    def query(path, details, formats)
      query = build_query(path, details)

      template_paths = find_template_paths query

      template_paths.map { |template|
        handler, format, variant = extract_handler_and_format_and_variant(template, formats)
        contents = File.binread(template)

        Template.new(contents, File.expand_path(template), handler,
          :virtual_path => path.virtual,
          :format       => format,
          :variant      => variant,
          :updated_at   => mtime(template)
        )
      }
    end

    def find_template_paths(query)
      Dir[query].reject { |filename|
        File.directory?(filename) ||
          # deals with case-insensitive file systems.
          !File.fnmatch(query, filename, File::FNM_EXTGLOB)
      }
    end

    # Helper for building query glob string based on resolver's pattern.
    def build_query(path, details)
      query = @pattern.dup

      prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
      query.gsub!(/\:prefix(\/)?/, prefix)

      partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
      query.gsub!(/\:action/, partial)

      details.each do |ext, variants|
        query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
      end

      File.expand_path(query, @path)
    end

    def escape_entry(entry)
      entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
    end
  end

end

もっとも重要なメソッドはbuild_queryfind_template_pathsです。

  • PathResolverはパス中のテンプレート探索のためにパターンを使用し、デフォルトパターンはDEFAULT_PATTERNです

  • build_query: このメソッドはパスやdetailsからクエリを作成します。前章であった通りActionView::AbstractRenderer#extract_detailsはオプションからdetailsを抽出します。例えば、Articlesコントローラのindexアクションの場合クエリは以下のようになります

articles/index{.en,}{.html,}{+:variants,}{.haml,}

このクエリでは、find_template_pathsDir[query]が呼ばれますが、これは実際のところglobのエイリアスで、クエリを展開します。例えば、上記クエリだと{p, q}pまたはqのどちらかがマッチします。なので例えば{.en,}の場合.enまたは空文字列にマッチします。テンプレートarticles/index.html.hamlを定義する場合このクエリにマッチします。詳細はRuby docを確認してください。

マッチした各テンプレートにおいてqueryメソッドではテンプレートファイルをラップするTemplateインスタンスを作成します。

まとめ

さて、まとめとしてRailsがテンプレートを見つける流れで分かったことは以下のとおりです。

  • テンプレートを解決するためにPathsの組みを初期化し、デフォルトではapp/viewsのみが含まれる
  • コントローラのアクションにアクセスがあった時、レンダーは明示的または暗黙的に呼ばれる
  • レンダーメソッドに渡されたオプションからactionやdetails、prefixesを抽出
  • prefixesやaction、detailsから構成されたクエリを用いてパスセットを検索することによってテンプレート探索する

  1. gistは元記事執筆者作成です。また、訳者がgem化していますのでそちらもご確認ください。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
127