论坛首页 编程语言技术论坛

Rails源码研究之ActionView:一,基本架构和ERB

浏览 4716 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2007-06-30  
先看源码再分析
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(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;")
    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
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics