Open8

erb探索記

manimotomanimoto

動機

あるとき静的HTMLを生成する必要があり「Ruby on Railsでerbファイルよく書いてるから、rubyのERBクラス使えばサクっとできそう」と思ったら使い勝手がRailsのそれと全然違った。
「じゃあRailsの render メソッドってなんなんだ?」と思い調べてみたのでスクラップに積んでいく。
(正直昨今あまりRailsでフロントエンド書かれてない気がするので需要はほぼ無い気がする)

manimotomanimoto

eRubyについて

  • HTMLへRubyスクリプトを埋め込む技術
  • embedded Ruby の略
  • ERBとも表記され、ファイル拡張子も.erbである事が多い
  • HTMLだけでなく、任意のプレーンテキストに適用できる

eRuby誕生の経緯

  • もともと、RubyでHTMLを生成するにはputsなどでコードを記述・出力するしかなかった
  • ただ、putsでHTMLのタグを書くのは正直しんどい
  • そこで「PHPのようにHTMLに変数や処理を埋め込めないか」という案を元に考案されたのがeRuby

参考:eRuby - Wikipedia
https://ja.wikipedia.org/wiki/ERuby

manimotomanimoto

ERBについて 概要・基本的な使い方編

  • Ruby標準ライブラリのeRuby実装
    • eRubyの実装には他にもeruby, Erubis, Erubiがある

基本的な使い方

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
https://magazine.rubyist.net/articles/0017/0017-BundledLibraries.html

manimotomanimoto

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
manimotomanimoto

組み込み関数 binding について

  • binding.prybinding.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 # 直接参照しようとするとスコープ外なのでエラーになる
  ^^^^^

参考

manimotomanimoto

Ruby on Railsの render メソッドを探索

  • renderメソッドのどこかで ERB.new が呼ばれているはず!?

要約

以下を順に追っていった先に答えはある!

  1. ArticlesController#render
  2. ActionController::Metal::Rendering#render
  3. AbstractController::Rendering#render
  4. ActionController::Metal::Rendering#render_to_body
  5. ActionView::Rendering#render_to_body
  6. ActionController:Metal::Streaming#_render_template
  7. ActionView::Rendering#_render_template
  8. ActionView::Renderer#render_to_object
  9. ActionView::Renderer#render_template_to_object
  10. ActionView::Template#render
  11. ActionView::Template#compile!
  12. ActionView::Template#compile
  13. 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

https://github.com/rails/rails/blob/v7.0.4/actionpack/lib/action_controller/metal/rendering.rb#L28-L31

actionpack-7.0.4/lib/abstract_controller/rendering.rb

https://github.com/rails/rails/blob/v7.0.4/actionpack/lib/abstract_controller/rendering.rb#L23-L33

actionpack-7.0.4/lib/action_controller/metal/rendering.rb

https://github.com/rails/rails/blob/v7.0.4/actionpack/lib/action_controller/metal/rendering.rb#L45-L47

actionview-7.0.4/lib/action_view/rendering.rb

https://github.com/rails/rails/blob/v7.0.4/actionview/lib/action_view/rendering.rb#L101-L104

action_controller/metal/streaming.rb

https://github.com/rails/rails/blob/v7.0.4/actionpack/lib/action_controller/metal/streaming.rb#L212-L218

actionview-7.0.4/lib/action_view/rendering.rb

https://github.com/rails/rails/blob/v7.0.4/actionview/lib/action_view/rendering.rb#L108-L124

actionview-7.0.4/lib/action_view/renderer/renderer.rb

def render_to_object

https://github.com/rails/rails/blob/v7.0.4/actionview/lib/action_view/renderer/renderer.rb#L25-L31

def render_template_to_object

https://github.com/rails/rails/blob/v7.0.4/actionview/lib/action_view/renderer/renderer.rb#L60-L62

actionview/lib/action_view/template.rb

render

https://github.com/rails/rails/blob/v7.0.4/actionview/lib/action_view/template.rb#L154-L161

comple!

https://github.com/rails/rails/blob/v7.0.4/actionview/lib/action_view/template.rb#L241-L261

compile

https://github.com/rails/rails/blob/v7.0.4/actionview/lib/action_view/template.rb#L275-L314

  • 肝は 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 だと…
manimotomanimoto

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 にする
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

参考

manimotomanimoto

Ruby on Railsの render メソッドを探索のシーケンス図

(mermaid.jsのシーケンス図が2000文字超えてzennのブロック内文字数制限にかかったため、PNG画像&文字列で記載)

https://mermaid.live/edit#pako:eNqtls2O0zAQgF_F5IACyvYBLLQSCytx4bKLOEWyHHvahnXsYDss1Wov3ScBIcSNGxI_b5MDPAZ2_tqGNE2BHip7PPPNeDye-CZgikOAAwOvC5AMnqZ0oWkWS-R-tLBKFlkCupkzqzQq7z6Wdz_K9Rf3X8tzqm3K0pxKi6qhAEOYklYrIUAjatDjRvykkw6YMpsquWVISAbSUkGIBslBp3JRsSq9DelRok8xfg5Osx5etNpH-TBWA82m-rhstQd8JA7lHO142d1Bo9HnHw78TQrXhCTUwCbKl05W2585-QHTgUxu7Kf6ryHNyQ4yRs63RljIckHt4DZeNGsTEYQsqeQuiYYQ0MkYsZ49a9Tr2fnFWSxrV9ulfXJ6OlDLGJXr9-X6c7n-Xq6_NtfiTzVvPKGaMaqHoaArVViM5lQYeNDdtoMA72e02joPD6lemAjdT4RiV62HUdMj90CsIoniq1Dl3sgcuYvBGp3G7hmNx93dcoxIC2-KY3rkHWQk8mn0vbHvXrTtRKjkFTAb-rjgrY3QQe52Oe7Dtlfp__BbXFd9U2Ct0X4YU1meCrgX-oV_QoSZ4n8D6LUax6NChAbEPEJGFZrBGLVnPdmJ58-YoKbqWDMnI6lThaocPaASS7gO3dImxzOj2RHRjCWNwxHJ8l8nX_-FDDOwS8WJpBlEqE6T6z-uyUXIx5wU8znoCFHOfeUZS9kV3pn1O1bfz8nkDgi8aiGTOp-Dbn8KMPr17tPPD99iGURBBjqjKXdvphvPigO7dAcRB9gNOcxpIWwcxPLWqRY5d-k556l7NgW46u1R4B9VlyvJAmx1Aa1S8-6qhbe_AUjipfc

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-->>ユーザー: 表示