概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Disassembling Rails — Template Rendering (2) – Ruby Inside – Medium
- 原文公開日: 2018/12/08
- 著者: Stan Lo -- Goby言語の作者でありRails開発者です。
Railsのテンプレートレンダリングを分解調査する#2 ActionView編(翻訳)
本記事は、Railsのテンプレートレンダリングを分解調査する#1探索編の続きです(半年も前の記事で失礼!)。今回は、RailsがRubyオブジェクトからテンプレートをレンダリングする過程を解説します。
今回チェックするファイル
- actionview/lib/action_view/renderer/template_renderer.rb
- actionview/lib/action_view/template.rb
ここまでのおさらい
前回の記事では、Railsがテンプレートファイルを読み取り、それを用いてテンプレートオブジェクトを初期化するところまでをご紹介しました。
# actionview/lib/action_view/template/resolver.rb
def query(path, details, formats, outside_app_allowed)
query = build_query(path, details)
template_paths = find_template_paths(query)
......
template_paths.map do |template|
......
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
end
Railsはテンプレートオブジェクトをどう使っているか
ActionView::TemplateRenderer
がテンプレートを見つけると、#render_template
を呼び出し、テンプレートを引数として渡します。このときにlayout
やlocals
も同時に渡され、オプションから展開される点にご注意ください。
# actionview/lib/action_view/renderer/template_renderer.rb
module ActionView
class TemplateRenderer < AbstractRenderer #:nodoc:
def render(context, options)
……
# Found your template
template = determine_template(options)
……
render_template(template, options[:layout], options[:locals])
end
end
end
そして以下が#render_template
です。
def render_template(template, layout_name = nil, locals = nil)
.....
render_with_layout(layout_name, locals) do |layout|
# instrumenting block
template.render(view, locals) { |*name| view._layout_for(*name) }
# end
end
end
実際のレンダリングがrender_with_layout
ブロックでラップされていることがわかりますね。今回はこの部分をスキップしますが、また別のテンプレートレンダリング記事でご説明したいと思います。
Template#render
Template#render
では2つのステップを実行します。テンプレートのコンテンツをメソッド(もちろんRubyのメソッドです)にコンパイルする作業と、そのメソッドを呼び出す作業です。これだけシンプルなら十分理解できますね。
def render(view, locals, buffer = nil, &block)
......
compile!(view)
view.send(method_name, locals, buffer, &block)
end
テンプレートの「コンパイル」とは
ここで言うテンプレートのコンテンツのコンパイルとは、RailsがActionView::Base
(より正確にはActionView::CompiledTemplates
)にメソッドを1つ定義することを指します。そのメソッドはlocalsを受け取って文字列を1つ返します。ここで例を用いて解説します。localを1つ受け取るsay_hi
というテンプレートをレンダリングしたいとしましょう。
# say_hi.erbHi <%= name %>
テンプレートがコンパイルされてメソッドになると、以下のようにRailsで使えるようになります。
def say_hi(local_assigns)
name = local_assigns[:name]
“Hi #{name}”
end
view.say_hi(name: "Stan")
#=> Hi Stan
ここの仕組みはどうなっているのでしょうか?Railsはこれを実現するために、次のようにRubyのメタプログラミングサポートをフル活用しています。
require "erb"
# ViewはActionView::Base的に振る舞うクラス
class View
end
class Template
def initialize(name, content)
@name = name
@content = content
end
def render(view)
compile(view)
view.send(@name)
end
def compile(view)
# コンパイルが必要なのは1度だけ
return if @compiled
# テンプレートエンジンとしてerbを使う
body = ERB.new(@content).src
src = <<-end_src
def #{@name}
#{body}
end
end_src
view.singleton_class.module_eval(src)
@compiled = true
end
end
view = View.new
template = Template.new("say_hi", "Hi!")
template.render(view) #=> Hi!
view.methods.first #=> :say_hi
しかしlocalsはどうやって処理しているのでしょうか?Railsでテンプレートごとに定義されるメソッドが1つだけだとしたら、どうやってlocalsを動的に渡せばよいのでしょう?私が最も見事だと思った設計はこの部分です。上のコードを少し変えてみましょう。
require "erb"
class View
end
class Template
def initialize(name, content, locals)
@name = name
@content = content
@locals = locals
end
def render(view, local_assigns)
compile(view)
view.send(@name, local_assigns)
end
def compile(view)
return if @compiled
body = ERB.new(@content).src
src = <<-end_src
def #{@name}(local_assigns)
#{locals_code}
#{body}
end
end_src
view.singleton_class.module_eval(src)
@compiled = true
end
def locals_code
@locals.map do |local|
"#{local} = local_assigns[:#{local}]"
end.join("\n")
end
end
view = View.new
template = Template.new("say_hi", "Hi! <%= name %>", [:name])
template.render(view, name: "Stan") #=> Hi! Stan
先の例で用いたsay_hi
メソッド全体は、以下のように定義されます。
def say_hi(local_assigns)
# locals_codeで生成される
name = local_assigns[:name]
# 以下はERBで生成されるが、わかりやすくするため展開してある
_erbout = +’’
_erbout.<< “Hi! “.freeze
erbout.<<(( name ).tos)
_erbout
end
巧妙な動作だと思いませんか?私は何年も前にこの実装を初めて読んだときについ惚れ惚れとしてしまいました。そしてこの実装は今も変わっていないのです!
もちろん、実際の#compile
メソッドはさらに複雑なケースに対応するため、これよりずっと複雑になっています。このあたりにご関心がおありの方は、こちらをご覧ください。
なお、以上のメカニズムはRailsコンソールを使って直接確認することもできます。
# 注: 以下の`posts`や`posts/index`は実際のRailsアプリのものに変更すること
paths = ActionController::Base.view_paths
view = ActionView::Base.new(paths)
# この時点ではコンパイル済みメソッドがない
view.methods.grep(/posts/) #=> []
# このrenderはaction_view/helpers/rendering_helper.rbが由来
view.render(template: "posts/index")
# コンパイル完了!
view.methods.grep(/posts/) #=> [:_app_views_posts_index_html_erb___4416254959938662165_70267190542480]
まとめ
ActionView
は実によく設計されたライブラリであると私は思っています。ActionView
によるテンプレートの探索やレンダリングの手法はエレガントです。そしてActionView
の基本設計はこの10年でほとんど変更されておらず、それでいて深刻な問題を引き起こしていないのです!私はActionView
のコードベースから実に多くのことを学びました。皆さんもお手すきのときにActionView
のコードを読んでみてはいかがでしょう。その価値は十分あると思います😋。