これは何?
- Railsのテンプレート探索についてまとめた記事です
- Railsのバージョンは4.2です
- 本記事は以下の内容を翻訳したものに加筆・修正を加えています
構成
この後の構成は以下のとおりです。
- はじめに
- 1章 オプションのノーマライズ
- 2章 オプションよりレンダー
- 3章 レンダラーによるレンダー
- 4章 オプションよりテンプレートパスを見つける
- まとめ
はじめに
Railsアプリケーションでコントローラアクションにリクエストがあった時、Railsがどのようにレンダーするテンプレートを見つけているか疑問に思ったことはありませんか?例えばArticlesControllerのindexアクションにリクエストがあった時、通常テンプレートapp/views/articles/index.html.erbが選ばれ、レンダーされます。最近、Railsのコードを読んでいてますが、今回、皆さんと一緒にActionPackやActionViewのソースコードを見ていきたいと思います。
まず最初の章ではrenderがどのように機能しているのかを見ていきます。Railsのバージョンは4.2を想定していることに留意してください。もし違うバージョンを見ている場合多少実装が異なるかもしれません。
1章 オプションのノーマライズ
レンダーのエントリーポイントはAbstractController::Rendering#renderメソッドです。AbstractControllerはActionControllerやActionMailerにおいて共通で取り込まれているモジュールです。その2つのモジュールは多くの機能性が共通しているため、それら共通の機能性をAbstractControllerとして切り出しています。
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が呼ばれます。
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がどのように実装されているかを見ていきましょう。
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はオプションハッシュにこの第一引数を取り込み、適切なキーをあてるという責務があります。
それでは実装を見ていきましょう。
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アプリケーションでは、ApplicationControllerはActionController::Baseを継承し、ActionController::Baseは多くのモジュールをインクルードし、各モジュールはこのメソッドをオーバーライドして他のオプションを追加しています。
まずはじめにAbstractController::Renderingでどのようにこのメソッドが実装されているのかを確認していきましょう。
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::RenderingとActionController::Renderingの2つのモジュールがインスタンスメソッドをオーバーライドしています。上から下の順に確認していきましょう。
はじめにActionView::Renderingを見ていきます。
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を見ていきます。
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を見ていきましょう。
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メソッドはトリガーになったアクションの名前が返ります。
例えばArticlesControllerのindexアクションでトリガーした場合には、action_nameはindexになります。
もし: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メソッドに注目していきましょう。
module ActionView
module ViewPaths
# The prefixes used in render "foo" shortcuts.
def _prefixes # :nodoc:
self.class._prefixes
end
end
end
_prefixesメソッドはクラスメソッドの_prefixesを呼んでいるだけです。
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は再帰的に呼ばれます。これは親クラスの_prefixesでlocal_prefixesを追加しています。local_prefixesは唯一の要素controller_pathを持つ配列を返します。
controller_pathメソッドはとても単純で、AbstractController::Baseで実装されています。例えばArticlesControllerにおいてcontroller_pathはarticlesを返し、Articles::CommentsControllerにおいてcontroller_pathはarticles/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"]
プレフィックスとしてarticlesとapplicationの2つがあることが分かります。ApplicationControllerはActionController::Baseを継承しており、ActionController::Baseはabstractです。
なので、ArticlesController#indexアクションで引数なしでレンダーした場合にオプションは次の通りになります。
:prefixes : array [“articles”, “application”]
:template : string “index”
次にActionView::Layoutsがどのように_normalize_optionsメソッドをオーバーライドしているかを見ていきましょう。
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.erbがapp/views/layoutsディレクトリに作成されますが、デフォルトで全てのコントローラの親がApplicationControllerであるためにこのレイアウトがデフォルトで使用されるのです。
最後にActionController::Renderingがどのように_normalize_optionsをオーバーライドしているかを確認していきましょう。
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へ作成したオプションハッシュを渡します。
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を見てみると何もありません。例のごとく他のモジュールがオーバーライドしているのです。
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::RenderersとActionController::Rendering、ActionView::Renderingで実装されていることが分かります。一つずつ見ていきましょう。
ActionController::Renderers#render_to_body
ActionController::Renderers#render_to_bodyではレンダラーを登録しており、オプションがレンダラーのキーを持っていればレンダラーを呼びます。もしレンダラーが見つからない場合は、superを呼びます。
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
:jsonはActionController::Renderersで登録されたレンダラーとなるので、このレンダーが呼ばれます。ActionController::Renderers.addでオリジナルのレンダラーを追加することもできます。
ActionController::Rendering#render_to_body
ActionController::Renderers#render_to_bodyでレンダラーが見つからない場合superが呼ばれますが、これはActionController::Rendering#render_to_bodyです。このメソッドで何をするかを確認していきましょう。
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
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_templateはview_renderer.render(view_context, options)を呼びます。
view_rendererはActionView::Rendererのインスタンスで、初期化時にActionView::LookupContextのインスタンスであるlookup_contextを渡します。ActionView::LookupContextはオプションに基づきテンプレートを探すための情報全てを保持します。さて次章ではこのクラスを詳しく見ていき、LookupContextやViewPaths、PathSetがどのように組み合わさってテンプレートを探すのかを調べていきましょう。
3章 レンダラーによるレンダー
前章ではテンプレートはActionView::Renderer#renderメソッドでレンダーされることを見てきました。そしてこの章ではこのクラスを詳しく見ていきましょう。
それでは見ていきましょう。
ActionView::Renderer
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クラスを見てみましょう。
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]は配列になります。例えば、ArticlesControllerのindexアクションにアクセスがあった場合に:template及び:prefixesは以下のようになります。
:prefixes : array [“articles”, “application”]
:template : string “index”
そしてテンプレートはfind_templateメソッドによって見つけられます。このメソッドは親クラスのActionView::AbstractRendererで定義されていますので、このクラスを見ていきましょう。
ActionView::AbstractRenderer
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
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メソッドを確認してみましょう。
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::ViewPathsとActionView::LookupContextで実装されています。find_templateはfindメソッドのエイリアスであることが分かります。findメソッドはActionView::PathSetのインスタンスである@view_pathsに委任します。そのためActionView::PathSet#findが何をしているのかを理解する必要があります。実際にはActionView::PathSet#findはPathResolverの組みに対して探索を委任していますが、この点について次章で確認していきます。
4章 オプションよりテンプレートパスを見つける
前章ではActionView::LookupContext#find_templateは単にActionView::PathSet#findに委任していることがわかりました。それではActionView::PathSet#findが何をしているのかを確認しましょう。
ActionView::PathSet
PathSetというクラス名より、このクラスがテンプレートが保持されているパスの組みを管理するであろうことが分かります。さてこのクラスの定義を見ていきましょう。
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がどのように生成されているのかを確認することができます。
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メソッドの中にあります。
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_pathsはActionView::LookupContextインスタンスの初期化で渡されます。そして、ActionView::ViewPathsでActionView::LookupContextを初期化します。
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_pathやprepend_view_pathがビューパスをビューパス配列の最初または最後に追加します。このprepend_view_pathと
append_view_pathはクラスメソッドとしてもインスタンスメソッドとしても定義されていることに注意してください。
ではビューパスはどこで初期化されているでしょうか?実はRails::Engineクラスなのです。
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は渡ってくる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において、パスの中でテンプレートをどのように見つけるのかというロジックがどのように実装されているかについて注目していきましょう。
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_queryとfind_template_pathsです。
-
PathResolverはパス中のテンプレート探索のためにパターンを使用し、デフォルトパターンはDEFAULT_PATTERNです -
build_query: このメソッドはパスやdetailsからクエリを作成します。前章であった通りActionView::AbstractRenderer#extract_detailsはオプションからdetailsを抽出します。例えば、Articlesコントローラのindexアクションの場合クエリは以下のようになります
articles/index{.en,}{.html,}{+:variants,}{.haml,}
このクエリでは、find_template_pathsでDir[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から構成されたクエリを用いてパスセットを検索することによってテンプレート探索する
-
gistは元記事執筆者作成です。また、訳者がgem化していますのでそちらもご確認ください。 ↩
