これは何?
- 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化していますのでそちらもご確認ください。 ↩