Edited at

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

More than 1 year has passed since last update.


これは何?


構成

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


  • はじめに

  • 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化していますのでそちらもご確認ください。