layoutのyieldが何をしているのかという話
共通レイアウトファイルからyieldするときの挙動を検証してみました。
これが分かったところで、特に何か応用できるわけでもないと思いますが
ブラックボックスの気持ち悪さを少し解消できました。
rails version : 5.2.4.5
動作の検証
ベース
まず適当にこんな感じでページを作ります。
h1 Page Title p hogehoge p fugafuga br = link_to 'Blogs', blogs_path
doctype html
html
head
[ ... ]
body
= yield
こんな感じのページになります。
変更
viewでprovideを使ってコンテンツブロックを作って、共通レイアウトで名前付きのyieldを呼び出してみます。
- # bodyという名前のコンテンツブロックを作成 - provide :body h3 provide :body1 h1 Page Title p hogehoge p fugafuga - # titleという名前のコンテンツブロックを作成 - provide :title h2 provide :title - # bodyという名前のコンテンツブロックを作成 - provide :body h3 provide :body2 br = link_to 'Blogs', blogs_path
doctype html html head [ ... ] body p ------------------------------ p yield :title = yield :title p ------------------------------ p yield :body = yield :body p ------------------------------ p yield = yield p ------------------------------
出力ページはこの様になります。
同じ名前でprovideしたところは定義順に出力され、上書きされない。
ということがわかると思います。
どういう実装になっているのか
ということで処理を詳しくみていきます。
- provide :body h3 provide :body1
provideは以下の様に定義されます。
def provide(name, content = nil, &block) content = capture(&block) if block_given? result = @view_flow.append!(name, content) if content result unless content end
&block
: do~end の中身をprocオブジェクトとして格納しています。
capture(&block)
: &blockの中身をstringで返します。
@view_flow.append!(name, content) # @view_flow.append!(:body, <h3>provide :body1</h3>
この引数が二つあるview_flow.appendは以下のようにオーバーライドされています。
append(key , value)do @content[key] << value end
これによって@view_flowには以下の様な形で値が格納されます。
pry() > @view_flow => <ActionView::OutputFlow:0x00007ff5454f2d50 @content={:body=>"<h3>provide :body1</h3>"}>
同じ名前でprovideした時に定義順に@content[key]に格納されていく事がappendのコードから分かると思います。
def render_with_layout(path, locals) layout = path && find_layout(path, locals.keys, [formats.first]) content = yield(layout) if layout view = @view view.view_flow.set(:layout, content) layout.render(view, locals) { |*name| view._layout_for(*name) } else content end end
index.html.slim全体はyield(layout)
で取得されて、:layout
というkeyでview_flow
にセットされます。
ここのyieldは詳しくみませんが、index.html.slimをrenderしています。
yield(layout)
で取得している途中でprovideのブロックを@view_flow
に格納してく感じです。
※この辺りのコードリーディングをしている時にyieldは遅延評価だと理解しました。
ちなみにlayoutは上書きなのでviewテンプレート内でprovide :layout {}
を定義しても消滅します。
pry()> view.view_flow => #<ActionView::OutputFlow:0x00007ff546a780a8 @content= {:body=>"<h3>provide :body1</h3><h3>provide :body2</h3>", :title=>"<h2>provide :title</h2>", :layout=>"<h1>Page Title</h1><p>hogehoge</p><p>fugafuga</p><br /><a href=\"/blogs\">Blogs</a>"}>
layout.render(view, locals)でapplication.html.slimをrenderしています。
つまり共通レイアウトの中のyield :name
はview._layout_for(*name)
を実行します。
def _layout_for(*args, &block) name = args.first if block && !name.is_a?(Symbol) capture(*args, &block) else super end end
layout_forにブロックは渡されていないのでsuper classのlayout_forを呼びます。
def _layout_for(name = nil) name ||= :layout view_flow.get(name).html_safe end
こいつがレイアウトファイルにおけるyieldの実体です。
名前をつけないyieldはyield(:layout)
と同じという事ですね。
実際に共通レイアウトからview_flow.get(name).html_safe
を呼んで、yieldと同じ出力を得ることができました。
参考
GitHub - rails/rails: Ruby on Rails
Disassembling Rails — Template Rendering (2) | by Stan Lo | Ruby Inside | Medium