Disassembling Rails — Template Rendering (1)

Stan Lo
Ruby Inside
Published in
5 min readJul 4, 2018

--

This is the second post of the “Disassembling Rails” series. You don’t need to see the first post for reading this one, but I still recommend you to read it: Disassembling Rails — Fragment Caching.

Because Rails does a lot of things for rendering a template, I will use two posts to explain how Rails does it. In this post I’ll explain from the render method to how Rails find the template we want to render. And in the next post I’ll explain how a template object becomes the html that we can use for response. Let’s get started!

Files to checkout

If you want to take a look at the source code by yourself (which I recommend you to do), here’re the files you should checkout:

  • actionview/lib/action_view/helpers/rendering_helper.rb
  • actionview/lib/action_view/renderer/renderer.rb
  • actionview/lib/action_view/lookup_context.rb
  • actionview/lib/action_view/renderer/template_renderer.rb
  • actionview/lib/action_view/path_set.rb

User Interface

In Rails there are several ways to render a template, one of them is to use the #render method manually. So I’ll start from it

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

As you might expected, the render method comes from a helper, which is called 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

And after looking at the #render method, it’s pretty clear that there’s a thing called view_renderer which is responsible for rendering the template. The view_renderer is actually an ActionView::Renderer object, let’s take a look at its #render method:

# 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

It turns out there are two separate classes responsible for template rendering and partial rendering. In today’s post I will only introduce template rendering because partial rendering is more complicated.

One important thing we should also take a look here is render’s instance variable @lookup_context. It contains all the information we need for finding the target template. And it looks like this:

#<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,}">]>>

What’s ActionView::LookupContext

In my opinion, ActionView::LookupContext is the most important component in template rendering. Let’s take a closer look at its attributes, especially @details and @view_paths.

@details is a hash that contains locale, formats, variants and handlers. These informations has two usages:
1. They are a part of cache key that used to cache the found templates

# 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 uses them to filter template file’s extensions:

# 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

And @view_paths is an instance of ActionView::PathSet. The PathSet is a set of paths that we should find templates at. And each paths is guarded by an object called Resolver, which looks like:

#<ActionView::FileSystemResolver:0x00007fa8d5c9c7c0
# This is used for caching found templates
@cache=#<ActionView::Resolver::Cache:0x7fa8d5c9c680 keys=0 queries=0>,
# This is where we should find templates at
@path="/Users/st0012/projects/sample/app/views",
# This is the pattern we used to assemble the template query
# In most of the situation they're all the same
@pattern=":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}">

In normal cases, our app’s RAILS_PROJECT/app/views is one of the place we put view templates. Therefore, there will be a resolver that guards this place and help us find template from there. And if you use some rails engines like kaminari or devise, then you will also have a resolver that guards kaminari/app/views or devise/app/view to help us find templates from these gems.

So as you can see, LookupContext tells Rails where to find the templates and what template should it look for. This is why I say it’s the most important piece in template rendering.

How does Rails find the template?

Let’s back to 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

In order to render the template, we need to first get an template object, which is what I’m going to explain next. And because next few steps are just a series of method delegations so I’ll speedup a little bit. The following steps would be:
1. TemplateRenderer#find_template (delegate to @lookup_context)
2. LookupContext#find_template (alias to #find)
3. LookupContext#find(delegate to @view_paths)
4. PathSet#find calls #find_all calls #_find_all
5. PathSet#_find_all goes through each path (resolver)and call #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 calls PathResolver#find_template

And finally, we can now see the real template finding logic:

# 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

The actual 3 steps for finding the template

There are 3 major steps to actually find the template:

1. Building template query

# 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

And this is how the query looks like:

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

2. Querying 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)
......
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. Using found template to initialize an AcrtionView::Template object

# 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

Summary

Finding a template is a very long journey, actually I think it’s longer than it should be. The actual logic of template finding is very straightforward and simple, but is covered by a series of method delegations. Compare to this, rendering a template (from template object to end result) is far more interesting. And I think the mechanism it uses is quite brilliant. So in the next post I’m going to introduce how an erb template becomes a html document, don’t miss it 😄

--

--

Stan Lo
Ruby Inside

Creator of Goby language(https://github.com/goby-lang/goby), also a Rails/Ruby developer, Rails contributor. Love open source, cats and boxing.