从这章开始解析ActionView的代码,Rails处理View的逻辑相当复杂,需要非常小心的将其一一分解。这里先从第一步,Mime开始。
Rails关于Mime的功能分在多个文件中,以Mime
模块为基本模块,定义在actionpack-3.2.13/lib/action_dispatch/http/mime_type.rb
中。Mime::Type
是核心的数据类型,然后是Mime::Mimes
是Array
的子类,其实例对象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_context
的formats
和rendered_format
上。
这里我们接触到了lookup_context
,这个方法定义在actionpack-3.2.13/lib/abstract_controller/view_paths.rb
的AbstractController::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.rb
的ActionController::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
其中:partial
和template
都设置成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机制的具体解析将在下一个章节展开。
接着将routes
和helpers
传入到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_helpers
,mounted_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
这里仅仅是简单的将controller
,request
和config
赋值在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::Renderer
的render
方法了:
# 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则是FallbackFileSystemResolver
的instances
方法的返回值,至于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_template
在TemplateRenderer
中其实是@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_paths
的find
过程,@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,将prefixes
与path
传入,直到搜索到第一个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_all
较find_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
正如注释所言,cached
在caching?
打开的时候表明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
调用了Path
的build
方法:
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_query
是OptimizedFileSystemResolver
里的实现,专门为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_layout
和template.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,开始调用template
的render
方法:
# 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
这个方法名届时将存储模版编译的结果。
随后就开始调用@handler
的call
方法对模版进行编译,这里以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_flow
的layout
变量中以便过会使用,这里的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::RenderingHelper
的render
方法:
# 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::Renderer
的render
方法可知,参数中有: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的过程就完成了。