Sinatraコールドリーディングも今回で最終回です🎩
今回はerb
でテンプレートを指定して実行後に実際にブラウザに返却されるresponse bodyが生成されるまでを見ていきます!
👇前回までの内容はこちら、以前の記事を前提として書いてますので1回目、2回目を読んで頂いた方が分かりやすいかと思います。
目次
はじめに
サンプルのアプリケーションの実装は下記の通りです。
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が生成されブラウザに返却されるまでを見ていきます!
前回で、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.fetch
でTilt::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を使っているようですね👀
レスポンスボディへの設定
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
で定義した処理)が実行されます。
それでinvoke
のcatch(: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アプリケーションのフレームワーク作ったらかなり勉強になりそうだったので、時間があったらやってみたいと思いました💪