Skip to content

Latest commit

 

History

History
1876 lines (1591 loc) · 66.7 KB

06.view.md

File metadata and controls

1876 lines (1591 loc) · 66.7 KB

An Http Request Through Rails

06. View

从这章开始解析ActionView的代码,Rails处理View的逻辑相当复杂,需要非常小心的将其一一分解。这里先从第一步,Mime开始。

Rails关于Mime的功能分在多个文件中,以Mime模块为基本模块,定义在actionpack-3.2.13/lib/action_dispatch/http/mime_type.rb中。Mime::Type是核心的数据类型,然后是Mime::MimesArray的子类,其实例对象Mime::SET存储了所有在Rails中注册过的Mime类型。Rails所有用到的Mime类型都必须经过注册,其中默认的注册代码全部写在actionpack-3.2.13/lib/action_dispatch/http/mime_types.rb

# Build list of Mime types for HTTP responses
# http://www.iana.org/assignments/media-types/

Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
Mime::Type.register "text/plain", :text, [], %w(txt)
Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )
Mime::Type.register "text/css", :css
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv

Mime::Type.register "image/png", :png, [], %w(png)
Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe)
Mime::Type.register "image/gif", :gif, [], %w(gif)
Mime::Type.register "image/bmp", :bmp, [], %w(bmp)
Mime::Type.register "image/tiff", :tiff, [], %w(tif tiff)

Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe)

Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
Mime::Type.register "application/rss+xml", :rss
Mime::Type.register "application/atom+xml", :atom
Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml )

Mime::Type.register "multipart/form-data", :multipart_form
Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form

# http://www.ietf.org/rfc/rfc4627.txt
# http://www.json.org/JSONRequest.html
Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )

Mime::Type.register "application/pdf", :pdf, [], %w(pdf)
Mime::Type.register "application/zip", :zip, [], %w(zip)

# Create Mime::ALL but do not add it to the SET.
Mime::ALL = Mime::Type.new("*/*", :all, [])

从代码中可见,这里大量调用了Mime::Type.register方法,其实现如下:

def register(string, symbol, mime_type_synonyms = [], extension_synonyms = [], skip_lookup = false)
  Mime.const_set(symbol.to_s.upcase, Type.new(string, symbol, mime_type_synonyms))

  SET << Mime.const_get(symbol.to_s.upcase)

  ([string] + mime_type_synonyms).each { |str| LOOKUP[str] = SET.last } unless skip_lookup
  ([symbol] + extension_synonyms).each { |ext| EXTENSION_LOOKUP[ext.to_s] = SET.last }
end

可以看到register的实现实际就是创建一个Mime::Type对象并赋值给Mime模块下的一个同名常量,然后把常量放入到SET对象中,然后将其中的Mime subtype部分放入到LOOKUP哈希中,将扩展名 - Mime::Type对象放入到EXTENSION_LOOKUP哈希中。随后,Mime提供了查询功能:

def lookup(string)
  LOOKUP[string]
end

def lookup_by_extension(extension)
  EXTENSION_LOOKUP[extension.to_s]
end

以上是Mime的基本部分,随后让我们开始真正的代码解析:

首先从scaffold默认生成的代码开始:

respond_to do |format|
  format.html # index.html.erb
  format.json { render json: @users }
end

首先,respond_to定义在ActionController::MimeResponds模块中,定义在actionpack-3.2.13/lib/action_controller/metal/mime_responds.rb,从名字即可看出,与Mime有关,实际当然也确实如此。在这个模块中,respond_to包含两个版本,类方法和实例方法。

其中类方法的示例如下:

class PeopleController < ApplicationController
  respond_to :html, :xml, :json

  def index
    @people = Person.all
    respond_with(@people)
  end
end

而实例方法的示例在一开始已经出现。这里我们只解析实例方法的实现,类方法实际上只是Controller级别的设置而已。respond_to实例方法的源码如下:

def respond_to(*mimes, &block)
  raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?

  if collector = retrieve_collector_from_mimes(mimes, &block)
    response = collector.response
    response ? response.call : default_render({})
  end
end

首先一开始是参数检查,这里通常只有block而没有参数。然后进入了retrieve_collector_from_mimes方法:

# Collects mimes and return the response for the negotiated format. Returns
# nil if :not_acceptable was sent to the client.
#
def retrieve_collector_from_mimes(mimes=nil, &block)
  mimes ||= collect_mimes_from_class_level
  collector = Collector.new(mimes)
  block.call(collector) if block_given?
  format = collector.negotiate_format(request)

  if format
    self.content_type ||= format.to_s
    lookup_context.formats = [format.to_sym]
    lookup_context.rendered_format = lookup_context.formats.first
    collector
  else
    head :not_acceptable
    nil
  end
end

首先,如果没有设置Mime的话,将试图从类级别的Mime中去取,方法就是collect_mimes_from_class_level,虽然这里我们也没有Mime,但是respond_to会在这里传入空数组,所以并没有去检查类级别的Mime。如果要检查的话,代码如下:

# Collect mimes declared in the class method respond_to valid for the
# current action.
#
def collect_mimes_from_class_level
  action = action_name.to_s

  self.class.mimes_for_respond_to.keys.select do |mime|
    config = self.class.mimes_for_respond_to[mime]

    if config[:except]
      !action.in?(config[:except])
    elsif config[:only]
      action.in?(config[:only])
    else
      true
    end
  end
end

可见也只是将类级别的配置取下后再用:except:only过滤下而已。

随后,将会根据Mime创建ActionController::MimeResponds::Collector的实例对象。Collector类的任务是收集并管理Rails所有的Mime类型。先来看看它的核心模块AbstractController::Collector的实现方法:

require "action_dispatch/http/mime_type"

module AbstractController
  module Collector
    def self.generate_method_for_mime(mime)
      sym = mime.is_a?(Symbol) ? mime : mime.to_sym
      const = sym.to_s.upcase
      class_eval <<-RUBY, __FILE__, __LINE__ + 1
        def #{sym}(*args, &block)                # def html(*args, &block)
          custom(Mime::#{const}, *args, &block)  #   custom(Mime::HTML, *args, &block)
        end                                      # end
      RUBY
    end

    Mime::SET.each do |mime|
      generate_method_for_mime(mime)
    end

  protected

    def method_missing(symbol, &block)
      mime_constant = Mime.const_get(symbol.to_s.upcase)

      if Mime::SET.include?(mime_constant)
        AbstractController::Collector.generate_method_for_mime(mime_constant)
        send(symbol, &block)
      else
        super
      end
    end
  end
end

这个module实际相当于一个抽象类,它提供了对Mime::SET,也就是Rails所有注册的Mime类型的同名方法集合,每个同名方法都是对custom方法的调用,而custom方法则在实际的类中实现,也就是这里ActionController::MimeResponds::Collector的实现:

class Collector
  include AbstractController::Collector
  attr_accessor :order, :format

  def initialize(mimes)
    @order, @responses = [], {}
    mimes.each { |mime| send(mime) }
  end

  def any(*args, &block)
    if args.any?
      args.each { |type| send(type, &block) }
    else
      custom(Mime::ALL, &block)
    end
  end
  alias :all :any

  def custom(mime_type, &block)
    mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
    @order << mime_type
    @responses[mime_type] ||= block
  end

  def response
    @responses[format] || @responses[Mime::ALL]
  end

  def negotiate_format(request)
    @format = request.negotiate_mime(order)
  end
end

custom方法将Mime类型放入了@order数组,然后创建了一个Hash @response来实现Mime类型到block的map。这个类的初始化代码对所有传入的Mime类型均调用了同名方法以实现前面叙述的功能。

不过这里我们依然会传入空数组。随后,将新创建的collector对象传入到block中去。也就是我们在Scaffold代码中看到的format对象。随后,Scaffold代码中通常定义了html和json两种Mime类型,按照之前解析的代码完成了声明之后,将调用collector的negotiate_format方法,这个方法相当于一个代理方法:

def negotiate_format(request)
  @format = request.negotiate_mime(order)
end

随后将进入request对象的negotiate_mime方法,这个方法定义在ActionDispatch::Http::MimeNegotiation模块中,actionpack-3.2.13/lib/action_dispatch/http/mime_negotiation.rb文件内。该模块的主要任务是,根据发送过来的请求的元信息,与传入的声明的Mime类型,最终可以得到一个匹配的Mime类型出来。negotiate_mime方法的实现是:

# Receives an array of mimes and return the first user sent mime that
# matches the order array.
#
def negotiate_mime(order)
  formats.each do |priority|
    if priority == Mime::ALL
      return order.first
    elsif order.include?(priority)
      return priority
    end
  end

  order.include?(Mime::ALL) ? formats.first : nil
end

formats方法确定了所有可以相匹配的Mime类型:

def formats
  @env["action_dispatch.request.formats"] ||=
    if parameters[:format]
      Array(Mime[parameters[:format]])
    elsif use_accept_header && valid_accept_header
      accepts
    elsif xhr?
      [Mime::JS]
    else
      [Mime::HTML]
    end
end

当请求的URL中有format参数时,将通过Type.lookup_by_extension方法根据扩展名查询对应的Mime。否则,如果没有关闭接受ACCEPT Header这个功能的话(use_accept_header的返回值要为true)并且用户请求的Mime类型符合要求(valid_accept_header的返回值要为true),将按照用户请求的Mime类型来处理,处理方法是accepts

# Returns the accepted MIME type for the request.
def accepts
  @env["action_dispatch.request.accepts"] ||= begin
    header = @env['HTTP_ACCEPT'].to_s.strip

    if header.empty?
      [content_mime_type]
    else
      Mime::Type.parse(header)
    end
  end
end

主要是调用Mime::Type.parse方法来处理:

def parse(accept_header)
  if accept_header !~ /,/
    accept_header = accept_header.split(Q_SEPARATOR_REGEXP).first
    if accept_header =~ TRAILING_STAR_REGEXP
      parse_data_with_trailing_star($1)
    else
      [Mime::Type.lookup(accept_header)]
    end
  else
    # keep track of creation order to keep the subsequent sort stable
    list, index = [], 0
    accept_header.split(/,/).each do |header|
      params, q = header.split(Q_SEPARATOR_REGEXP)
      if params.present?
        params.strip!

        if params =~ TRAILING_STAR_REGEXP
          parse_data_with_trailing_star($1).each do |m|
            list << AcceptItem.new(index, m.to_s, q)
            index += 1
          end
        else
          list << AcceptItem.new(index, params, q)
          index += 1
        end
      end
    end
    list.sort!

    # Take care of the broken text/xml entry by renaming or deleting it
    text_xml = list.index("text/xml")
    app_xml = list.index(Mime::XML.to_s)

    if text_xml && app_xml
      # set the q value to the max of the two
      list[app_xml].q = [list[text_xml].q, list[app_xml].q].max

      # make sure app_xml is ahead of text_xml in the list
      if app_xml > text_xml
        list[app_xml], list[text_xml] = list[text_xml], list[app_xml]
        app_xml, text_xml = text_xml, app_xml
      end

      # delete text_xml from the list
      list.delete_at(text_xml)

    elsif text_xml
      list[text_xml].name = Mime::XML.to_s
    end

    # Look for more specific XML-based types and sort them ahead of app/xml

    if app_xml
      idx = app_xml
      app_xml_type = list[app_xml]

      while(idx < list.length)
        type = list[idx]
        break if type.q < app_xml_type.q
        if type.name =~ /\+xml$/
          list[app_xml], list[idx] = list[idx], list[app_xml]
          app_xml = idx
        end
        idx += 1
      end
    end

    list.map! { |i| Mime::Type.lookup(i.name) }.uniq!
    list
  end
end

除了*号需要特殊处理外,大部分情况下还是简单的用Mime::Type.lookup查询即可。

formats完成查询之后,最终获得了所有可以接受的Mime列表。如果其中有Mime::ALL的话,那么取写在respond_to里的第一个类型,否则仅仅取出声明过的类型。

如果formats没有返回值的话,直接设置Header为not_acceptable即可。如果有的话,将返回值设置到lookup_contextformatsrendered_format上。

这里我们接触到了lookup_context,这个方法定义在actionpack-3.2.13/lib/abstract_controller/view_paths.rbAbstractController::ViewPaths模块中:

# LookupContext is the object responsible to hold all information required to lookup
# templates, i.e. view paths and details. Check ActionView::LookupContext for more
# information.
def lookup_context
  @_lookup_context ||=
    ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes)
end

这里self.class._view_paths默认是Rails根目录下的app/views目录,至于view_paths的作用之后将重点分析。details_for_lookup这里返回空Hash,_prefixes则是查询所有View Template可能的前缀:

# The prefixes used in render "foo" shortcuts.
def _prefixes
  @_prefixes ||= begin
    parent_prefixes = self.class.parent_prefixes
    parent_prefixes.dup.unshift(controller_path)
  end
end

先获取到类方法的parent_prefixes,然后将当前controller_path放入后返回。parent_prefixes的实现如下:

def parent_prefixes
  @parent_prefixes ||= begin
    parent_controller = superclass
    prefixes = []

    until parent_controller.abstract?
      prefixes << parent_controller.controller_path
      parent_controller = parent_controller.superclass
    end

    prefixes
  end
end

这里的实现就是不断获取父类的controller_path直到父类的controller为abstract?为止。 因此_prefixes实际上返回当前Controller到祖先的所有路径。

至于LookupContext的功能,就是封装了需要查找的Template的多个参数,以及存储了所有可用的Template的路径,并且提供了根据路径对Template进行查找的功能,具体实现会在下面解析。

随后回到respond_to的代码,如果之前能找到collector,那么接下来调用collector.response,实现如下:

def response
  @responses[format] || @responses[Mime::ALL]
end

之前说过,@responses记录了Mime类型到用于response的block的Hash,这里将返回这个block。在生成Scaffold代码的时候,html通常没有block,而json一般会有一句render语句。如果没有block的话,将会执行default_render方法,我们这里解析这个方法,这个方法的实现是这样的:

def default_render(*args)
  render(*args)
end

可见事实上还是一个render方法的调用。而default_render的调用形式是这样的:

response ? response.call : default_render({})

这里传入的是空的hash,相当于全部使用了默认值。

render方法分几层实现,这里的第一层是ActionController::Rendering的实现,定义在actionpack-3.2.13/lib/action_controller/metal/rendering.rb中:

# Check for double render errors and set the content_type after rendering.
def render(*args)
  raise ::AbstractController::DoubleRenderError if response_body
  super
  self.content_type ||= Mime[lookup_context.rendered_format].to_s
  response_body
end

仅仅只是检查response_body防止render两次,以及针对content_type的赋值,最后返回response_body的返回值。

随后是AbstractController::Rendering模块,定义在actionpack-3.2.13/lib/abstract_controller/rendering.rb中:

# Normalize arguments, options and then delegates render_to_body and
# sticks the result in self.response_body.
def render(*args, &block)
  options = _normalize_render(*args, &block)
  self.response_body = render_to_body(options)
end

这里分两步,_normalize_render主要负责生成render用的options,而render_to_body负责实际的render工作。下面是_normalize_render的实现:

# Normalize args and options.
# :api: private
def _normalize_render(*args, &block)
  options = _normalize_args(*args, &block)
  _normalize_options(options)
  options
end

这里同样分两步,_normalize_args负责初始化选项,它的实现也有多个层次,ActionController::Rendering中的实现是:

# Normalize arguments by catching blocks and setting them on :update.
def _normalize_args(action=nil, options={}, &blk)
  options = super
  options[:update] = blk if block_given?
  options
end

仅仅是将block赋值给了:update选项,下一层AbstractController::Rendering的实现是:

# Normalize args by converting render "foo" to render :action => "foo" and
# render "foo/bar" to render :file => "foo/bar".
# :api: plugin
def _normalize_args(action=nil, options={})
  case action
  when NilClass
  when Hash
    options = action
  when String, Symbol
    action = action.to_s
    key = action.include?(?/) ? :file : :action
    options[key] = action
  else
    options[:partial] = action
  end

  options
end

这里通过_normalize_args初始化了选项。从代码中可见,action如果是Hash的话(在目前的实例中action确实是一个空Hash),则直接赋值给选项(这里就可以简单的视为没有传入action参数,只给了options部分)。如果是字符串或是Symbol,则在options中加入:file或是:action选项。否则,将其赋值给:partial选项。

然后是_normalize_options的实现,同样有几层,定义在actionpack-3.2.13/lib/action_controller/metal/compatibility.rbActionController::Compatibility模块中的实现如下:

def _normalize_options(options)
  options[:text] = nil if options.delete(:nothing) == true
  options[:text] = " " if options.key?(:text) && options[:text].nil?
  super
end

这个方法主要设置:text的选项,保证这个参数如果有的话就必须是有至少一个空格的字符串。

ActionController::Rendering中的实现部分如下:

# Normalize both text and status options.
def _normalize_options(options)
  if options.key?(:text) && options[:text].respond_to?(:to_text)
    options[:text] = options[:text].to_text
  end

  if options[:status]
    options[:status] = Rack::Utils.status_code(options[:status])
  end

  super
end

这里主要是针对options[:text]调用to_text方法,以及调用Rack::Utils.status_code方法处理:status,这个方法当传入Symbol的时候会根据SYMBOL_TO_STATUS_CODE这个Hash将Status表示的语义转换成对应的状态码。

然后是AbstractController::Layouts的实现,代码在actionpack-3.2.13/lib/abstract_controller/layouts.rb

def _normalize_options(options)
  super

  if _include_layout?(options)
    layout = options.key?(:layout) ? options.delete(:layout) : :default
    options[:layout] = _layout_for_option(layout)
  end
end

首先判定是否存在layout,方法是_include_layout?

def _include_layout?(options)
  (options.keys & [:text, :inline, :partial]).empty? || options.key?(:layout)
end

如果没有指定:text:inline:partial这三个选项,或是指定了:layout,都被认为是有layout的。

对于有layout的情况,将会将:layout选项传入_layout_for_option,然后将返回值赋值给:layout选项。_layout_for_option的实现如下:

# Determine the layout for a given name, taking into account the name type.
#
# ==== Parameters
# * <tt>name</tt> - The name of the template
def _layout_for_option(name)
  case name
  when String     then _normalize_layout(name)
  when Proc       then name
  when true       then Proc.new { _default_layout(true)  }
  when :default   then Proc.new { _default_layout(false) }
  when false, nil then nil
  else
    raise ArgumentError,
      "String, true, or false, expected for `layout'; you passed #{name.inspect}"
  end
end

可以看到如果传入字符串,则相当于执行了layout的名字。那么如果传入的名字还没有带layout/,则在之前添加layout/使之成为layout文件所在路径。其余情况均返回proc,其中true:default将返回调用了_default_layout的proc,对于该方法的分析稍后进行。_normalize_options的最后一层是AbstractController::Rendering的实现:

# Normalize options.
# :api: plugin
def _normalize_options(options)
  if options[:partial] == true
    options[:partial] = action_name
  end

  if (options.keys & [:partial, :file, :template]).empty?
    options[:prefixes] ||= _prefixes
  end

  options[:template] ||= (options[:action] || action_name).to_s
  options
end

其中:partialtemplate都设置成action的名字作为默认值,:prefix之前已经描述过,当前及祖先所有Controller的名字。

随后就进入了render_to_body方法,一样的多层体系,第一层是ActionController::Compatibility的实现:

def render_to_body(options)
  options[:template].sub!(/^\//, '') if options.key?(:template)
  super || " "
end

仅仅只是除去:template选项的/符号,为了兼容性。然后是ActionController::Renderers的实现:

def render_to_body(options)
  _handle_render_options(options) || super
end

_handle_render_options的实现是:

def _handle_render_options(options)
  _renderers.each do |name|
    if options.key?(name)
      _process_options(options)
      return send("_render_option_#{name}", options.delete(name), options)
    end
  end
  nil
end

如果选项中存在之前声明过的Mime类型的话,这里就可以直接将对应的值返回回去。先执行_process_options方法,同样多层实现,ActionController::Stream是Rails中包含的可以用Streaming技术传输数据的模块,定义在actionpack-3.2.13/lib/action_controller/metal/streaming.rb中,实现如下:

# Set proper cache control and transfer encoding when streaming
def _process_options(options)
  super
  if options[:stream]
    if env["HTTP_VERSION"] == "HTTP/1.0"
      options.delete(:stream)
    else
      headers["Cache-Control"] ||= "no-cache"
      headers["Transfer-Encoding"] = "chunked"
      headers.delete("Content-Length")
    end
  end
end

这个方法在当:stream的选项为true的时候设置Streaming用的Header。随后是ActionController::Rendering

# Process controller specific options, as status, content-type and location.
def _process_options(options)
  status, content_type, location = options.values_at(:status, :content_type, :location)

  self.status = status if status
  self.content_type = content_type if content_type
  self.headers["Location"] = url_for(location) if location

  super
end

主要是将选项中的:status:content_type:location参数赋值给Controller。

随后执行这句语句:return send("_render_option_#{name}", options.delete(name), options)

并不是所有Mime类型都有相对的方法,事实上Rails只定义了三种常见的类型,代码在ActionController::Renderers::All里:

add :json do |json, options|
  json = json.to_json(options) unless json.kind_of?(String)
  json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
  self.content_type ||= Mime::JSON
  json
end

add :js do |js, options|
  self.content_type ||= Mime::JS
  js.respond_to?(:to_js) ? js.to_js(options) : js
end

add :xml do |xml, options|
  self.content_type ||= Mime::XML
  xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
end

对于其他类型,就必须自己编写响应的回调方法了。

如果选项并没有指定响应的Mime类型的话,那就接着执行render_to_body的方法,接着是AbstractController::Rendering的实现:

# Raw rendering of a template to a Rack-compatible body.
# :api: plugin
def render_to_body(options = {})
  _process_options(options)
  _render_template(options)
end

_process_options的功能已经详细描述,不再复述。至于_render_template也是多层实现,ActionController::Streaming的实现如下:

# Call render_to_body if we are streaming instead of usual +render+.
def _render_template(options)
  if options.delete(:stream)
    Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
  else
    super
  end
end

对于指定了Stream的情况,则创建相应的Rack::Chunked::Body对象返回,这里不详细解析这种情况。AbstractController::Rendering中的实现是:

# Find and renders a template based on the options given.
# :api: private
def _render_template(options)
  lookup_context.rendered_format = nil if options[:formats]
  view_renderer.render(view_context, options)
end

这里的重点是第二句语句,首先是view_renderer

# Returns an object that is able to render templates.
def view_renderer
  @_view_renderer ||= ActionView::Renderer.new(lookup_context)
end

ActionView::Renderer封装传入的lookup_context去实现多种关于render的操作。

view_context的实现是:

# An instance of a view class. The default view class is ActionView::Base
#
# The view class must have the following methods:
# View.new[lookup_context, assigns, controller]
#   Create a new ActionView instance for a controller
# View#render[options]
#   Returns String with the rendered template
#
# Override this method in a module to change the default behavior.
def view_context
  view_context_class.new(view_renderer, view_assigns, self)
end

其中view_context_class的实现是:

def view_context_class
  @_view_context_class ||= self.class.view_context_class
end

可以看到是一个类方法的代理,然后来看类方法:

def view_context_class
  @view_context_class ||= begin
    routes  = _routes  if respond_to?(:_routes)
    helpers = _helpers if respond_to?(:_helpers)
    ActionView::Base.prepare(routes, helpers)
  end
end

这里的_routes事实上定义在ActionDispatch::Routing::RouteSet中:

def url_helpers
  @url_helpers ||= begin
    routes = self

    helpers = Module.new do
      extend ActiveSupport::Concern
      include UrlFor

      @_routes = routes
      class << self
        delegate :url_for, :to => '@_routes'
      end
      extend routes.named_routes.module

      included do
        routes.install_helpers(self)
        singleton_class.send(:redefine_method, :_routes) { routes }
      end

      define_method(:_routes) { @_routes || routes }
    end

    helpers
  end
end

可以看到_routes其实是当前Rails的RouteSet对象。

至于_helpers则可以查看AbstractController::Helpers的代码,定义在actionpack-3.2.13/lib/abstract_controller/helpers.rb,其中有一段这样的代码:

included do
  class_attribute :_helpers
  self._helpers = Module.new

  class_attribute :_helper_methods
  self._helper_methods = Array.new
end

module ClassMethods
  # When a class is inherited, wrap its helper module in a new module.
  # This ensures that the parent class's module can be changed
  # independently of the child class's.
  def inherited(klass)
    helpers = _helpers
    klass._helpers = Module.new { include helpers }
    klass.class_eval { default_helper_module! unless anonymous? }
    super
  end
end

关于Helper机制的具体解析将在下一个章节展开。

接着将routeshelpers传入到ActionView::Base.prepare方法中,ActionView::Base是所有View代码最终的执行环境,定义在actionpack-3.2.13/lib/action_view/base.rb中:

# This method receives routes and helpers from the controller
# and return a subclass ready to be used as view context.
def prepare(routes, helpers)
  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

可以看到这里创建了一个新的类,并在这个新类中包含进了url_helpersmounted_helpers以及helpers三个模块。这三者构成了View执行时的方法集合。

随后来看view_assigns的实现:

# This method should return a hash with assigns.
# You can overwrite this configuration per controller.
# :api: public
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

在这里instance_variable_names指的是instance_variables的字符串版本,而protected_instance_variables定义在ActionController::Compatibility中:

self.protected_instance_variables = %w(
  @_status @_headers @_params @_env @_response @_request
  @_view_runtime @_stream @_url_options @_action_has_layout
)

DEFAULT_PROTECTED_INSTANCE_VARIABLES则还是定义在AbstractController::DEFAULT_PROTECTED_INSTANCE_VARIABLES中:

DEFAULT_PROTECTED_INSTANCE_VARIABLES = %w(
  @_action_name @_response_body @_formats @_prefixes @_config
  @_view_context_class @_view_renderer @_lookup_context
)

随后返回值hash就是所有剩余variables中变量与它的值的Hash。

接着就创建了ActionView::Base.prepare返回的匿名类的实例:

def initialize(context = nil, assigns = {}, controller = nil, formats = nil)
  @_config = ActiveSupport::InheritableOptions.new

  # Handle all these for backwards compatibility.
  # TODO Provide a new API for AV::Base and deprecate this one.
  if context.is_a?(ActionView::Renderer)
    @view_renderer = context
  elsif
    lookup_context = context.is_a?(ActionView::LookupContext) ?
      context : ActionView::LookupContext.new(context)
    lookup_context.formats  = formats if formats
    lookup_context.prefixes = controller._prefixes if controller
    @view_renderer = ActionView::Renderer.new(lookup_context)
  end

  assign(assigns)
  assign_controller(controller)
  _prepare_context
end

这里主要是后面三步,首先是assign方法:

def assign(new_assigns) # :nodoc:
  @_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end

这里事实上就是将前面获取到的变量的名值对赋进去。

然后是assign_controller的实现,这个实现定义在ActionView::Helpers::ControllerHelper,位置在actionpack-3.2.13/lib/action_view/helpers/controller_helper.rb中:

def assign_controller(controller)
  if @_controller = controller
    @_request = controller.request if controller.respond_to?(:request)
    @_config  = controller.config.inheritable_copy if controller.respond_to?(:config)
  end
end

这里仅仅是简单的将controllerrequestconfig赋值在View中。

随后是_prepare_context的实现,定义在ActionView::Context中,位置在actionpack-3.2.13/lib/action_view/context.rb中:

def _prepare_context
  @view_flow     = OutputFlow.new
  @output_buffer = nil
  @virtual_path  = nil
end

这里的OutputFlow将在下文重点分析。

随后真正进入了ActionView::Rendererrender方法了:

# 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

可以看到这里有两个分支,对于partial而言,render_partial是:

# Direct access to partial rendering.
def render_partial(context, options, &block)
  _partial_renderer.render(context, options, block)
end

def _partial_renderer
  @_partial_renderer ||= PartialRenderer.new(@lookup_context)
end

可以看到实现类的PartialRenderer,而render_template的实现是:

# Direct accessor to template rendering.
def render_template(context, options)
  _template_renderer.render(context, options)
end

def _template_renderer
  @_template_renderer ||= TemplateRenderer.new(@lookup_context)
end

可以看到实现类是TemplateRenderer。我们将在这里先解析TemplateRenderer,稍后则解析PartialRenderer

TemplateRenderer的位置在actionpack-3.2.13/lib/action_view/renderer/template_renderer.rb,主要负责对Template的render工作,它的render方法的实现是:

def render(context, options)
  @view    = context
  @details = extract_details(options)
  extract_format(options[:file] || options[:template], @details)
  template = determine_template(options)
  context  = @lookup_context

  unless context.rendered_format
    context.formats = template.formats unless template.formats.empty?
    context.rendered_format = context.formats.first
  end

  render_template(template, options[:layout], options[:locals])
end

extract_details只是从选项中找出details的部分返回。随后extract_format是个兼容性方法,看options[:file]options[:template]中是否存在扩展名,如果存在的话,取出给details赋值,并且给予过时警告。

随后就进入这里的主要方法determine_template

# Determine the template to be rendered using the given options.
def determine_template(options)
  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

可以看到这里集中处理了template的四种形式,:text:file:inline:template。其中:text创建了Template::Text的对象,这个对象实际上就是String的子类,定义在actionpack-3.2.13/lib/action_view/template/text.rb中。

:file:template实际非常接近,:file只是额外增加了fallback的功能,可以从with_fallbacks中看到:

# Add fallbacks to the view paths. Useful in cases you are rendering a :file.
def with_fallbacks
  added_resolvers = 0
  self.class.fallbacks.each do |resolver|
    next if view_paths.include?(resolver)
    view_paths.push(resolver)
    added_resolvers += 1
  end
  yield
ensure
  added_resolvers.times { view_paths.pop }
end

这里将类上的fallbacks返回值插入了view_paths后yield的block的代码。view_paths是就好像是UNIX系统搜索命令时候的$PATH一样,它是一个数组,里面每一个Resolver对象都提供了Rails路径使得Rails能够根据相对路径搜索到对应的Template。在LookupContext对象创建的时候,就自动将app/views目录设置为了唯一的view_paths,而这里的fallbacks则是FallbackFileSystemResolverinstances方法的返回值,至于FallbackFileSystemResolver的实现则在actionpack-3.2.13/lib/action_view/template/resolver.rb中,代码是:

# The same as FileSystemResolver but does not allow templates to store
# a virtual path since it is invalid for such resolvers.
class FallbackFileSystemResolver < FileSystemResolver
  def self.instances
    [new(""), new("/")]
  end

  def decorate(*)
    super.each { |t| t.virtual_path = nil }
  end
end

具体的实现实际都在该文件的它的基类中,但是从这段代码已经可以看到,实际上就是当前路径和系统根路径。

如果是:inline的时候可以直接创建Template对象,这个其实是find_template的最终结果,因此我们这里重点分析find_template,由于find_templateTemplateRenderer中其实是@lookup_context的代理方法,而这个代理方法在@lookup_context中的名字是find

def find(name, prefixes = [], partial = false, keys = [], options = {})
  @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options))
end

可以看到这个方法其实又是@view_paths的代理方法,不过先来关注args_for_lookup的实现吧:

def args_for_lookup(name, prefixes, partial, keys, details_options)
  name, prefixes = normalize_name(name, prefixes)
  details, details_key = detail_args_for(details_options)
  [name, prefixes, partial || false, details, details_key, keys]
end

这里分两个步骤,normalize_name主要是将Template名和前缀(也就是路径)重新分配:

# Support legacy foo.erb names even though we now ignore .erb
# as well as incorrectly putting part of the path in the template
# name instead of the prefix.
def normalize_name(name, prefixes)
  name  = name.to_s.sub(handlers_regexp) do |match|
    ActiveSupport::Deprecation.warn "Passing a template handler in the template name is deprecated. " \
      "You can simply remove the handler name or pass render :handlers => [:#{match[1..-1]}] instead.", caller
    ""
  end

  prefixes = nil if prefixes.blank?
  parts    = name.split('/')
  name     = parts.pop

  return name, prefixes || [""] if parts.empty?

  parts    = parts.join('/')
  prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]

  return name, prefixes
end

一开始依然警告用户不需要将扩展名写进template中。随后如果template中没有'/',则直接返回,否则将template中的文件名部分取出,之前的路径部分与prefixes一一结合,然后返回。

然后是detail_args_for的实现:

# Compute details hash and key according to user options (e.g. passed from #render).
def detail_args_for(options)
  return @details, details_key if options.empty? # most common path.
  user_details = @details.merge(options)
  [user_details, DetailsKey.get(user_details)]
end

这个实现就是将选项与当前@details合并后重新创建一个DetailsKey对象后与之前的合并结果一起返回。如果选项为空的话,则直接返回当前@details和当前details_key。事实上这里大部分情况下都是如此。

随后,我们开始了对@view_pathsfind过程,@view_paths的类型是ActionView::PathSet,定义在actionpack-3.2.13/lib/action_view/path_set.rb中:

def find(*args)
  find_all(*args).first || raise(MissingTemplate.new(self, *args))
end

可以看到主要是find_all方法返回值的第一个结果,如果找不到的话,就创建一个特殊的错误类型MissingTemplate,实现在actionpack-3.2.13/lib/action_view/template/error.rb中,这里不解析这个类型。

find_all的实现是:

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

从代码中可以看到,这里是将针对每个resolver,将prefixespath传入,直到搜索到第一个template为止。

接着来解析resolver.find_all的实现:

# 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

可以看到find_allfind_templates增加了Cache的功能,我们先看cached的实现:

# Handles templates caching. If a key is given and caching is on
# always check the cache before hitting the resolver. Otherwise,
# it always hits the resolver but check if the resolver is fresher
# before returning it.
def cached(key, path_info, details, locals)
  name, prefix, partial = path_info
  locals = locals.map { |x| x.to_s }.sort!

  if key && caching?
    @cached[key][name][prefix][partial][locals] ||= decorate(yield, path_info, details, locals)
  else
    fresh = decorate(yield, path_info, details, locals)
    return fresh unless key

    scope = @cached[key][name][prefix][partial]
    cache = scope[locals]
    mtime = cache && cache.map(&:updated_at).max

    if !mtime || fresh.empty?  || fresh.any? { |t| t.updated_at > mtime }
      scope[locals] = fresh
    else
      cache
    end
  end
end

# Ensures all the resolver information is set in the template.
def decorate(templates, path_info, details, locals)
  cached = nil
  templates.each do |t|
    t.locals         = locals
    t.formats        = details[:formats] || [:html] if t.formats.empty?
    t.virtual_path ||= (cached ||= build_path(*path_info))
  end
end

正如注释所言,cachedcaching?打开的时候表明cache永不过期,而关闭时总会执行block里的代码但是会根据传入的本地参数和修改时间返回新的结果还是cache中的结果。

另外,这里的build_path只是根据传入的参数获取Template更为明确的相关路径而已:

# Helpers that builds a path. Useful for building virtual paths.
def build_path(name, prefix, partial)
  Path.build(name, prefix, partial)
end

调用了Pathbuild方法:

def self.build(name, prefix, partial)
  virtual = ""
  virtual << "#{prefix}/" unless prefix.empty?
  virtual << (partial ? "_#{name}" : name)
  new name, prefix, partial, virtual
end

接着,我们来看被cached包裹起来的find_templates方法,注意这个方法在ActionView::Resolver本身没有定义,而其子类PathResolver中才有:

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

可以看到主要是query方法实现的搜索:

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

  # deals with case-insensitive file systems.
  sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] }

  template_paths = Dir[query].reject { |filename|
    File.directory?(filename) ||
      !sanitizer[File.dirname(filename)].include?(filename)
  }

  template_paths.map { |template|
    handler, format = extract_handler_and_format(template, formats)
    contents = File.binread template

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

query调用的第一个方法是build_query,从后面的Dir[query]可以很明显的看出,这个方法的目的在于生成glob,这里的build_queryOptimizedFileSystemResolver里的实现,专门为Rails设计:

def build_query(path, details)
  exts = EXTENSIONS.map { |ext| details[ext] }
  query = escape_entry(File.join(@path, path))

  query + exts.map { |ext|
    "{#{ext.compact.uniq.map { |e| ".#{e}," }.join}}"
  }.join
end

一开始的EXTENSIONS的值是[:locale, :formats, :handlers],正好是details三个主要的key。随后将view_paths的绝对路径@path和当前path合并后调用escape_entry,这个方法的实现是这样的:

def escape_entry(entry)
  entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
end

其实就是在特殊符号前加两个'\'转义。

随后,通过之前的exts变量生成一个由三个{}组成的glob字符串,每一项都是details里的一组值,用逗号分割,并且每个项都可以省略,效果就像这样:"{.en,}{.html,}{.erb,.builder,.coffee,}"

将路径query添加在前面就组成了完整的glob字符串。

随后其实就是用这个glob字符串找出所有符合条件的文件,同时又过滤掉了文件夹和隐藏文件。

对于每一个搜索到的路径,均取出文件名中的format部分和handler部分。同时又调用File.binread获取文件内容,然后就创建了ActionView::Template的实例。

find_template方法出来后,不要忘记会进入decorate方法中重新设置template对象的部分参数。

随后我们进入另外一个重要方法render_template

# Renders the given template. An string representing the layout can be
# supplied as well.
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

这里我们只需要看render_with_layouttemplate.render两个方法,其中render_with_layout的主要工作是查找响应的layout并且成为layout和template之间的桥梁:

def render_with_layout(path, locals)
  layout  = path && find_layout(path, locals.keys)
  content = yield(layout)

  if layout
    view = @view
    view.view_flow.set(:layout, content)
    layout.render(view, locals){ |*name| view._layout_for(*name) }
  else
    content
  end
end

其中find_layout的功能就是搜索相应的layout,它的实现是:

# This is the method which actually finds the layout using details in the lookup
# context object. If no layout is found, it checks if at least a layout with
# the given name exists across all details before raising the error.
def find_layout(layout, keys)
  with_layout_format { resolve_layout(layout, keys) }
end

这里先调用了with_layout_format方法:

# A method which only uses the first format in the formats array for layout lookup.
def with_layout_format
  if formats.size == 1
    yield
  else
    old_formats = formats
    _set_detail(:formats, formats[0,1])

    begin
      yield
    ensure
      _set_detail(:formats, old_formats)
    end
  end
end

这个方法约束了layout的format只能是所有候选的format中的第一个。

然后就是核心的resolve_layout的实现:

def resolve_layout(layout, keys)
  case layout
  when String
    begin
      if layout =~ /^\//
        with_fallbacks { find_template(layout, nil, false, keys, @details) }
      else
        find_template(layout, nil, false, keys, @details)
      end
    rescue ActionView::MissingTemplate
      all_details = @details.merge(:formats => @lookup_context.default_formats)
      raise unless template_exists?(layout, nil, false, keys, all_details)
    end
  when Proc
    resolve_layout(layout.call, keys)
  when FalseClass
    nil
  else
    layout
  end
end

从代码中可见,如果layout是字符串,则调用find_template来搜索layout的位置。如果是proc的话,则调用这个proc后重新执行该方法。

这里我们重点解析确定默认layout的方法,_default_layout

# Returns the default layout for this controller.
# Optionally raises an exception if the layout could not be found.
#
# ==== Parameters
# * <tt>require_layout</tt> - If set to true and layout is not found,
#   an ArgumentError exception is raised (defaults to false)
#
# ==== Returns
# * <tt>template</tt> - The template object for the default layout (or nil)
def _default_layout(require_layout = false)
  begin
    value = _layout if action_has_layout?
  rescue NameError => e
    raise e, "Could not render layout: #{e.message}"
  end

  if require_layout && action_has_layout? && !value
    raise ArgumentError,
      "There was no default layout for #{self.class} in #{view_paths.inspect}"
  end

  _normalize_layout(value)
end

这里的主要方法是_layout,而_layout其实是Rails动态生成的方法,它的生成代码是:

  # Creates a _layout method to be called by _default_layout .
  #
  # If a layout is not explicitly mentioned then look for a layout with the controller's name.
  # if nothing is found then try same procedure to find super class's layout.
  def _write_layout_method
    remove_possible_method(:_layout)

    prefixes    = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"]
    name_clause = if name
      <<-RUBY
        lookup_context.find_all("#{_implied_layout_name}", #{prefixes.inspect}).first || super
      RUBY
    end

    if defined?(@_layout)
      layout_definition = case @_layout
        when String
          @_layout.inspect
        when Symbol
          <<-RUBY
            #{@_layout}.tap do |layout|
              unless layout.is_a?(String) || !layout
                raise ArgumentError, "Your layout method :#{@_layout} returned \#{layout}. It " \
                  "should have returned a String, false, or nil"
              end
            end
          RUBY
        when Proc
          define_method :_layout_from_proc, &@_layout
          "_layout_from_proc(self)"
        when false
          nil
        when true
          raise ArgumentError, "Layouts must be specified as a String, Symbol, false, or nil"
        when nil
          name_clause
        end
    else
      # Add a deprecation if the parent layout was explicitly set and the child
      # still does a dynamic lookup. In next Rails release, we should @_layout
      # to be inheritable so we can skip the child lookup if the parent explicitly
      # set the layout.
      parent   = self.superclass.instance_eval { @_layout if defined?(@_layout) }
      @_layout = nil
      inspect  = parent.is_a?(Proc) ? parent.inspect : parent

      layout_definition = if parent.nil?
          name_clause
        elsif name
          <<-RUBY
            if template = lookup_context.find_all("#{_implied_layout_name}", #{prefixes.inspect}).first
              ActiveSupport::Deprecation.warn 'Layout found at "#{_implied_layout_name}" for #{name} but parent controller ' \
                'set layout to #{inspect.inspect}. Please explicitly set your layout to "#{_implied_layout_name}" ' \
                'or set it to nil to force a dynamic lookup.'
              template
            else
              super
            end
          RUBY
        end
    end

    self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
      def _layout
        if conditional_layout?
          #{layout_definition}
        else
          #{name_clause}
        end
      end
      private :_layout
    RUBY
  end
end

从代码中可见,如果之前定义过@_layout,那么这里就使用这个layout,否则将先试图中lookup_context.find_all方法搜索layout,如果搜索不到,且父类存在@_layout的话,调用super到父类进行搜索(通常是搜索一个名为application的layout)。

最后调用_normalize_layout方法,可以为找到的Template补充'/layout'前缀,如果没有的话:

def _normalize_layout(value)
  value.is_a?(String) && value !~ /\blayouts/ ? "layouts/#{value}" : value
end

这样,我们就找到了需要的layout的Template对象。

找到layout之后,就先进入block,开始调用templaterender方法:

# Render a template. If the template was not compiled yet, it is done
# exactly before rendering.
#
# This method is instrumented as "!render_template.action_view". Notice that
# we use a bang in this instrumentation because you don't want to
# consume this in production. This is only slow if it's being listened to.
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

随后执行compile!方法对模版进行编译:

# Compile a template. This method ensures a template is compiled
# just once and removes the source after it is compiled.
def compile!(view)
  return if @compiled

  # Templates can be used concurrently in threaded environments
  # so compilation and any instance variable modification must
  # be synchronized
  @compile_mutex.synchronize do
    # Any thread holding this lock will be compiling the template needed
    # by the threads waiting. So re-check the @compiled flag to avoid
    # re-compilation
    return if @compiled

    if view.is_a?(ActionView::CompiledTemplates)
      mod = ActionView::CompiledTemplates
    else
      mod = view.singleton_class
    end

    compile(view, mod)

    # Just discard the source if we have a virtual path. This
    # means we can get the template back.
    @source = nil if @virtual_path
    @compiled = true
  end
end

从代码中可见,每次compile之前都会上锁,然后只会进行一次compile的过程,之后就使用之前compile的结果了。这里调用compile方法做编译:

# Among other things, this method is responsible for properly setting
# the encoding of the compiled template.
#
# If the template engine handles encodings, we send the encoded
# String to the engine without further processing. This allows
# the template engine to support additional mechanisms for
# specifying the encoding. For instance, ERB supports <%# encoding: %>
#
# Otherwise, after we figure out the correct encoding, we then
# encode the source into <tt>Encoding.default_internal</tt>.
# In general, this means that templates will be UTF-8 inside of Rails,
# regardless of the original source encoding.
def compile(view, mod)
  encode!
  method_name = self.method_name
  code = @handler.call(self)

  # Make sure that the resulting String to be evalled is in the
  # encoding of the code
  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

  if source.encoding_aware?
    # Make sure the source is in the encoding of the returned code
    source.force_encoding(code.encoding)

    # In case we get back a String from a handler that is not in
    # BINARY or the default_internal, encode it to the default_internal
    source.encode!

    # Now, validate that the source we got back from the template
    # handler is valid in the default_internal. This is for handlers
    # that handle encoding but screw up
    unless source.valid_encoding?
      raise WrongEncodingError.new(@source, Encoding.default_internal)
    end
  end

  begin
    mod.module_eval(source, identifier, 0)
    ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
  rescue Exception => e # errors from template code
    if logger = (view && view.logger)
      logger.debug "ERROR: compiling #{method_name} RAISED #{e}"
      logger.debug "Function body: #{source}"
      logger.debug "Backtrace: #{e.backtrace.join("\n")}"
    end

    raise ActionView::Template::Error.new(self, {}, e)
  end
end

和所有Web框架一样,处理View的第一步就是处理Encoding。这里调用了encode!方法来处理:

# This method is responsible for properly setting the encoding of the
# source. Until this point, we assume that the source is BINARY data.
# If no additional information is supplied, we assume the encoding is
# the same as <tt>Encoding.default_external</tt>.
#
# The user can also specify the encoding via a comment on the first
# line of the template (# encoding: NAME-OF-ENCODING). This will work
# with any template engine, as we process out the encoding comment
# before passing the source on to the template engine, leaving a
# blank line in its stead.
def encode!
  return unless source.encoding_aware? && source.encoding == Encoding::BINARY

  # Look for # encoding: *. If we find one, we'll encode the
  # String in that encoding, otherwise, we'll use the
  # default external encoding.
  if source.sub!(/\A#{ENCODING_FLAG}/, '')
    encoding = magic_encoding = $1
  else
    encoding = Encoding.default_external
  end

  # Tag the source with the default external encoding
  # or the encoding specified in the file
  source.force_encoding(encoding)

  # If the user didn't specify an encoding, and the handler
  # handles encodings, we simply pass the String as is to
  # the handler (with the default_external tag)
  if !magic_encoding && @handler.respond_to?(:handles_encoding?) && @handler.handles_encoding?
    source
  # Otherwise, if the String is valid in the encoding,
  # encode immediately to default_internal. This means
  # that if a handler doesn't handle encodings, it will
  # always get Strings in the default_internal
  elsif source.valid_encoding?
    source.encode!
  # Otherwise, since the String is invalid in the encoding
  # specified, raise an exception
  else
    raise WrongEncodingError.new(source, encoding)
  end
end

由于ActionView和Ruby 1.9+的一样存在Magic encoding的功能,这里先在获取到得View文件的内容里用ENCODING_FLAG匹配,ENCODING_FLAG对应的值是#.*coding[:=]\s*(\S+)[ \t]*。如果匹配不到的话,就将编码设置为默认值,UTF-8。然后就调用force_encoding方法修改encoding。随后,如果没有被指定过Magic encoding,并且@handler承诺可以处理Encoding的话,就直接返回了View的内容,否则,就强制编码转换后返回。

然后用method_name生成一个唯一的方法名:

def method_name
  @method_name ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".gsub('-', "_")
end

def identifier_method_name
  inspect.gsub(/[^a-z_]/, '_')
end

这个方法名届时将存储模版编译的结果。

随后就开始调用@handlercall方法对模版进行编译,这里以ERb为例,ERb的Handler定义在actionpack-3.2.13/lib/action_view/template/handlers/erb.rb中:

def call(template)
  if template.source.encoding_aware?
    # First, convert to BINARY, so in case the encoding is
    # wrong, we can still find an encoding tag
    # (<%# encoding %>) inside the String using a regular
    # expression
    template_source = template.source.dup.force_encoding("BINARY")

    erb = template_source.gsub(ENCODING_TAG, '')
    encoding = $2

    erb.force_encoding valid_encoding(template.source.dup, encoding)

    # Always make sure we return a String in the default_internal
    erb.encode!
  else
    erb = template.source.dup
  end

  self.class.erb_implementation.new(
    erb,
    :escape => (self.class.escape_whitelist.include? template.mime_type),
    :trim => (self.class.erb_trim_mode == "-")
  ).src
end

可以看到ERb重新处理了encoding的过程,方法与之前的类似,唯一的区别就是ERb先把View内容转换成了二进制格式然后再搜索Magic encoding,其余内容均一致。

然后,创建了了self.class.erb_implementation的实例,在Rails 3.2.13版本中,这个值返回Erubis::Eruby的子类ActionView::Template::Handlers::Erubis,这个子类主要是在原来父类的基础上做一定的方法覆盖以满足Rails Html自动escape的功能。最后调用它的src方法即可完成compile的过程。

随后回到compile,这里生成了一段定义一个方法的代码,方法内容主要是暂时保存了@virtual_path@output_buffer,然后将之前的@locals数组变换成针对local_assigns的调用的代码,最后再附上之前Handler编译的结果,即可。

接着,对生成的这段代码再次做强制编码,随后在一个之前传入的module(之前用于执行View环境的module的一个祖先)中执行这段代码以生成目标方法,然后定义了针对GC的回调方法,回调的内容正是删除这个新增的方法:

# This finalizer is needed (and exactly with a proc inside another proc)
# otherwise templates leak in development.
Finalizer = proc do |method_name, mod|
  proc do
    mod.module_eval do
      remove_possible_method method_name
    end
  end
end

compile的过程就此结束,随后又回到render方法,立即调用刚才创建的那个方法,执行模版代码。

随后,render_with_layout方法里获取了模版的执行结果,把它存放在view_flowlayout变量中以便过会使用,这里的view_flow正是ActionView::OutputFlow的实例,这个类为View提供了一个存储编译结果的Hash,Hash中的值初始化为ActiveSupport::SafeBuffer的实例:

class OutputFlow
  attr_reader :content

  def initialize
    @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new }
  end

  # Called by _layout_for to read stored values.
  def get(key)
    @content[key]
  end

  # Called by each renderer object to set the layout contents.
  def set(key, value)
    @content[key] = value
  end

  # Called by content_for
  def append(key, value)
    @content[key] << value
  end

  # Called by provide
  def append!(key, value)
    @content[key] << value
  end
end

回到render_with_layout方法,然后执行layout.render方法对layout的模版进行执行,并且传入了view._layout_for方法对之前的编译结果进行调用。

layout模版引擎的执行和编译的过程与之前一致,不再重复。主要关心view._layout_for的实现,它的实现也有多个层次,第一层是ActionView::Helpers::RenderingHelper的实现,定义在actionpack-3.2.13/lib/action_view/helpers/rendering_helper.rb

def _layout_for(*args, &block)
  name = args.first

  if block && !name.is_a?(Symbol)
    capture(*args, &block)
  else
    super
  end
end

这个实现主要是capture的调用,但仅在传入block的时候起效,这里我们不需要研究这个实现。接下来一层是ActionView::Context的实现:

# Encapsulates the interaction with the view flow so it
# returns the correct buffer on yield. This is usually
# overwriten by helpers to add more behavior.
# :api: plugin
def _layout_for(name=nil)
  name ||= :layout
  view_flow.get(name).html_safe
end

这里将name默认设置成:layout,然后取值,即可得到之前的模版执行结果。由于这个结果之前已经经过了一次html_escape,所以这里将结果标记成html_safe,无需再次escape了。

将模版执行的结果与layout执行的结果合并,最后得到页面最终render的结果。

下面将解析partial render的代码,当在View中调用render方法调用partial的时候,将进入ActionView::Helpers::RenderingHelperrender方法:

# Returns the result of a render that's dictated by the options hash. The primary options are:
#
# * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt>.
# * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those.
# * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller.
# * <tt>:text</tt> - Renders the text passed in out.
#
# If no options hash is passed or :update specified, the default is to render a partial and use the second parameter
# as the locals hash.
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)
  end
end

从代码中可见,如果render方法后面没有跟Hash参数的话将直接进入render_partial方法,即是提供了Hash参数但是提供了block的话也一样进入render_partial方法。即使不提供block,从前面的ActionView::Rendererrender方法可知,参数中有:partial部分的话也可以进入render_partial方法:

# Direct access to partial rendering.
def render_partial(context, options, &block)
  _partial_renderer.render(context, options, block)
end

之前已经提到过,_partial_renderer生成PartialRenderer的实例,这个类定义在actionpack-3.2.13/lib/action_view/renderer/partial_renderer.rb中,其render方法的实现是:

def render(context, options, block)
  setup(context, options, block)
  identifier = (@template = find_partial) ? @template.identifier : @path

  @lookup_context.rendered_format ||= begin
    if @template && @template.formats.present?
      @template.formats.first
    else
      formats.first
    end
  end

  if @collection
    instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do
      render_collection
    end
  else
    instrument(:partial, :identifier => identifier) do
      render_partial
    end
  end
end

其中setup方法主要是设置instance variables:

def setup(context, options, block)
  @view   = context
  partial = options[:partial]

  @options = options
  @locals  = options[:locals] || {}
  @block   = block
  @details = extract_details(options)

  if String === partial
    @object     = options[:object]
    @path       = partial
    @collection = collection
  else
    @object = partial

    if @collection = collection_from_object || collection
      paths = @collection_data = @collection.map { |o| partial_path(o) }
      @path = paths.uniq.size == 1 ? paths.first : nil
    else
      @path = partial_path
    end
  end

  if @path
    @variable, @variable_counter = retrieve_variable(@path)
  else
    paths.map! { |path| retrieve_variable(path).unshift(path) }
  end

  if String === partial && @variable.to_s !~ /^[a-z_][a-zA-Z_0-9]*$/
    raise ArgumentError.new("The partial name (#{partial}) is not a valid Ruby identifier; " +
                            "make sure your partial name starts with a letter or underscore, " +
                            "and is followed by any combinations of letters, numbers, or underscores.")
  end

  extract_format(@path, @details)
  self
end

随后就是find_partial方法:

def find_partial
  if path = @path
    locals = @locals.keys
    locals << @variable
    locals << @variable_counter if @collection
    find_template(path, locals)
  end
end

这里主要还是调用find_template方法来搜索Partial用的Template:

def find_template(path=@path, locals=@locals.keys)
  prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
  @lookup_context.find_template(path, prefixes, true, locals, @details)
end

搜索Parital Template的功能与之前搜索Template的方法一致,区别仅在Path.build的时候,对于partial将在文件名前面加下划线区分。

随后就是通过render_partial完成实际的render过程,代码是:

def render_partial
  locals, view, block = @locals, @view, @block
  object, as = @object, @variable

  if !block && (layout = @options[:layout])
    layout = find_template(layout.to_s)
  end

  object ||= locals[as]
  locals[as] = object

  content = @template.render(view, locals) do |*name|
    view._layout_for(*name, &block)
  end

  content = layout.render(view, locals){ content } if layout
  content
end

这个代码和之前render template的时候依然很相似,只是layout变成了可选而已。这样Partial render的过程就完成了。