Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby: SinatraのHTMLが生成され画面に返却されるまでのコードを読んでみた。

Sinatraコールドリーディングも今回で最終回です🎩
今回はerbでテンプレートを指定して実行後に実際にブラウザに返却されるresponse bodyが生成されるまでを見ていきます!

👇前回までの内容はこちら、以前の記事を前提として書いてますので1回目、2回目を読んで頂いた方が分かりやすいかと思います。

madogiwa0124.hatenablog.com

madogiwa0124.hatenablog.com

目次

はじめに

サンプルのアプリケーションの実装は下記の通りです。

app.rb

require 'sinatra'

get '/' do
  @title = "index"
  @content = "Hello World"
  erb :index
end

views/layout.erb

<html>
<head>
  <title><%= @title %></title>
</head>
<body>
  <%= yield %>
</body>
</html>

views/index.erb

<h1>Sinatra Sample</h1>
<p>message:<%= @content %></p>

上記コードから下記画面のHTMLが生成されブラウザに返却されるまでを見ていきます! f:id:madogiwa0124:20190812120454p:plain

前回で、requestが発生した際にgetで定義した内容が実行されるまでは見たのでので、 今回はerbが実行されて実際にresponseのbodyが生成されるまでをみていきます。

まず前提知識としてgetメソッド内でインスタンス変数が定義されていますが、このインスタンス変数は、Sinatra::Appricationインスタンス変数になるということを覚えておきましょう。

require 'sinatra'

get '/' do
  @title = "index"
  @content = "Hello World"
  erb :index
end

実際に読んでいるコードはこのファイルに記載されている内容です 👩‍💻

sinatra/base.rb at master · sinatra/sinatra · GitHub

テンプレートの取得とHTMLテキストの生成

では早速、最初に実行されるerbメソッドから見ていきます。 erbメソッドを実行すると引数に:erbを渡してrenderメソッドが実行されます。

def erb(template, options = {}, locals = {}, &block)
  render(:erb, template, options, locals, &block)
end

renderメソッドの中では、

  • optionの生成
  • compile_templateを使ってレンダリングに使用するtemplateのオブジェクトを生成
  • template.renderを使ってテンプレートエンジンを使ったHTMLテキストの生成
  • layoutのレンダリング処理

ちなみに、このrenderメソッドはindex.erbレンダリングlayout.erbレンダリングの2回実行されます。

def render(engine, data, options = {}, locals = {}, &block)
  # merge app-level options
  engine_options = settings.respond_to?(engine) ? settings.send(engine) : {}
  options.merge!(engine_options) { |key, v1, v2| v1 }

  # extract generic options
  locals          = options.delete(:locals) || locals         || {}
  views           = options.delete(:views)  || settings.views || "./views"
  layout          = options[:layout]
  layout          = false if layout.nil? && options.include?(:layout)
  eat_errors      = layout.nil?
  layout          = engine_options[:layout] if layout.nil? or (layout == true && engine_options[:layout] != false)
  layout          = @default_layout         if layout.nil? or layout == true
  layout_options  = options.delete(:layout_options) || {}
  content_type    = options.delete(:default_content_type)
  content_type    = options.delete(:content_type)   || content_type
  layout_engine   = options.delete(:layout_engine)  || engine
  # ここでscopeに`Sinatra::Application`のインスタンスが格納される。
  scope           = options.delete(:scope)          || self
  options.delete(:layout)

  # set some defaults
  options[:outvar]           ||= '@_out_buf'
  options[:default_encoding] ||= settings.default_encoding

  # ここからテンプレートを取得してレンダリングする。
  # templateにテンプレートを使ったレンダリング用のオブジェクトを入れて、
  # outputに実際のレンダリング済みのHTMLテキストが格納される。
  # compile and render template
  begin
    layout_was      = @default_layout
    @default_layout = false
    template        = compile_template(engine, data, options, views)
    # scopeの中に設定したインスタンス変数(@content, @title)が格納されているので、
    # template.renderの引数に渡すことでviewとインスタンス変数が紐づき、
    #  :scope=>
    #   <Sinatra::Application:0x00007fed46258268
    #     @content="Hello World",
    #     @title="index"
    # 最終的なHTMLテキストが返却される。
    output          = template.render(scope, locals, &block)
  ensure
    @default_layout = layout_was
  end

  # render layout
  if layout
    options = options.merge(:views => views, :layout => false, :eat_errors => eat_errors, :scope => scope).
            merge!(layout_options)
    # ここでlayoutのレンダリング処理が実行される。
    catch(:layout_missing) { return render(layout_engine, layout, options, locals) { output } }
  end

  output.extend(ContentTyped).content_type = content_type if content_type
  output
end

compile_templateの中で実行しているのは下記の通り

  • emplate_cache.fetchTilt::Cache#fetchを使ってキャッシュ
  • Tilt[engine]でtemplate engine(今回はerb)を取得
  • find_templateでtemplateファイルのパス(./views/index.erb等)を取得
  • template.new(path, 1, options)レンダリング用のオブジェクトを返却
def compile_template(engine, data, options, views)
  eat_errors = options.delete :eat_errors
  # templateのキャッシュを生成
  template_cache.fetch engine, data, options, views do
    # ここでerbのテンプレートエンジンを取得
    # https://github.com/rtomayko/tilt
    template = Tilt[engine]
    raise "Template engine not found: #{engine}" if template.nil?

    # dataは:index or :layoutになる。
    case data
    when Symbol
      # これらは全部nilになってる。
      body, path, line = settings.templates[data]
      if body
        body = body.call if body.respond_to?(:call)
        template.new(path, line.to_i, options) { body }
      else
        found = false
        @preferred_extension = engine.to_s
        # templateファイルのpathを返却
        # :index => "./views/index.erb"
        # :layout => "./views/layout.erb"
        find_template(views, data, template) do |file|
          path ||= file # keep the initial path rather than the last one
          if found = File.exist?(file)
            path = file
            break
          end
        end
        throw :layout_missing if eat_errors and not found
        # レンダリング用のtemplateオブジェクトを返却
        template.new(path, 1, options)
      end
    when Proc, String
      body = data.is_a?(String) ? Proc.new { data } : data
      caller = settings.caller_locations.first
      path = options[:path] || caller[0]
      line = options[:line] || caller[1]
      template.new(path, line.to_i, options, &body)
    else
      raise ArgumentError, "Sorry, don't know how to render #{data.inspect}."
    end
  end
end

Sinatraでは、rubyの各種template(hamlとかerbとか)を良い感じに扱えるgemを使っているようですね👀

github.com

レスポンスボディへの設定

HTMLテキストの生成されるまでの流れが理解出来たところで最初に戻ってみると、getメソッドの最後にerbが実行されているから最終的なHTMLテキストがgetメソッドの返り値として返却されることがわかります!

require 'sinatra'

get '/' do
  @title = "index"
  @content = "Hello World"
  erb :index
end

これを踏まえてresponceの生成処理を見ていきます。前回の記事の通りsinatraはリクエストを受け取ると#call!が呼ばれ、invoke { dispatch! }が呼ばれます。dispath!の中ではroute!が実行されてroutingの処理(getで定義した処理)が実行されます。

それでinvokecatch(:halt) { yield }dispath!が実行されて、resに返り値の最終的なHTMLテキストが格納されます。

それでbody(res.pop)response.body = valueでレスポンスのボディに最終的なHTMLテキストが設定されるというわけですね 🎉

def invoke
  # dispath!が実行されてされてHTMLテキストが返却される。
  # => "<html>\n<head>\n  <title>index</title>\n</head>\n<body>\n  <h1>Sinatra Sample</h1>\n<p>message:Hello World</p>\n\n\n</body>\n</html>\n"
  res = catch(:halt) { yield }
  res = [res] if Integer === res or String === res
  if Array === res and Integer === res.first
    res = res.dup
    status(res.shift)
    body(res.pop)
    headers(*res)
  elsif res.respond_to? :each
    body res
  end
  nil # avoid double setting the same response tuple twice
end

おわりに

Sinatraのコード読んで、

  • Webサーバーがどうやって起動するか
  • URLにアクセスしてroutingで定義した処理がどう実行されているか
  • viewsに置いたテンプレートとインスタンス変数がどう紐付いて実際のHTMLとして返却されるか

といったことがRubyのコードでどう実現されているかが何となく理解することが出来ました 💪

あとはSinatraチュートリアルを読んで見るとrailsのbefore_actionに相当する機能があったり、もっとシンプルなフレームワークかと思っていたのですが意外と機能があることがわかりました👀

普通に個人で軽くサービス作るならRailsではなくSinatraの方が前提知識も少なくていいし、割と勉強中の方にはいいのでは?という気持ちにもなったのですが、いかんせん情報量的にRailsのほうが圧倒的に多いので難しいところですね💦

Sinatraをよりシンプルにした軽量のWebアプリケーションのフレームワーク作ったらかなり勉強になりそうだったので、時間があったらやってみたいと思いました💪