Open8
erb探索記
動機
あるとき静的HTMLを生成する必要があり「Ruby on Railsでerbファイルよく書いてるから、rubyのERBクラス使えばサクっとできそう」と思ったら使い勝手がRailsのそれと全然違った。
「じゃあRailsの render
メソッドってなんなんだ?」と思い調べてみたのでスクラップに積んでいく。
(正直昨今あまりRailsでフロントエンド書かれてない気がするので需要はほぼ無い気がする)
eRubyについて
- HTMLへRubyスクリプトを埋め込む技術
- embedded Ruby の略
- ERBとも表記され、ファイル拡張子も.erbである事が多い
- HTMLだけでなく、任意のプレーンテキストに適用できる
eRuby誕生の経緯
- もともと、RubyでHTMLを生成するにはputsなどでコードを記述・出力するしかなかった
- ただ、putsでHTMLのタグを書くのは正直しんどい
- そこで「PHPのようにHTMLに変数や処理を埋め込めないか」という案を元に考案されたのがeRuby
参考:eRuby - Wikipedia
ERBについて 概要・基本的な使い方編
基本的な使い方
main.rb
require "erb"
str = "hoge"
puts ERB.new("value = <%= str %>").result(binding)
$ ruby main.rb
value = hoge
テンプレートを外部ファイル化した場合
template.erb
value = <%= str %>
main.rb
require "erb"
str = "hoge"
template = File.read("template.erb")
puts ERB.new(template).result(binding)
$ ruby main.rb
value = hoge
eachやifは使える
template.erb
<% numbers.each do |num| %>
<%= num if num.even? %>
<% end %>
main.rb
require "erb"
numbers = [*1..10]
template = File.read("template.erb")
puts ERB.new(template).result(binding)
$ ruby main.rb
2
4
6
8
10
変数strどうやって渡してる??
- result メソッドの引数に 組み込み関数
binding
を使って取得したBindingオブジェクトを指定することで変数strを渡している(Bindingオブジェクトについては後述)
参考:標準添付ライブラリ紹介 【第 10 回】 ERB
ERBについて 使い方発展編
ERB#run:標準出力に出力
- 基本的な使い方ではputsで出力していたが、
ERB#run
を使えばputsなしで標準出力できる
main.rb
require "erb"
str = "hoge"
ERB.new("value = <%= str %>").run(binding)
$ ruby main.rb
value = hoge
ERB#src:eRubyをRubyスクリプトに変換したソースを返す
- ソースにすることでevalで実行できる
main.rb
require "erb"
str = "hoge"
src = ERB.new("value = <%= str %>").src
puts "--- src ---"
p src
puts "--- eval ---"
puts eval(src, binding)
$ ruby main.rb
--- src ---
"#coding:UTF-8\n_erbout = +''; _erbout.<< \"value = \".freeze; _erbout.<<(( str ).to_s); _erbout"
--- eval ---
value = hoge
binding
について
組み込み関数 -
binding.pry
やbinding.irb
についてるbinding
変数・メソッドなどの環境情報を含んだ Binding オブジェクトを生成して返します。通常、Kernel.#eval の第二引数として使います。
参照:Kernel.#binding (Ruby 3.1 リファレンスマニュアル)
Bindingオブジェクトとは
- 変数・メソッドなどの環境情報を表すオブジェクト
main.rb
var_a = 1
def func_b
"c"
end
puts "--- Bindingオブジェクト ---"
p binding
puts "--- 変数 ---"
p binding.local_variables
puts "--- メソッド ---"
p binding.private_methods
--- Bindingオブジェクト ---
#<Binding:0x0000000102834750>
--- 変数 ---
[:var_a]
--- メソッド ---
[:func_b, ..(以下標準メソッドが続くため省略)..]
- Bindingオブジェクトを用いることで、ローカル変数を外から参照できます
main.rb
def func_a
var_a = 1
binding
end
puts "--- binfing経由でローカル変数var_aを出力"
p func_a.local_variable_get(:var_a)
puts "--- 直接ローカル変数var_aを出力"
p var_a
--- binfing経由でローカル変数var_aを出力
1 # Binding#local_variable_get を使えばローカル変数を外から参照できる!
--- 直接ローカル変数var_aを出力
binding.rb:10:in `<main>': undefined local variable or method `var_a' for main:Object (NameError)
p var_a # 直接参照しようとするとスコープ外なのでエラーになる
^^^^^
参考
- “binding.pry”って実際のところは何なのだろう - Qiita
- Ruby における Binding オブジェクトとは | UX MILK
- class Binding (Ruby 3.1 リファレンスマニュアル)
render
メソッドを探索
Ruby on Railsの - renderメソッドのどこかで
ERB.new
が呼ばれているはず!?
要約
以下を順に追っていった先に答えはある!
- ArticlesController#render
- ActionController::Metal::Rendering#render
- AbstractController::Rendering#render
- ActionController::Metal::Rendering#render_to_body
- ActionView::Rendering#render_to_body
- ActionController:Metal::Streaming#_render_template
- ActionView::Rendering#_render_template
- ActionView::Renderer#render_to_object
- ActionView::Renderer#render_template_to_object
- ActionView::Template#render
- ActionView::Template#compile!
- ActionView::Template#compile
- ActionView::Template::Handlers::ERB#call
- self.class.erb_implementation.new(erb, options).src
- class_attribute :erb_implementation, default: Erubi
詳細:どこで ERB.new が呼ばれているか?
- 127.0.0.1:3000/articles にアクセスし、処理を追います
- Rails 7.0.4 を使用
articles_controller.rb
- 以下のcontroller, viewを用意
- 処理の簡略化のために
layout: false
を設定
- 処理の簡略化のために
app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
@str = "hoge"
render layout: false
end
end
app/views/articles/index.html.erb
value = <b><%= @str %></b>
actionpack-7.0.4/lib/action_controller/metal/rendering.rb
- controllerでrenderを実行すると最初に入るメソッド
- 二重レンダリングエラーはここで発生させられている
- renderはRailsの組み込み関数ではない。継承されたメソッド。
- 各controllerはApplicationControllerを継承している
- ApplicationControllerはActionController::Baseを継承している
- ActionController::BaseはActionController::Metalを継承している
- https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionpack/lib/action_controller/base.rb#L167
- なので各controllerから
render
で実行できる
- Metalとは
- 最も簡単なController
- コールバックやContent-Type制御、render、HTTP認証などActionController::Baseの多くの基本的な機能を省いたもの
- 参考
actionpack-7.0.4/lib/abstract_controller/rendering.rb
- AbstractControllerとは
- Controller層の中間レイヤー
- ControllerであるActionControllerだけでなく、ActionMailerもAbstractControllerを継承している
- 参考
actionpack-7.0.4/lib/action_controller/metal/rendering.rb
actionview-7.0.4/lib/action_view/rendering.rb
action_controller/metal/streaming.rb
actionview-7.0.4/lib/action_view/rendering.rb
actionview-7.0.4/lib/action_view/renderer/renderer.rb
def render_to_object
def render_template_to_object
actionview/lib/action_view/template.rb
render
comple!
compile
- 肝は
code = @handler.call(self, source)
[1] pry(#<ActionView::Template>)> @handler
=> #<ActionView::Template::Handlers::ERB:0x000000010b21c850>
[2] pry(#<ActionView::Template>)> self
=> #<ActionView::Template app/views/articles/index.html.erb locals=[]>
[3] pry(#<ActionView::Template>)> source
=> "value = <b><%= @str %></b>\n"
-
ActionView::Template::Handlers::ERB !
-
ActionView::Template::Handlers::ERB#call メソッドに
app/views/articles/index.html.erb
を渡している! -
actionview-7.0.4/lib/action_view/template/handlers/erb.rb
actionview-7.0.4/lib/action_view/template/handlers/erb.rb
module ActionView
class Template
module Handlers
class ERB
# 〜中略〜
def self.call(template, source)
new.call(template, source)
end
# 〜中略〜
def call(template, source)
# 〜中略〜
# ↓見覚えのある new(erb).src 構文!!
self.class.erb_implementation.new(erb, options).src
end
- 見覚えのある new(erb).src 構文!
- self.class.erb_implementation が
ERB
のはず!
actionview-7.0.4/lib/action_view/template/handlers/erb.rb
module ActionView
class Template
module Handlers
class ERB
# 〜中略〜
# ↓erb_implementation の定義を発見!
class_attribute :erb_implementation, default: Erubi
-
Erubi
だと…
Erubi について
- Erubi もERBと同様eRuby実装の1つ
- ERB → Erubis → Erubi の順に誕生した
- Rails 5.0 まではErubisが使われていたが、5.1からErubiが使われるようになった
- Erubiは元々Erubisのfork
- Erubisの開発が2011年で止まったままのため、Erubisに切り替えられた模様 (pull request)
Erubiの使い方
- ERB#srcの使い方とほぼ同じ
- requireを
erubi
にする - ERB.newを
Erubi::Engine.new
にする
- requireを
main
require 'erubi'
str = "hoge"
src = Erubi::Engine.new("value = <%= str %>").src
puts "--- src ---"
p src
puts "--- eval ---"
puts eval(src, binding)
$ ruby main.rb
--- src ---
"_buf = ::String.new; _buf << 'value = '.freeze; _buf << ( str ).to_s;\n_buf.to_s\n"
--- eval ---
value = hoge
参考
- Erubi とは何か - Qiita
- Erubis徹底解説
- Erubis の Preprocessing 機能を使って Ruby on Rails の View 層を高速化する
Ruby on Railsの render メソッドを探索のシーケンス図
(mermaid.jsのシーケンス図が2000文字超えてzennのブロック内文字数制限にかかったため、PNG画像&文字列で記載)
sequenceDiagram
autonumber
actor ユーザー
participant articles_controller as ArticlesController
participant action_controller__mental__rendering as ActionController<br>::Metal<br>::Rendering
participant action_controller__mental__streaming as ActionController<br>::Metal<br>::Streaming
participant abstract_controller__rendering as AbstractController<br>::Rendering
participant action_view__base as ActionView<br>::Base
participant action_view__rendering as ActionView<br>::Rendering
participant action_view__renderer as ActionView<br>::Renderer
participant action_view__template as ActionView<br>::Template
participant action_view__template__handlers__erb as ActionView<br>::Template<br>::Handlers<br>::ERB
ユーザー->>articles_controller: アクセス
articles_controller->>action_controller__mental__rendering: render(layout: false)
action_controller__mental__rendering->>abstract_controller__rendering: render(*args, &block)
abstract_controller__rendering->>action_controller__mental__rendering: render_to_body(options)
action_controller__mental__rendering->>action_view__rendering: render_to_body(options)
action_view__rendering->>action_controller__mental__streaming: _render_template(options)
action_controller__mental__streaming->>action_view__rendering: _render_template(options)
action_view__rendering->>action_view__renderer: render_to_object(context, options)
action_view__renderer->>action_view__renderer: render_template_to_object(context, options)
action_view__renderer->>action_view__template: render(context, options)
action_view__template->>action_view__template: compile!(view)
action_view__template->>action_view__template: compile(mod)
action_view__template->>action_view__template__handlers__erb: call(self, source)
action_view__template__handlers__erb->>action_view__template__handlers__erb: self.class<br>.erb_implementation<br>.new(erb, options).src
action_view__template__handlers__erb-->>action_view__template: code
action_view__template->>action_view__base: _run(method_name, self, locals, <br>buffer, add_to_stack: add_to_stack, &block)
action_view__base-->>abstract_controller__rendering: rendered_body
abstract_controller__rendering-->>ユーザー: 表示