Disassembling Rails — Template Rendering (1)
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 😄