#397 Action View Walkthrough pro
- Download:
- source codeProject Files in Zip (30.1 KB)
- mp4Full Size H.264 Video (46.2 MB)
- m4vSmaller H.264 Video (25.4 MB)
- webmFull Size VP8 Video (24.3 MB)
- ogvFull Size Theora Video (68 MB)
In this episode we’ll continue the walk through the Rails source code that we started in episode 395 this time looking at the view layer. What exactly happens when Rails renders a template in response to a request? The first thing we’ll do is try to get some context as to where our code is being executed in the Rails stack so we’ll modify the template for a page in a Rails application to display the current class.
<h1>Articles</h1> <%= self.class %> <div id="articles"> <%= render @articles %> </div> <p><%= link_to "New Article", new_article_path %></p>
When we load this page now we’ll see what looks like an anonymous class which isn’t very helpful. If we change this to get the current class’s superclass
instead we’ll get a more useful result.
Now we’re getting somewhere. The code inside our layout file is executed in the context of ActionView::Base
; where is this in the Rails source code? We can get the Rails source code by cloning its Git repository by running the following command. We’ll then move into the rails
directory and check out the branch of the code that matches the version of Rails that our application is using.
$ git clone https://github.com/rails/rails.git $ cd rails $ git checkout v3.2.9
The view and controller code is under the actionpack/lib
directory and in here we’ll find the ActionView::Base
class. This class includes a number of modules, three of which are Helpers
, ::ERB::Util
and Context
. The Helpers
module includes a large number of other modules that contain all the helper methods that we use all the time in our views, while ::ERB::Util
contains code to handle escaping, such as HTML escaping. The Context
module is rather interesting. According to its documentation it can be used in another class that we want to act as a view context beside ActionView::Base
. There aren’t many examples of this in action but it seems like an interesting idea.
Following a Request
Now that we have some idea as to where the view code is located we’ll take a look at what happens when a request comes in to our index action. In episode 395 we walked through what happens in the controller layer when a request comes in. In the AbstractController::Rendering
module we have a render
method which is where the controller and view layers meet.
def render(*args, &block) options = _normalize_render(*args, &block) self.response_body = render_to_body(options) end
It’s this method that renders the template and sets it to the body of the response. Before it does the rendering it generates some options by calling _normalize_render
. This method calls _normalize_args
and if we pass it a symbol or a string it will convert it to an :action
option. It then calls _normalize_options
which in most cases will add :template
and :prefixes
options to our render call.
Lookup Contexts, View Contexts, View Renderers
Let’s go back to our Rails app to see what this is doing. We saw in episode 395 that if there’s no call to render
in an action it’s called implicitly without any arguments. In the case the :template
option is set to the name of the action and :prefixes
is set to the name of the controller and its superclasses, in this case to %w[articles application]
. This means that render
will look for an index
template in the articles
directory or in the application
directory if this doesn’t exist. Back in the render
method in the Rails source code after we normalize the options we call render_to_body
and this calls _render_template
.
def _render_template(options) #:nodoc: lookup_context.rendered_format = nil if options[:formats] view_renderer.render(view_context, options) end
This method first clears the lookup_context.rendered_format
if a :formats
option exists, then calls render
on a view renderer, passing in a view context. There are three different objects that we’re introducing here: view_context
, view_renderer
and lookup_context
and these are all related to rendering out the view. We’ll look at each one, starting with the view_context
.
This instantiates a new view_context_class
which calls ActionView::Base.prepare
. This method calls Class.new
which creates a new anonymous class and as we pass self
to it it will create a subclass of ActionView::Base
. This is the anonymous class that we saw at the beginning of this episode when we inspected it in the template.
def prepare(routes, helpers) #:nodoc: Class.new(self) do if routes include routes.url_helpers include routes.mounted_helpers end if helpers include helpers self.helpers = helpers end end end
This class inherits all the behaviour of ActionView::Base
and also includes a few modules passed in from the controller. This gives us some custom routing and helper behaviour so that each controller can have its own context that has specific methods in it. When the view_context_class
is instantiated we pass in a few arguments, including one called view_assigns
. This is where we transfer the instance variables from the controller to the view.
def view_assigns hash = {} variables = instance_variable_names variables -= protected_instance_variables variables -= DEFAULT_PROTECTED_INSTANCE_VARIABLES variables.each { |name| hash[name.to_s[1, name.length]] = instance_variable_get(name) } hash end
Here we grab the instance variable names from the controller and assign their values into a hash. This is passed in to ActionView::Base
when we instantiate it and an assign
method is then called which sets instance variables for each value in that hash. Some developers don’t like the fact that Rails does this as it goes against the grain of objects but it’s good to know that the code involved isn’t too complex.
Now that we have some idea as to how a view context is set up we’ll take a look at the lookup_context
next. This method is defined in the view_paths
module and it creates a new ActionView::LookupContext
, passing in the view paths along with a few other details.
def lookup_context @_lookup_context ||= ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes) end
To get a better understanding as to what view paths are we’ll take a look at this in the Rails console. If we call view_paths
on our ArticlesController
we get a PathSet
object back.
>> ArticlesController.view_paths => #<ActionView::PathSet:0x007fc2e1f49088 @paths=[/Users/eifion/blog/app/views]>
This object includes the paths to the views directory for that controller and there are ways to extend this and add other directory paths and episode 269 has more details on how to do this.
What does a LookupContext
do? This class is defined in the action_view
directory and as its name implies it’s used to look up templates so it makes sense that we need to pass our view paths into it. We’ll look at this class later when we look at rendering a template. Speaking of rendering let’s go back to the Rendering
module and take a look at the third piece of the puzzle: ActionView::Renderer
. We pass in the lookup context when this is instantiated and this is the object that we call render
on when we call render
in a controller.
# Main render entry point shared by AV and AC. def render(context, options) if options.key?(:partial) render_partial(context, options) else render_template(context, options) end end
The behaviour for rendering a partial and for rendering a template is quite different so this split is made early on. The render_template
method calls render on a TemplateRenderer
class. There’s quite a lot of code in that method but it basically fetches a template object then renders that template. Let’s see how we fetch the template first. This is done in a determine_template
method in TemplateRenderer
.
def determine_template(options) #:nodoc: keys = options[:locals].try(:keys) || [] if options.key?(:text) Template::Text.new(options[:text], formats.try(:first)) elsif options.key?(:file) with_fallbacks { find_template(options[:file], nil, false, keys, @details) } elsif options.key?(:inline) handler = Template.handler_for_extension(options[:type] || "erb") Template.new(options[:inline], "inline template", handler, :locals => keys) elsif options.key?(:template) options[:template].respond_to?(:render) ? options[:template] : find_template(options[:template], options[:prefixes], false, keys, @details) else raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file or :text option." end end
The behaviour in this method changes a lot depending on the options that are passed in. We’ll focus on the code that’s executed if a :template
option is passed in as this is the option that’s automatically added when we normalize the render options from the controller action. This code calls find_template
and this method is delegating to the lookup context. This makes sense since that is designed to fetch a template. In LookupContext
find_template
is delegated to find which uses the view paths to find it.
def find(name, prefixes = [], partial = false, keys = [], options = {}) @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options)) end alias :find_template :find
The @view_paths
variable is set to an instance of PathSet
and this class’s find
method looks for all the templates and fetches the first one. If it doesn’t find any it raises a MissingTemplate
exception.
def find(*args) find_all(*args).first || raise(MissingTemplate.new(self, *args)) end def find_all(path, prefixes = [], *args) 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
The find_all
method here loops through all the different paths that are available and returns a resolver which we call find_all
on. The Resolver
class lives in the template
directory and its find_all
method looks for a cached template that matches the details passed in and calls find_templates
if it fails to find one.
# Normalizes the arguments and passes it on to find_template. 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
The find_templates
method raises a NotImplementedError
if it’s called unless we’re calling it on a subclass, which is what we’re doing in our Rails application. The resolver can fetch templates in different ways depending on the class that we’re using and the Crafting Rails book has an example of how we can make a custom resolver. This method is also defined in the PathResolver
class which inherits from Resolver
and which is in the same file as it. This calls query
which is finally the code that reads the contents of the template file and creates a Template
object. We pass the file’s contents into this object and also a handler object which will interpret the template. Rails comes built-in with a couple of handlers for erb and XML builder but we can create our own as we demonstrated in episode 379.
Now that we have our template object let’s quickly review what it took to get it. Our template renderer delegated to the lookup context which in turn delegated to the path set and this delegated to the resolver. The code goes through quite a few layers but this allows for some flexibility in which layers we can swap out if we want to change some of the lookup behaviour.
Let’s go all the way back up the chain to our template renderer. We now know that its find_template
method ends up returning a Template
object. Once we have this object we can render it and this is done in a render_template
method. Before we attempt to render the template we first try to render it with a layout.
def render_template(template, layout_name = nil, locals = {}) view, locals = @view, locals || {} render_with_layout(layout_name, locals) do |layout| instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do template.render(view, locals) { |*name| view._layout_for(*name) } end end end
To fetch a layout render_with_layout
we call find_layout
which calls resolve_layout
and this fetches a layout in one of a variety of ways, most of which call find_template
. A layout template is fetched in the same way as a normal template and once we’ve fetched it we yield to the block that’s passed in which will render the template first and then the layout. The content of the rendered template goes into the layout wherever it calls yield
without an argument being passed in.
Next let’s find out what the render method on the Template
object does. The rendering behaviour is split into two parts. First the template is compiled which generates a new method on the view context. This method is then called on the view. This is done for performance reasons.
def render(view, locals, buffer=nil, &block) ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do compile!(view) view.send(method_name, locals, buffer, &block) end rescue Exception => e handle_render_error(view, e) end
The compile!
method ends up calling a compile
method but before that it checks to see which module it should add the method to. Ours will be added to ActionView::CompiledTemplates
which is a module that holds the compiled template code which is the module we’re adding the compile method to.
if view.is_a?(ActionView::CompiledTemplates) mod = ActionView::CompiledTemplates else mod = view.singleton_class end compile(view, mod)
This module is passed to the compile
method and this calls @handler.call
. If you’re familiar with template handlers you’ll know that this should return some Ruby code that needs to be compiled into a method. This code is added to the end of a dynamic method definition and this method is then added to the module.
def compile(view, mod) #:nodoc: 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 # rest of method omitted. end
The name of the compiled method is determined in the method_name
method.
def method_name #:nodoc: @method_name ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".gsub('-', "_") end
This will look familiar if you’ve ever seen an exception raised in the view. The method name is long and includes the name of the template and it’s here where it’s generated.
Back in the render method after compile!
is called we call view.send
and pass in the name of the generated method. It’s this that renders out the template and returns the string of content. The code defined in the compiled method it determined by the handler, in this case an erb template. The string of rendered content is returned in the template renderer from the render method and is eventually set to the response_body
in the Rendering
module.
There’s quite a bit going on behind the scenes when a request comes in to a Rails application and a template is rendered, but as there’s a lot of caching built into the framework so this all works fairly quickly. One thing we haven’t covered in this episode are the helper methods that can be called in a template such as render
or link_to
, but these are easy to explore on your own.