完整trace在末尾

假设有sinatra script如下,有实例变量@a和局部变量b

get '/' do
  @a = "Hello, world"
  erb(:index, {}, {b: 123})
end


模板index.erb为

instance a: <%= @a %>
local b: <%= b %>


现跟踪该erb如何运作

sinatra中各种erb、erubis、haml、sass、scss,调用的都是render,render再用tilt来加载相应模板引擎来进行实际的render。于是用trace_tree来跟踪title的render方法

def erb(template, options = {}, locals = {}, &block)
  render(:erb, template, options, locals, &block)
end

def render(engine, data, options = {}, locals = {}, &block)
  locals          = options.delete(:locals) || locals         || {}
  scope           = options.delete(:scope)          || self
  # ...
  begin
    #...
    require 'trace_tree'
    output = binding.trace_tree(html: true, tmp: ['sinatra', 'render.html']) do
      template.render(scope, locals, &block)
    end
  ensure
    @default_layout = layout_was
  end
  # ...
  output
end


粗略地看,render干的就是两件事:动态定义一个方法,然后执行它,这就返回了填充好的模板了

def evaluate(scope, locals, &block)
  locals_keys = locals.keys
  if SYMBOL_ARRAY_SORTABLE
    locals_keys.sort!
  else
    locals_keys.sort!{|x, y| x.to_s <=> y.to_s}
  end
  method = compiled_method(locals_keys)
  method.bind(scope).call(locals, &block)
end


调用栈如下



compiled_method如下:动态定义方法,方法名含线程id以免重复,方法体是用字符串拼接(用lambda也可以,但这样就不像字符串那样好检查了),然后class_eval生成这个方法,再remove_method,以免污染,最后缓存在@compiled_method

TOPOBJECT = Object.superclass || Object

def compiled_method(locals_keys)
  LOCK.synchronize do
    @compiled_method[locals_keys] ||= compile_template_method(locals_keys)
  end
end

def compile_template_method(local_keys)
  source, offset = precompiled(local_keys)
  local_code = local_extraction(local_keys)

  method_name = "__tilt_#{Thread.current.object_id.abs}"
  method_source = String.new

  if method_source.respond_to?(:force_encoding)
    method_source.force_encoding(source.encoding)
  end

  method_source << <<-RUBY
    TOPOBJECT.class_eval do
      def #{method_name}(locals)
        Thread.current[:tilt_vars] = [self, locals]
        class << self
          this, locals = Thread.current[:tilt_vars]
          this.instance_eval do
            #{local_code}
  RUBY
  offset += method_source.count("\n")
  method_source << source
  method_source << "\nend;end;end;end"
  Object.class_eval(method_source, eval_file, line - offset)
  unbind_compiled_method(method_name)
end

def unbind_compiled_method(method_name)
  method = TOPOBJECT.instance_method(method_name)
  TOPOBJECT.class_eval { remove_method(method_name) }
  method
end


加入puts来检查method_source,可见模板如下(去掉头尾的TOPOBJECT.class_eval和end才是真正的方法定义)

        TOPOBJECT.class_eval do
          def __tilt_12082330(locals)
            Thread.current[:tilt_vars] = [self, locals]
            class << self
              this, locals = Thread.current[:tilt_vars]
              this.instance_eval do
                b = locals[:b]
        begin
          __original_outvar = @_out_buf if defined?(@_out_buf)

@_out_buf = _buf = String.new
 @_out_buf << 'instance a: '; @_out_buf << ( @a ).to_s; @_out_buf << '
'; @_out_buf << 'local b: '; @_out_buf << ( b ).to_s; @_out_buf << '
';
@_out_buf

        ensure
          @_out_buf = __original_outvar
        end

end;end;end;end


中间的@_out_buf那段来自于precompiled



precompiled的实现是一种template pattern,所有XXXTemplate都继承自Template,并重写precompiled(其实也有些是直接重写evaluate的,例如AsciidoctorTemplate,反正Template#render调用的是Template#evaluate,只要接口统一为evaluate就可以,也算是种adapter pattern吧)

module Tilt
  class ERBTemplate < Template
    #...
    if RUBY_VERSION >= '1.9.0'
      def precompiled(locals)
        source, offset = super
        [source, offset + 1]
      end
    end
  end
end


Template的precompiled会将重写过的precompiled_preamble,precompiled_template,precompiled_postamble拼在一起

def precompiled(local_keys)
  preamble = precompiled_preamble(local_keys)
  template = precompiled_template(local_keys)
  postamble = precompiled_postamble(local_keys)
  source = String.new
  #...
  source << preamble << "\n" << template << "\n" << postamble
  [source, preamble.count("\n")+1]
end


其中precompiled_template就是模板的生成。例如,ERBTemplate就是这样重写precompiled_template的

module Tilt
  class ERBTemplate < Template
    #...

    def prepare
      #...
      @engine = ::ERB.new(data, options[:safe], options[:trim], @outvar)
    end

    def precompiled_template(locals)
      source = @engine.src
      source
    end


演示一下平常地使用erb(不过tilt默认用erubis所以刚刚才生成的@_out_buf......不同)

irb(main):010:0> puts ERB.new('a: <%= @a %>').src
#coding:UTF-8
_erbout = ''; _erbout.concat "a: "; _erbout.concat(( @a ).to_s); _erbout.force_encoding(__ENCODING__)
=> nil


串联起来,为了能获取实例变量,模板函数要绑定在传入的scope(原Sinatra::Base用dup复制出的self)上执行

method.bind(scope).call(locals, &block)


而局部变量,如上所示,作为方法参数传入,而方法体早已用local_extraction写死,得出诸如b = locals[:b]的语句

def local_extraction(local_keys)
  local_keys.map do |k|
    if k.to_s =~ /\A[a-z_][a-zA-Z_0-9]*\z/
      "#{k} = locals[#{k.inspect}]"
    else
      raise "invalid locals key: #{k.inspect} (keys must be variable names)"
    end
  end.join("\n")
end


至此,模板填充完毕。

完整trace如下

20170222_render.html