あの日見たerbを僕達はまだ知らない。
この記事は MICIN Advent Calendar 2022 の1日目の記事です。
はじめに
Ruby on Railsを書いている人であれば、ほとんどの方がindex.html.erbのようなerbファイルを書いたことがあると思いますが、その仕組みについてはあまり知られていないのではないでしょうか。
本記事では、erbの仕様と機能、そしてRuby on Railsのrenderメソッドがどのようにerbを描画しているかについて紹介したいと思います。
(正直、近年フロントエンドとバックエンドの分離が進み、Ruby on Railsでフロントエンドを書くことが少なくなり、本記事の需要もほぼ無いようにも思われますが、気にせず進めていきたいと思います。)
本記事ではRuby 3.1.2, Ruby on Rails 7.0.4を使用しています。
eRuby とは
まず仕様であるeRubyの話からはじめたいと思います。
eRubyはHTMLへRuby スクリプトを埋め込む書式の仕様です。
embedded Ruby の略でeRubyです。
もともと、RubyでHTMLを生成するにはputsなどでコードを記述・出力するしかありませんでした。
ただ、putsでHTMLのタグを書くのは正直しんどい…。
そこで 「PHPのようにHTMLに変数や処理を埋め込めないか」 という案を元に考案されたのがeRubyです。
「HTMLへRubyスクリプトを埋め込む」と書きましたが、HTMLだけでなく、任意のプレーンテキストに適用できる仕様になっています。
このeRubyの実装の1つがERBです。
ERBの概要・基本的な使い方
ERBはRuby標準ライブラリのeRuby実装です。
eRubyの実装にはERBの他にもeruby, Erubis, Erubi などがあります。
それではERBの基本的な使い方を見てみましょう。
基本的な使い方
require "erb"
str = "hoge"
puts ERB.new("value = <%= str %>").result(binding)
$ ruby main.rb
value = hoge
Ruby on Railsのviewで書くものと全然違いますね。
renderメソッドではありませんし、viewファイルもありません。
main.rbの3行目の ERB.new の引数に書かれた "value = <%= str %>"
。これがERBのテンプレートになります。
しかし2行目で変数 str
を宣言していますが、3行目の ERB.new では str
を渡している形跡はありません。newで渡したテンプレートの部分はあくまで文字列です。
一体どうやって変数 str
を渡しているのか?
そしてresultの引数にいきなり出てくる binding
…。これは何でしょうか?
実は 変数 str
をテンプレートに渡す秘密がこの binding
なのです。
binding
binding
はBindingオブジェクトを返すRubyの組み込み関数です。
変数・メソッドなどの環境情報を含んだ Binding オブジェクトを生成して返します。通常、Kernel.#eval の第二引数として使います。
参照:Kernel.#binding (Ruby 3.1 リファレンスマニュアル)
ところでこの binding
、どこかで見たことがないでしょうか?
Ruby on Railsで開発経験のある方なら binding.pry
や binding.irb
を一度は使ったことがあるのではないでしょうか。その binding
です。
Bindingオブジェクトとは?
Bindingオブジェクトは変数・メソッドなどの環境情報を表すオブジェクトです。
環境情報を表すオブジェクト?と言われてもピンとこないかと思います。実際にBindingオブジェクトを見てみましょう。
var_a = 1
def func_b
"c"
end
puts "--- Bindingオブジェクト ---"
p binding
puts "--- 変数 ---"
p binding.local_variables
puts "--- メソッド ---"
p binding.private_methods
$ ruby main.rb
--- Bindingオブジェクト ---
#<Binding:0x0000000102834750>
--- 変数 ---
[:var_a]
--- メソッド ---
[:func_b, ..(以下標準メソッドが続くため省略)..]
上記のように、環境内の変数名やメソッド名を配列で保持するオブジェクトです。
このBindingオブジェクトを用いると、ローカル変数をスコープ外から参照することが可能になります。
通常、func_a
メソッド内の変数 var_a
をスコープ外から直接参照しようとしてもエラーになります。
def func_a
var_a = 1
binding
end
puts "--- 直接ローカル変数var_aを出力"
p var_a
$ ruby main.rb
--- 直接ローカル変数var_aを出力
main.rb:7:in `<main>': undefined local variable or method `var_a' for main:Object (NameError)
p var_a # 直接参照しようとするとスコープ外なのでエラーになる
^^^^^
しかし、Binding#local_variable_get
を使えばローカル変数を外から参照できます。
def func_a
var_a = 1
binding
end
puts "--- binding経由でローカル変数var_aを出力"
p func_a.local_variable_get(:var_a)
$ ruby main.rb
--- binding経由でローカル変数var_aを出力
1 # Binding#local_variable_get を使えばローカル変数を外から参照できる!
ERBはresultメソッドの引数にbindingを渡すことで、ローカル変数をテンプレートに渡しているわけです。
テンプレートを外部ファイル化した場合
さてbindingの話をはさみましたが再びERBの話に戻ります。
先ほどの基本的な使い方はテンプレートを直接コードに埋め込んだ形式でした。
では次にテンプレートを外部ファイル化してみましょう。
value = <%= str %>
require "erb"
str = "hoge"
template = File.read("template.erb")
puts ERB.new(template).result(binding)
$ ruby main.rb
value = hoge
template.erbはRuby on Railsのviewファイルっぽいですね。
ERBではどんなことができるのか
先ほどテンプレートを外部ファイル化するところまではいきましたが、他にどんなことができるか見ていきましょう。
eachやifの使用
<% numbers.each do |num| %>
<%= num if num.even? %>
<% end %>
require "erb"
numbers = [*1..10]
template = File.read("template.erb")
puts ERB.new(template).result(binding)
$ ruby main.rb
2
4
6
8
10
each
や if
は使用できます。しかし出力結果を見ると空行が目立ちますね…。
標準ERBではテンプレート内記載の空白や改行がそのまま出力されます。
そのためテンプレートのeachとendの行も空行として出力されてしまうのです。
空行を回避する方法としてERB.newの引数に trim_mode
を指定することで空行を出力しないようにもできるのですが、erbの記述にも変更が必要でRailsのviewと書き味が変わるため本記事では割愛します。
ERB#run:標準出力に出力
これまでputsで出力していましたが、 ERB#run
を使えばputsなしで出力できます。
require "erb"
str = "hoge"
ERB.new("value = <%= str %>").run(binding)
$ ruby main.rb
value = hoge
ERB#src:eRubyをRubyスクリプトに変換したソースを返す
ソースにすることで eval
で実行できます。
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
Ruby on RailsのrenderメソッドはどこでERBを呼び出しているのか?
ここまでRuby標準ライブラリのERBの機能を見てきました。
ところでRuby on Railsだとerbの描画の際はERB.newは呼び出さず、 render
メソッドを用いるかと思います。
あの render
メソッドとはなんなのでしょうか?処理内部で ERB.new
を呼んでそうです。
Railsを「ERB.new」でgrepして探してみましょう。
$ git grep "ERB.new"
actionpack/lib/action_dispatch/journey/gtg/transition_table.rb: template = ERB.new erb
actionview/lib/action_view/template/handlers.rb: base.register_template_handler :erb, ERB.new
actionview/test/template/dependency_tracker_test.rb: @handler = ActionView::Template::Handlers::ERB.new
actionview/test/template/template_test.rb: ERBHandler = ActionView::Template::Handlers::ERB.new
activesupport/lib/active_support/configuration_file.rb: erb = ERB.new(@content).tap { |e| e.filename = @content_path }
railties/lib/rails/application/configuration.rb: yaml = DummyERB.new(Pathname.new(path).read).result
railties/lib/rails/command/base.rb: @desc ||= ERB.new(File.read(usage_path), trim_mode: "-").result(binding) if usage_path
railties/lib/rails/generators/base.rb: ERB.new(File.read(usage_path)).result(binding)
railties/lib/rails/generators/migration.rb: ERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer").result(context)
railties/lib/rails/secrets.rb: source = ERB.new(preprocess(path)).result
railties/test/application/test_runner_test.rb: file_content = ERB.new(<<-ERB, trim_mode: "-").result_with_hash(with: with.to_s)
railties/test/generators/generators_test_helper.rb: erb = ERB.new(File.read(file), trim_mode: "-", eoutvar: "@output_buffer")
tasks/release.rb: puts ERB.new(template, trim_mode: "<>").result(binding)
うーん、それっぽいものが見つからないですね…。
controllerのrender呼び出しから探索していってみましょう。
前準備:探索用のcontrollerとviewを用意
以下のようなcontrollerとviewを用意しました。
controller上のrenderは通常省略できるのですが、今回は処理の簡略化のため layout: false
を設定しlayoutを使用しないようにしています。
class ArticlesController < ApplicationController
def index
@str = "hoge"
render layout: false
end
end
value = <b><%= @str %></b>
準備完了です。
127.0.0.1:3000/articles にアクセスし、処理を探索していきたいと思います。
いざ探索!
と思ったのですが、探索内容を順に書いていくとここまで書いた倍以上の記事ボリュームになりそうでした…。
そのため本記事では探索概要のシーケンス図掲載にとどめ、探索結果のみ記述していこうと思います。
renderメソッド探索のシーケンス図
探索結果
actionview-7.0.4/lib/action_view/template/handlers/erb.rb に見覚えのある new(erb).src
構文を見つけました。
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
ERB#src:eRubyをRubyスクリプトに変換したソースを返す で紹介したERB#srcの構文ですね。
この self.class.erb_implementation
が ERB
のはず!?
module ActionView
class Template
module Handlers
class ERB
# 〜中略〜
# ↓erb_implementation の定義を発見!
class_attribute :erb_implementation, default: Erubi
Erubi
だと…
Erubi について
まさかのたどりついた先にあったのはERBではないものでした。
これまでERBについて調べてrenderメソッドを探索してきたのは何だったのでしょうか…。
しかしこのErubi、少し見覚えがありませんか?
ERBはRuby標準ライブラリのeRuby実装です。
https://docs.ruby-lang.org/ja/latest/class/ERB.htmleRubyの実装にはERBの他にもeruby, Erubis, Erubi などがあります。
そうです、実は ERBの概要・基本的な使い方 で名前が出ていたeRubyの実装の1つなのです。
経緯を見ると、ERB → Erubis → Erubi の順に誕生した、eRuby実装になります。
Ruby on RailsではRails 5.0まではErubisが使われていたのですが、Erubisの開発が2011年で止まったままのためRails 5.1からErubiが使われるようになりました。 (pull request)
Erubiの使い方
ERB#srcの使い方とほぼ同じです。以下の点を変更するだけで使用できます。
- requireを
erubi
にする - ERB.newを
Erubi::Engine.new
にする
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
まとめ
- ERBはRuby標準ライブラリのeRuby実装
- ERBはbindingを用いてテンプレートに変数を渡している
- Ruby on Railsのrenderメソッドで使われているのはERBではなくErubi
参考資料
ERB
binding
- Kernel.#binding (Ruby 3.1 リファレンスマニュアル)
- class Binding (Ruby 3.1 リファレンスマニュアル)
- “binding.pry”って実際のところは何なのだろう - Qiita
- Ruby における Binding オブジェクトとは
Erubi
- Erubi とは何か - Qiita
- Erubis徹底解説
- Erubis の Preprocessing 機能を使って Ruby on Rails の View 層を高速化する
- GitHub - jeremyevans/erubi: Small ERB Implementation
render探索
全体像
- Railsのテンプレートレンダリングを分解調査する#1探索編(翻訳)|TechRacho by BPS株式会社
- Railsのテンプレートレンダリングを分解調査する#2 ActionView編(翻訳)|TechRacho by BPS株式会社
- Railsの全体像を知ろう
- Template rendering in rails
Discussion
とても勉強になりました。
ありがとうございました!