先看源码再分析
1,action_view.rb
$:.unshift(File.dirname(__FILE__) + "/action_view/vendor")
require 'action_view/base'
require 'action_view/partials'
ActionView::Base.class_eval do
include ActionView::Partials
end
ActionView::Base.load_helpers(File.dirname(__FILE__) + "/action_view/helpers/")
还是require base.rb/partials.rb,然后helpers目录下有许多helpers模块都include进来
2,base.rb
module ActionView
class Base
module CompiledTemplates
end
include CompiledTemplates
class ObjectWrapper < Struct.new(:value)
end
def self.load_helpers(helper_dir)
Dir.entries(helper_dir).sort.each do |helper_file|
next unless helper_file =~ /^([a-z][a-z_]*_helper).rb$/
require File.join(helper_dir, $1)
helper_module_name = $1.camelize
class_eval("include ActionView::Helpers::#{helper_module_name}") if Helpers.const_defined?(helper_module_name)
end
end
def render(options = {}, old_local_assigns = {}, &block)
if options.is_a?(String)
render_file(options, true, old_local_assigns)
elsif options == :update
update_page(&block)
elsif options.is_a?(Hash)
options[:locals] ||= {}
options[:use_full_path] = options[:use_full_path].nil? ? true : options[:use_full_path]
if options[:file]
render_file(options[:file], options[:use_full_path], options[:locals])
elsif options[:partial] && options[:collection]
render_partial_collection(options[:partial], options[:collection], options[:spacer_template], options[:locals])
elsif options[:partial]
render_partial(options[:partial], ActionView::Base::ObjectWrapper.new(options[:object]), options[:locals])
elsif options[:inline]
render_template(options[:type] || :rhtml, options[:inline], nil, options[:locals] || {})
end
end
end
def render_file(template_path, use_full_path = true, local_assigns = {})
@first_render ||= template_path
if use_full_path
template_path_without_extension, template_extension = path_and_extension(template_path)
if template_extension
template_file_name = full_template_path(template_path_without_extension, template_extension)
else
template_extension = pick_template_extension(template_path).to_s
template_file_name = full_template_path(template_path, template_extension)
end
else
template_file_name = template_path
template_extension = template_path.split('.').last
end
template_source = nil
begin
render_template(template_extension, template_source, template_file_name, local_assigns)
rescue Exception => e
if TemplateError === e
e.sub_template_of(template_file_name)
raise e
else
raise TemplateError.new(@base_path, template_file_name, @assigns, template_source, e)
end
end
end
def render_template(template_extension, template, file_path = nil, local_assigns = {})
if handler = @@template_handlers[template_extension]
template ||= read_template_file(file_path, template_extension)
delegate_render(handler, template, local_assigns)
else
compile_and_render_template(template_extension, template, file_path, local_assigns)
end
end
def compile_and_render_template(extension, template = nil, file_path = nil, local_assigns = {})
local_assigns = local_assigns.symbolize_keys if @@local_assigns_support_string_keys
if compile_template?(template, file_path, local_assigns)
template ||= read_template_file(file_path, extension)
compile_template(extension, template, file_path, local_assigns)
end
method_name = @@method_names[file_path || template]
evaluate_assigns
send(method_name, local_assigns) do |*name|
instance_variable_get "@content_for_#{name.first || 'layout'}"
end
end
private
def read_template_file(template_path, extension)
File.read(template_path)
end
def create_template_source(extension, template, render_symbol, locals)
if template_requires_setup?(extension)
body = case extension.to_sym
when :rxml
"controller.response.content_type ||= 'application/xml'\n" +
"xml = Builder::XmlMarkup.new(:indent => 2)\n" +
template
when :rjs
"controller.response.content_type ||= 'text/javascript'\n" +
"update_page do |page|\n#{template}\nend"
end
else
body = ERB.new(template, nil, @@erb_trim_mode).src
end
@@template_args[render_symbol] ||= {}
locals_keys = @@template_args[render_symbol].keys | locals
@@template_args[render_symbol] = locals_keys.inject({}) { |h, k| h[k] = true; h }
locals_code = ""
locals_keys.each do |key|
locals_code << "#{key} = local_assigns[:#{key}]\n"
end
"def #{render_symbol}(local_assigns)\n#{locals_code}#{body}\nend"
end
def compile_template(extension, template, file_name, local_assigns)
render_symbol = assign_method_name(extension, template, file_name)
render_source = create_template_source(extension, template, render_symbol, local_assigns.keys)
line_offset = @@template_args[render_symbol].size
if extension
case extension.to_sym
when :rxml, :rjs
line_offset += 2
end
end
begin
unless file_name.blank?
CompiledTemplates.module_eval(render_source, file_name, -line_offset)
else
CompiledTemplates.module_eval(render_source, 'compiled-template', -line_offset)
end
rescue Exception => e
if logger
logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
logger.debug "Function body: #{render_source}"
logger.debug "Backtrace: #{e.backtrace.join("\n")}"
end
raise TemplateError.new(@base_path, file_name || template, @assigns, template, e)
end
@@compile_time[render_symbol] = Time.now
end
end
end
我们看到create_template_source方法中,对rxml后缀的模板使用Builder::XmlMarkup解析,对rjs后缀的模板使用update_page语句解析,而对rhtml则使用erb来解析
而compile_template方法则是对render_source调用CompiledTemplates.module_eval,由于render_source都是一些Ruby语句,所以module_eval会解析这些语句
render方法则根据options为:file、:partial、:partial collection、:inline来分发render方法
3,erb.rb
class ERB
class Compiler
Scanner.default_scanner = TrimScanner
class Scanner
SplitRegexp = /(<%%)|(%%>)|(<%=)|(<%#)|(<%)|(%>)|(\n)/
def scan; end
end
class TrimScanner < Scanner
TrimSplitRegexp = /(<%%)|(%%>)|(<%=)|(<%#)|(<%)|(%>\n)|(%>)|(\n)/
def scan(&block)
@stag = nil
if @percent
@src.each do |line|
percent_line(line, &block)
end
else
@src.each do |line|
@scan_line.call(line, &block)
end
end
nil
end
def percent_line(line, &block)
if @stag || line[0] != ?%
return @scan_line.call(line, &block)
end
line[0] = ''
if line[0] == ?%
@scan_line.call(line, &block)
else
yield(PercentLine.new(line.chomp))
end
end
def scan_line(line)
line.split(SplitRegexp).each do |token|
next if token.empty?
yield(token)
end
end
end
def compile(s)
out = Buffer.new(self)
content = ''
scanner = make_scanner(s)
scanner.scan do |token|
if scanner.stag.nil?
case token
when PercentLine
out.push("#{@put_cmd} #{content.dump}") if content.size > 0
content = ''
out.push(token.to_s)
out.cr
when :cr
out.cr
when '<%', '<%=', '<%#'
scanner.stag = token
out.push("#{@put_cmd} #{content.dump}") if content.size > 0
content = ''
when "\n"
content << "\n"
out.push("#{@put_cmd} #{content.dump}")
out.cr
content = ''
when '<%%'
content << '<%'
else
content << token
end
else
case token
when '%>'
case scanner.stag
when '<%'
if content[-1] == ?\n
content.chop!
out.push(content)
out.cr
else
out.push(content)
end
when '<%='
out.push("#{@insert_cmd}((#{content}).to_s)")
when '<%#'
# out.push("# #{content.dump}")
end
scanner.stag = nil
content = ''
when '%%>'
content << '%>'
else
content << token
end
end
end
out.push("#{@put_cmd} #{content.dump}") if content.size > 0
out.close
out.script
end
end
end
class ERB
def initialize(str, safe_level=nil, trim_mode=nil, eoutvar='_erbout')
@safe_level = safe_level
compiler = ERB::Compiler.new(trim_mode)
set_eoutvar(compiler, eoutvar)
@src = compiler.compile(str)
@filename = nil
end
def result(b=TOPLEVEL_BINDING)
if @safe_level
th = Thread.start {
$SAFE = @safe_level
eval(@src, b, (@filename || '(erb)'), 1)
}
return th.value
else
return eval(@src, b, (@filename || '(erb)'), 1)
end
end
end
class ERB
module Util
public
def html_escape(s)
s.to_s.gsub(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/</, "<")
end
alias h html_escape
module_function :h
module_function :html_escape
def url_encode(s)
s.to_s.gsub(/[^a-zA-Z0-9_\-.]/n){ sprintf("%%%02X", $&.unpack("C")[0]) }
end
alias u url_encode
module_function :u
module_function :url_encode
end
end
我们看到ERB初始化时调用了compiler.compile,compile方法首先调用默认的TrimScanner对模板scan并针对不同的标签进行处理,最后将结果写入Buffer
最后ERB的result方法使用Kernel#eval来处理模板文件并返回处理后的结果
Util模块定义了html_escape和url_encode方法,alias为h和u