Tech Racho エンジニアの「?」を「!」に。
  • 開発

Railsのテンプレートレンダリングを分解調査する#1探索編(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします(パブリックドメイン)。

Railsのテンプレートレンダリングを分解調査する#1探索編(翻訳)

今回は「Rails分解調査シリーズ」第2弾です。前回の記事「Railsのフラグメントキャッシュを分解調査する」をお読み頂いてない方でも今回の記事を読むのに差し支えはありませんが、それでもご一読をおすすめいたします。

Railsのテンプレートレンダリングの背後では実に多くの処理が行われているので、2回に分けて詳しくご紹介いたします。その第1回目として、表示したいテンプレートをRailsのrenderメソッドがどのように探索しているかを説明いたします。続く第2回では、テンプレートオブジェクトがレスポンスに使えるHTMLに変換される過程を説明します。それでは始めましょう!

注目すべきファイル

ソースコードを自分でも見てみたい方向けに(私からも推奨します)、今回のテーマで注目すべきファイルをリストアップします。

訳注: Rails 5.2-stableのソースにリンクしました。

ユーザーインターフェイス

Railsはさまざまな方法でテンプレートをレンダリングします。その1つが#renderメソッドを手動で実行することです。そこでまず以下から始めることにします。

render template: "comments/index", formats: :json

薄々お気づきのように、このrenderメソッドはActionView::Helpers::RenderingHelperというヘルパーにあります。

# actionview/lib/action_view/helpers/rendering_helper.rb
def render(options = {}, locals = {}, &block)
  case options
  when Hash
    if block_given?
      view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
    else
      view_renderer.render(self, options)
    end
  else
    view_renderer.render_partial(self, partial: options, locals: locals, &block)
  end
end

この#renderメソッドを見てみると、テンプレートのレンダリングを担当するview_rendererがあることがおよそ見て取れます。このview_rendererは実際にはActionView::Rendererオブジェクトなので、そちらの#renderメソッドを見てみましょう。

# actionview/lib/action_view/renderer/renderer.rb
module ActionView
  class Renderer
    def render(context, options)
      if options.key?(:partial)
        render_partial(context, options)
      else
        render_template(context, options)
      end
    end

    def render_template(context, options) #:nodoc:
      TemplateRenderer.new(@lookup_context).render(context, options)
    end

    def render_partial(context, options, &block)
      PartialRenderer.new(@lookup_context).render(context, options, block)
    end
  end
end

こちらを見ると、2つの異なるクラスがそれぞれtemplateのレンダリングとpartialのレンダリングを担当していることがわかります。partialのレンダリングはもう少し複雑なので、今回はtemplateのレンダリングのみをご紹介します。

ここで注目すべきは、Renderer@lookup_contextインスタンス変数です。探索するテンプレートに関する必要な情報はすべてここにあります。

#<ActionView::LookupContext:0x00007fa8d5c7f670
 @cache=true,
 @details=
  {:locale=>[:en],
   :formats=>
    [:html,
     :text,
     :js,
     :css,
       ......
     ],
   :variants=>[],
   :handlers=>[:raw, :erb, :html, :builder, :ruby]},
 @details_key=nil,
 @prefixes=[],
 @rendered_format=nil,
 @view_paths=
  #<ActionView::PathSet:0x00007fa8d5c7eba8
   @paths=
    [#<ActionView::FileSystemResolver:0x00007fa8d5c9c7c0
      @cache=#<ActionView::Resolver::Cache:0x7fa8d5c9c680 keys=0 queries=0>,
      @path="/Users/st0012/projects/rails/actionview/test/fixtures",
      @pattern=":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}">]>>

ActionView::LookupContextについて

私見では、このActionView::LookupContextこそがテンプレートのレンダリングにおける最も重要度の高いコンポーネントです。このコンポーネントの属性、特に@details@view_pathsを見てみましょう。

@detailsはハッシュで、localeformatsvariantshandlersが含まれています。@detailsの情報の使いみちは次の2とおりです。

1. 見つかったテンプレートのキャッシュに使われるキャッシュキーの一部となる(コード

# actionview/lib/action_view/template/resolver.rb
def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
  cached(key, [name, prefix, partial], details, locals) do
    find_templates(name, prefix, partial, details)
  end
end

2. Railsはこの情報を元にテンプレートのファイル拡張子をフィルタする(コード

# actionview/lib/action_view/template/resolver.rb
module ActionView
  class PathResolver < Resolver #:nodoc:
    EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
    DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
  .....
  end
end

次の@view_pathsは、ActionView::PathSetのインスタンスです。PathSetはテンプレートの探索対象となるパスのセットで、各パスはResolverというオブジェクトで次のようにガードされます。

#<ActionView::FileSystemResolver:0x00007fa8d5c9c7c0
  # これは見つかったテンプレートのキャッシュに用いる
  @cache=#<ActionView::Resolver::Cache:0x7fa8d5c9c680 keys=0 queries=0>,
  # これはテンプレートを探索すべき対象
  @path="/Users/st0012/projects/sample/app/views",
  # このパターンは、テンプレートクエリの組み立てに用いられ
  # 多くの場合違いはほとんどない
  @pattern=":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}">

通常の場合、アプリのRAILS_PROJECT/app/viewsはビューテンプレートの置き場所の1つなので、この場所をガードするリゾルバがこの場所でのテンプレート探索を補助します。一部のRailsエンジンkaminarideviseなど)を使っている場合、kaminari/app/viewsdevise/app/viewもリゾルバによってガードされ、これらのgemのテンプレート探索を補助します。

ご覧いただいたように、LookupContextはテンプレートの探索場所や探索すべきテンプレートの種類をRailsに伝える役割を果たします。ここがテンプレートのレンダリングで最も重要な箇所であると申し上げた理由はこれです。

Railsがテンプレートを探索するまでの道のり

ここでActionView::TemplateRenderer#renderに戻りましょう。

# actionview/lib/action_view/renderer/template_renderer.rb
module ActionView
  class TemplateRenderer < AbstractRenderer
    def render(context, options)
      ......
      template = determine_template(options)
      ......
        render_template(template, options[:layout], options[:locals])
    end

    def determine_template(options)
      ......
      if ......
      elsif options.key?(:template)
        ......
        find_template(options[:template], options[:prefixes], false, keys, @details)
        ......
      end
    end
  end
end

テンプレートをレンダリングするには、まずテンプレートオブジェクトの取得が必要なので、これより説明します。ここからの数ステップはメソッド委譲が連続しているだけなので、説明を少し簡略化します。ステップは次のようになります。

1. TemplateRenderer#find_template@lookup_contextに委譲)
2. LookupContext#find_template#findのエイリアス)
3. LookupContext#find@view_pathsに委譲)
4. PathSet#find#find_allを呼び出し、その#find_all#find_allを呼び出す
5. PathSet#_find_allpath (resolver)eachで回して#find_allを呼び出す

# actionview/lib/action_view/path_set.rb
def _find_all(path, prefixes, args, outside_app)
  prefixes = [prefixes] if String === prefixes
  prefixes.each do |prefix|
    paths.each do |resolver|
      ......
      templates = resolver.find_all(path, prefix, *args)
      ......
      return templates unless templates.empty?
    end
  end
  []
end

6.  Resolver#find_allからPathResolver#find_templateを呼び出す

これでやっと、実際のテンプレート探索ロジックにたどり着きました(コード)。

# actionview/lib/action_view/template/resolver.rb

def find_templates(name, prefix, partial, details, outside_app_allowed = false)
  path = Path.build(name, prefix, partial)
  query(path, details, details[:formats], outside_app_allowed)
end

def query(path, details, formats, outside_app_allowed)
  query = build_query(path, details)

  template_paths = find_template_paths(query)
  template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed

  template_paths.map do |template|
    handler, format, variant = extract_handler_and_format_and_variant(template)
    contents = File.binread(template)

    Template.new(contents, File.expand_path(template), handler,
      virtual_path: path.virtual,
      format: format,
      variant: variant,
      updated_at: mtime(template)
    )
  end
end

実際のテンプレート探索の3つのステップ

実際のテンプレート探索は大きく3つのステップからなります。

1. テンプレートクエリのビルド

# actionview/lib/action_view/template/resolver.rb

def find_templates(name, prefix, partial, details, outside_app_allowed = false)
  path = Path.build(name, prefix, partial)
  query(path, details, details[:formats], outside_app_allowed)
end

def query(path, details, formats, outside_app_allowed)
  query = build_query(path, details)
    ......
end

できあがったクエリは次のような感じになります。

"/Users/stanlow/projects/sample/app/views/posts/index{.en,}{.html,}{}{.raw,.erb,.html,.builder,.ruby,.coffee,.jbuilder,}"

2. テンプレートのクエリをかける

# actionview/lib/action_view/template/resolver.rb

def query(path, details, formats, outside_app_allowed)
  query = build_query(path, details)

  template_paths = find_template_paths(query)
  ......
end

def find_template_paths(query)
  Dir[query].uniq.reject do |filename|
    File.directory?(filename) ||
      !File.fnmatch(query, filename, File::FNM_EXTGLOB)
  end
end

3. 見つかったテンプレートでAcrtionView::Templateを初期化

# actionview/lib/action_view/template/resolver.rb

def query(path, details, formats, outside_app_allowed)
  query = build_query(path, details)

  template_paths = find_template_paths(query)
  ......
  template_paths.map do |template|
    ......
    contents = File.binread(template)

    Template.new(contents, File.expand_path(template), handler,
      virtual_path: path.virtual,
      format: format,
      variant: variant,
      updated_at: mtime(template)
    )
  end
end

まとめ

テンプレート探索の道のりは非常に長く、個人的にも必要以上に長いと思います。テンプレート探索の実際のロジックは非常に素直かつシンプルですが、メソッド委譲が折り重なっていることで覆い隠されています。これに比べれば、本編であるテンプレートレンダリング(テンプレートオブジェクトから出力を得る)の方がずっと興味深く、かつテンプレートレンダリングのしくみはかなりよくできていると思います。そこで次回は、いよいよerbテンプレートがHTMLドキュメントに変わるまでを追いかけます。どうぞお見逃しなく😄

関連記事

Railsのフラグメントキャッシュを分解調査する(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。