2. About me
Stan Lo
GitHub: @st0012
Twitter: @_st0012
Email: stan001212@gmail.com
Work at Polydice(iCook)
Love to contribute open source projects, currently working on Goby
language
3. Steps for Rails to render template
1. Create the rendering context
2. Prepare for rendering
3. Find template and convert it into an object (most complicated
part)
4. Compile template object into a method
5. Call compiled method and get final result
4. 1. Rails creates an ActionView::Base instance as template
rendering's context.
2. Then collects and initializes the informations Rails needs for
finding a template.
3. Find the template file and use it to create an
ActionView::Template instance.
4. Compile the template object's content into ActionView::Base's
method
5. Call the compiled method on ActionView::Base's instance and
it'll return the final result.
6. Normally we render template in two ways
• Automatically rendering by controller action
class PostsController < ApplicationController
def index
@posts = Post.all
end
end
• Or call render manually
class PostsController < ApplicationController
def search
@posts = Post.search(params[:q])
render :index
end
end
7. Either way, they will both call
ActionView::Rendering#render_to_body
actionview/lib/action_view/rendering.rb
def render_to_body(options = {})
_process_options(options)
_render_template(options)
end
Then call #_render_template
def _render_template(options)
variant = options.delete(:variant)
assigns = options.delete(:assigns)
context = view_context
context.assign assigns if assigns
lookup_context.rendered_format = nil if options[:formats]
lookup_context.variants = variant if variant
view_renderer.render(context, options)
end
8. And I'll use this action as example to explain the rendering process
class PostsController < ActionController::Base
def index
@posts = Post.all
end
end
9. Here's the first phase of template rendering:
Create rendering context
10. Let's take a look a #_render_template
def _render_template(options)
variant = options.delete(:variant)
assigns = options.delete(:assigns)
context = view_context
context.assign assigns if assigns
lookup_context.rendered_format = nil if options[:formats]
lookup_context.variants = variant if variant
view_renderer.render(context, options)
end
11. And we can see the keyword: view_context
def _render_template(options)
variant = options.delete(:variant)
assigns = options.delete(:assigns)
context = view_context
context.assign assigns if assigns
lookup_context.rendered_format = nil if options[:formats]
lookup_context.variants = variant if variant
view_renderer.render(context, options)
end
12. view_context is an instance of view_context_class.
def view_context
view_context_class.new(view_renderer, view_assigns, self)
end
def view_context_class
@_view_context_class ||= self.class.view_context_class
end
Rails initializes it with view_renderer and view_assigns, which represents ActionView::Renderer and instance
variables created in controller. (In our case it's @posts)
13. And this is where the view_context_class comes from.
module ClassMethods
def view_context_class
@view_context_class ||= begin
supports_path = supports_path?
routes = respond_to?(:_routes) && _routes
helpers = respond_to?(:_helpers) && _helpers
Class.new(ActionView::Base) do
if routes
include routes.url_helpers(supports_path)
include routes.mounted_helpers
end
if helpers
include helpers
end
end
end
end
end
14. It turns out Rails creates an anonymous class (which inherites
from ActionView::Base) as view_context_class.
module ClassMethods
def view_context_class
@view_context_class ||= begin
supports_path = supports_path?
routes = respond_to?(:_routes) && _routes
helpers = respond_to?(:_helpers) && _helpers
Class.new(ActionView::Base) do
if routes
include routes.url_helpers(supports_path)
include routes.mounted_helpers
end
if helpers
include helpers
end
end
end
end
end
15. The code here creates an anonymous class that inherits
ActionView::Base
module ClassMethods
def view_context_class
@view_context_class ||= begin
supports_path = supports_path?
routes = respond_to?(:_routes) && _routes
helpers = respond_to?(:_helpers) && _helpers
Class.new(ActionView::Base) do
if routes
include routes.url_helpers(supports_path)
include routes.mounted_helpers
end
if helpers
include helpers
end
end
end
end
end
16. And it will also includes some helpers depends on the context
module ClassMethods
def view_context_class
@view_context_class ||= begin
supports_path = supports_path?
routes = respond_to?(:_routes) && _routes
helpers = respond_to?(:_helpers) && _helpers
Class.new(ActionView::Base) do
if routes
include routes.url_helpers(supports_path)
include routes.mounted_helpers
end
if helpers
include helpers
end
end
end
end
end
17. So view_context can be considered as an instance of
ActionView::Base, which will be created in every rendering.
And its main function is to provide isolated environment for every
template rendering.
19. Let's back to #_render_template. After creates the context, Rails
will call view_renderer's #render method.
def _render_template(options)
......
context = view_context
......
view_renderer.render(context, options)
end
And the view_renderer is actually an ActionView::Renderer's
instance.
22. It stores an instance variable called @lookup_context, which
carries some important info for finding our template. Such as
locale, format, handlers...etc.
class Renderer
attr_accessor :lookup_context
def initialize(lookup_context)
@lookup_context = lookup_context
end
end
23. This is the lookup context created in our example. We'll see it
again later.
#<ActionView::LookupContext:0x007f8763cee608
@cache=true,
@details=
{:locale=>[:en],
:formats=>[:html],
:variants=>[],
:handlers=>[:raw, :erb, :html, :builder, :ruby, :coffee, :jbuilder]},
@details_key=nil,
@prefixes=["posts", "application"],
@rendered_format=nil,
@view_paths=
#<ActionView::PathSet:0x007f8763cee428
@paths=
[#<ActionView::OptimizedFileSystemResolver:0x007f8763a2f548
@cache=#<ActionView::Resolver::Cache:0x7f8763a2f228 keys=1 queries=0>,
@path="/Users/stanlow/projects/sample/app/views",
@pattern=
":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}">]>>
24. Renderer also determines whether we are rendering a partial or a
template, and passes the informations to coorresponding renderer.
def render(context, options)
if options.key?(:partial)
render_partial(context, options)
else
render_template(context, options)
end
end
25. It uses the lookup context it holds to initialize corresponding
renderer.
module ActionView
class Renderer
# Direct access to template rendering.
def render_template(context, options) #:nodoc:
TemplateRenderer.new(@lookup_context).render(context, options)
end
# Direct access to partial rendering.
def render_partial(context, options, &block) #:nodoc:
PartialRenderer.new(@lookup_context).render(context, options, block)
end
end
end
26. I will explain the rest of processes using
TemplateRenderer#render as an example.
27. This is how TemplateRenderer#render looks like:
module ActionView
class TemplateRnderer
def render(context, options)
@view = context
@details = extract_details(options)
template = determine_template(options)
prepend_formats(template.formats)
@lookup_context.rendered_format ||= (template.formats.first || formats.first)
render_template(template, options[:layout], options[:locals])
end
end
end
28. As you can see, our next step is to find the template
module ActionView
class TemplateRnderer
def render(context, options)
@view = context
@details = extract_details(options)
template = determine_template(options)
prepend_formats(template.formats)
@lookup_context.rendered_format ||= (template.formats.first || formats.first)
render_template(template, options[:layout], options[:locals])
end
end
end
29. So here's the third phase: Finding Template
This is the most complicated part in the rendering process, so we'll spend a lot of time here.
30. After calling #determine_template, we then calls #find_template
module ActionView
class TemplateRenderer
def render(context, options)
......
template = determine_template(options)
......
end
def determine_template(options)
......
if ......
elsif options.key?(:template)
# Means Rails already found that template
if options[:template].respond_to?(:render)
options[:template]
else
find_template(options[:template], options[:prefixes], false, keys, @details)
end
........
end
end
end
end
31. But #find_template is actually delegated to @lookup_context
delegate :find_template, :find_file, :template_exists?,
:any_templates?, :with_fallbacks, :with_layout_format,
:formats, to: :@lookup_context
32. So let's take a look at @lookup_context again
#<ActionView::LookupContext:0x007f8763cee608
@cache=true,
@details=
{:locale=>[:en],
:formats=>[:html],
:variants=>[],
:handlers=>[:raw, :erb, :html, :builder, :ruby, :coffee, :jbuilder]},
@details_key=nil,
@prefixes=["posts", "application"],
@rendered_format=nil,
@view_paths=
#<ActionView::PathSet:0x007f8763cee428
@paths=
[#<ActionView::OptimizedFileSystemResolver:0x007f8763a2f548
@cache=#<ActionView::Resolver::Cache:0x7f8763a2f228 keys=1 queries=0>,
@path="/Users/stanlow/projects/sample/app/views",
@pattern=
":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}">]>>
33. It's an instance of ActionView::LookupContext.
And as I said before, it carries some important info for finding
templates like:
• Details
• Prefixes
• View Paths
34. Details: Contains several informations for building a template
searching query later
#<ActionView::LookupContext:0x007f8763cee608
@cache=true,
@details=
{:locale=>[:en],
:formats=>[:html],
:variants=>[],
:handlers=>[:raw, :erb, :html, :builder, :ruby, :coffee, :jbuilder]},
@details_key=nil,
@prefixes=["posts", "application"],
@rendered_format=nil,
@view_paths=
#<ActionView::PathSet:0x007f8763cee428
@paths=
[#<ActionView::OptimizedFileSystemResolver:0x007f8763a2f548
@cache=#<ActionView::Resolver::Cache:0x7f8763a2f228 keys=1 queries=0>,
@path="/Users/stanlow/projects/sample/app/views",
@pattern=
":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}">]>>
36. View Paths: It tells Rails where to look for the template files. For
example: our app's app/views
#<ActionView::LookupContext:0x007f8763cee608
@cache=true,
@details=
{:locale=>[:en],
:formats=>[:html],
:variants=>[],
:handlers=>[:raw, :erb, :html, :builder, :ruby, :coffee, :jbuilder]},
@details_key=nil,
@prefixes=["posts", "application"],
@rendered_format=nil,
@view_paths=
#<ActionView::PathSet:0x007f8763cee428
@paths=
[#<ActionView::OptimizedFileSystemResolver:0x007f8763a2f548
@cache=#<ActionView::Resolver::Cache:0x7f8763a2f228 keys=1 queries=0>,
@path="/Users/stanlow/projects/sample/app/views",
@pattern=
":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}">]>>
37. After received #find_template from template renderer,
LookupContext will call #find on its @view_paths.
module ActionView
class LookupContext
module ViewPaths
def find(name, prefixes = [], partial = false, keys = [], options = {})
@view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options))
end
alias :find_template :find
end
include ViewPaths
end
end
38. The @view_paths is an instance of ActionView::PathSet and
contains several resolver instances.
<ActionView::PathSet:0x007f87678f9648
@paths=
[#<ActionView::OptimizedFileSystemResolver:0x007f8763a2f548
@cache=
#<ActionView::Resolver::Cache:0x7f8763a2f228 keys=2 queries=0>,
@path=
"/Users/stanlow/projects/sample/app/views",
@pattern=
":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}">]>
39. Every resolver represents a place Rails should looking for
templates, such as app/views in your app or devise's app/views.
<ActionView::PathSet:0x007f87678f9648
@paths=
[
#<ActionView::OptimizedFileSystemResolver:0x007f8763a2f548
@cache=#<ActionView::Resolver::Cache:0x7f8763a2f228 keys=2 queries=0>,
@path="/Users/stanlow/projects/sample/app/views",
@pattern=":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}">
]>
41. After we call #find on PathSet, it will iterate through every
resolvers it has to look for templates.
module ActionView
class PathSet
def _find_all(path, prefixes, args, outside_app)
prefixes.each do |prefix|
paths.each do |resolver|
if outside_app
templates = resolver.find_all_anywhere(path, prefix, *args)
else
templates = resolver.find_all(path, prefix, *args)
end
return templates unless templates.empty?
end
end
[]
end
end
end
42. And every resolver uses #find_templates to search the template
we want from templates it holds.
module ActionView
class Resolver
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
def find_all_anywhere(name, prefix, partial=false, details={}, key=nil, locals=[])
cached(key, [name, prefix, partial], details, locals) do
find_templates(name, prefix, partial, details, true)
end
end
end
end
43. Here's the last step for finding a template, I'll split it into several
phases.
44. First, Rails will create a resolver path object.
def find_templates(name, prefix, partial, details, outside_app_allowed = false)
path = Path.build(name, prefix, partial)
......
end
Which looks like:
#<ActionView::Resolver::Path:0x007ffa3a5523a0
@name="index",
@partial=false,
@prefix="posts",
@virtual="posts/index">
45. Then we can use this path object and lookup context's details to
build a query
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
46. This query is a kind of formal language, you can consider it a
regular expression for matching file's path and extension.
"/Users/stanlow/projects/sample/app/views/posts/index{.en,}{.html,}
{}{.raw,.erb,.html,.builder,.ruby,.coffee,.jbuilder,}"
47. And every resolver will use this query to find the target in its
templates.
def query(path, details, formats, outside_app_allowed)
query = build_query(path, details)
template_paths = find_template_paths(query)
......
template_paths.map do |template|
handler, format, variant = extract_handler_and_format_and_variant(template, formats)
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
48. If it found a matched template, it returns its absolute path.
["/path_to_your_app/app/views/layouts/application.html.erb"]
49. Finally, Rails can use the absolute path it got to read the file and
use its content to initialize an ActionView::Template's instance.
def query(path, details, formats, outside_app_allowed)
query = build_query(path, details)
template_paths = find_template_paths(query)
......
template_paths.map do |template|
handler, format, variant = extract_handler_and_format_and_variant(template, formats)
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
50. The above is the third phase of template rendering in Rails.
52. After ActionView::TemplateRenderer found the template, it'll call
#render_template and pass the template object as a argument.
module ActionView
class TemplateRenderer < AbstractRenderer #:nodoc:
def render(context, options)
......
# Found your template
template = determine_template(options)
......
render_template(template, options[:layout], options[:locals])
end
end
end
53. Then Rails will call #render on the template object with locals
(local variables) and the view context.
def render_template(template, layout_name = nil, locals = nil) #:nodoc:
view, locals = @view, locals || {}
render_with_layout(layout_name, locals) do |layout|
instrument(:template, .....) do
# the block will only be execute if your template contains `yield`
template.render(view, locals) { |*name| view._layout_for(*name) }
end
end
end
54. And #render will compile the template into a method and call it
immediately.
def render(view, locals, buffer = nil, &block)
instrument_render_template do
compile!(view)
view.send(method_name, locals, buffer, &block)
end
rescue => e
handle_render_error(view, e)
end
55. Every template will only be compiled once, Rails will check if it has
been compiled before compiles it.
def compile!(view)
return if @compiled
@compile_mutex.synchronize do
return if @compiled
.....
instrument("!compile_template") do
compile(mod)
end
......
@compiled = true
end
end
56. And this is how Template#compile looks like.
def compile(mod)
encode!
method_name = self.method_name
code = @handler.call(self)
source = <<-end_src
def #{method_name}(local_assigns, output_buffer)
_old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};
_old_output_buffer = @output_buffer;#{locals_code};#{code}
ensure
@virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
end
end_src
......
mod.module_eval(source, identifier, 0)
......
end
57. The source part is a string template that will become a method
definition in Ruby.
source = <<-end_src
def #{method_name}(local_assigns, output_buffer)
_old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};
_old_output_buffer = @output_buffer;#{locals_code};#{code}
ensure
@virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
end
end_src
58. I'll explain this process with the example partial file: app/views/
users/_hello.html.erb:
Hello <%= user.name %>!
And we render it using
render partial: "users/hello", locals: { user: @user }
59. The source generated with our example would look like this:
"def _app_views_users__hello_html_erb___4079420934646067298_70142066674000(local_assigns, output_buffer)n
_old_virtual_path, @virtual_path = @virtual_path, "users/hello";
_old_output_buffer = @output_buffer
# locals_code
user = user = local_assigns[:user]
# code
@output_buffer = output_buffer || ActionView::OutputBuffer.new;
@output_buffer.safe_append='Hello '.freeze;
@output_buffer.append=( user.name );
@output_buffer.safe_append='!n'.freeze;
@output_buffer.to_sn
ensuren
@virtual_path, @output_buffer = _old_virtual_path, _old_output_buffern
endn"
60. First, we need to take a look at locals_code
"def _app_views_users__hello_html_erb___4079420934646067298_70142066674000(local_assigns, output_buffer)n
_old_virtual_path, @virtual_path = @virtual_path, "users/hello";
_old_output_buffer = @output_buffer
# locals_code
user = user = local_assigns[:user]
# code
@output_buffer = output_buffer || ActionView::OutputBuffer.new;
@output_buffer.safe_append='Hello '.freeze;
@output_buffer.append=( user.name );
@output_buffer.safe_append='!n'.freeze;
@output_buffer.to_sn
ensuren
@virtual_path, @output_buffer = _old_virtual_path, _old_output_buffern
endn"
61. Remember how we render our partial?
render partial: "users/hello", locals: { user: @user }
The local_assigns is actually the locals
{ user: @user }
So when we call local variable user in our view, it's actually
searching the key :user's value from the locals hash.
And this mapping is generated during compilation.
62. Then the code section will be generated differently according to the
template's format and handler. In our case we are rendering an erb
file so it's generated by ERB handler.
"def _app_views_users__hello_html_erb___4079420934646067298_70142066674000(local_assigns, output_buffer)n
......
# locals_code
user = user = local_assigns[:user]
# code
@output_buffer = output_buffer || ActionView::OutputBuffer.new;
@output_buffer.safe_append='Hello '.freeze;
@output_buffer.append=( user.name );
@output_buffer.safe_append='!n'.freeze;
@output_buffer.to_sn
ensuren
@virtual_path, @output_buffer = _old_virtual_path, _old_output_buffern
endn"
erb uses a buffer to store strings, it keeps appending string into
the buffer and output the final result with to_s method.
63. Now we have a complete method definition in source variable.
Rails will then use module_eval to make mod
(ActionView::CompiledTemplates) evaluate the it, so the module
will have that method.
def compile(mod)
....
mod.module_eval(source, identifier, 0)
...
end
65. This is the simplist phase, Rails just calls the method it defined
and it'll return the result.
def render(view, locals, buffer = nil, &block)
instrument_render_template do
compile!(view)
view.send(method_name, locals, buffer, &block)
end
rescue => e
handle_render_error(view, e)
end
66. To summarize
• Template rendering is a long journey, so it consumes a lot of
computing resources and time.
• Everytime we render a template, Rails creates an
ActionView::Base's instance as an isolated rendering
environment.
• Rails renders templates and partials differently.
67. To summarize
• LookupContext plays a key role in the rendering process since it
holds view paths and details for searching a template.
• A resolver represents a place to look for templates.
• Every template would be compiled into a method once it gets
rendered. So to some degree we can say rendering template is
just calling methods on ActionView::Base's instance.