🍀

あの日見たerbを僕達はまだ知らない。

2022/11/30に公開約14,000字

この記事は 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実装です。
https://docs.ruby-lang.org/ja/latest/class/ERB.html

eRubyの実装にはERBの他にもeruby, Erubis, Erubi などがあります。

それではERBの基本的な使い方を見てみましょう。

基本的な使い方

main.rb
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

bindingBindingオブジェクトを返すRubyの組み込み関数です。

変数・メソッドなどの環境情報を含んだ Binding オブジェクトを生成して返します。通常、Kernel.#eval の第二引数として使います。
参照:Kernel.#binding (Ruby 3.1 リファレンスマニュアル)

ところでこの binding 、どこかで見たことがないでしょうか?

Ruby on Railsで開発経験のある方なら binding.prybinding.irb を一度は使ったことがあるのではないでしょうか。その binding です。

Bindingオブジェクトとは?

Bindingオブジェクトは変数・メソッドなどの環境情報を表すオブジェクトです。

環境情報を表すオブジェクト?と言われてもピンとこないかと思います。実際に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
$ ruby main.rb
--- Bindingオブジェクト ---
#<Binding:0x0000000102834750>
--- 変数 ---
[:var_a]
--- メソッド ---
[:func_b, ..(以下標準メソッドが続くため省略)..]

上記のように、環境内の変数名やメソッド名を配列で保持するオブジェクトです。

このBindingオブジェクトを用いると、ローカル変数をスコープ外から参照することが可能になります。

通常、func_a メソッド内の変数 var_a をスコープ外から直接参照しようとしてもエラーになります。

main.rb
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 を使えばローカル変数を外から参照できます。

main.rb
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の話に戻ります。

先ほどの基本的な使い方はテンプレートを直接コードに埋め込んだ形式でした。
では次にテンプレートを外部ファイル化してみましょう。

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

template.erbはRuby on Railsのviewファイルっぽいですね。

ERBではどんなことができるのか

先ほどテンプレートを外部ファイル化するところまではいきましたが、他にどんなことができるか見ていきましょう。

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

eachif は使用できます。しかし出力結果を見ると空行が目立ちますね…。

標準ERBではテンプレート内記載の空白や改行がそのまま出力されます。
そのためテンプレートのeachとendの行も空行として出力されてしまうのです。

空行を回避する方法としてERB.newの引数に trim_mode を指定することで空行を出力しないようにもできるのですが、erbの記述にも変更が必要でRailsのviewと書き味が変わるため本記事では割愛します。

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

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を使用しないようにしています。

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>

準備完了です。

127.0.0.1:3000/articles にアクセスし、処理を探索していきたいと思います。

いざ探索!

と思ったのですが、探索内容を順に書いていくとここまで書いた倍以上の記事ボリュームになりそうでした…。

そのため本記事では探索概要のシーケンス図掲載にとどめ、探索結果のみ記述していこうと思います。

renderメソッド探索のシーケンス図

探索結果

actionview-7.0.4/lib/action_view/template/handlers/erb.rb に見覚えのある new(erb).src 構文を見つけました。

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

ERB#src:eRubyをRubyスクリプトに変換したソースを返す で紹介したERB#srcの構文ですね。

この self.class.erb_implementationERB のはず!?

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 について

まさかのたどりついた先にあったのはERBではないものでした。
これまでERBについて調べてrenderメソッドを探索してきたのは何だったのでしょうか…。

しかしこのErubi、少し見覚えがありませんか?

ERBの概要・基本的な使い方

ERBはRuby標準ライブラリのeRuby実装です。
https://docs.ruby-lang.org/ja/latest/class/ERB.html

eRubyの実装には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 にする
main.rb
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

Erubi

render探索

全体像

ActionController::Metal, AbstractController

ActionView

その他

Discussion

ログインするとコメントできます