`
fantaxy025025
  • 浏览: 1250078 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类

Rails源码阅读(九)ActionView::Base_用户请求在rails中的处理流程(4)

 
阅读更多

Rails源码阅读(九)ActionView::Base_用户请求在rails中的处理流程(4)

 

衔接

ActionController中使用来@template.render来生成页面内容。这个@template就是ActionView::Base.new出来的实例。

ActionController中的具体代码:

response.template = ActionView::Base.new(self.class.view_paths, {}, self) 

分析:

#1 controller持有view的一个实例(@template)

   根据new方法和ActionView::Base的new参数可以,view也持有controller一个实例:

ActionView::Base的初始化方法:

    def initialize(view_paths = [], assigns_for_first_render = {}, controller = nil)#:nodoc:
      @assigns = assigns_for_first_render
      @assigns_added = nil
      @controller = controller #这里持有!!!
      @helpers = ProxyModule.new(self)
      self.view_paths = view_paths

      @_first_render = nil
      @_current_render = nil
    end
  

#2 self,即当前controller的实例传入来view中

   这样有个结论:view和controller互相持有对方的实例,难怪说VC不分家呢(放在一起的action_pack)

 

#3 ActionView里的view_paths是controller传入的,而且传入的是类的view_paths方法,即公共路径。

   因为controller里持有view的一个实例,因此可以在controller里面修改view的view_paths所包含的路径。

   例如Controller的实例方法:

 

      def prepend_view_path(path)
        @view_paths = superclass.view_paths.dup if !defined?(@view_paths) || @view_paths.nil?
        @view_paths.unshift(*path)
      end

 

   过程是复制一份class的view_paths,成为本实例的view文件寻找路径,这里例子只显示了怎么用:加入view_paths路径的顶端。

 

ActionView::Base中render的执行

 

也就是erb页面的生成过程。这个过程比较复杂的地方在于:参数多;套用模板;使用了helper;等。

render的代码分析:

    #返回值字符串
    #options里面的参数是Controller中传入的,例如:参数file的类型是ActionView::ReloadableTemplate。
    #可见控制都是在Controller做的。
    def render(options = {}, local_assigns = {}, &block) #:nodoc:
      local_assigns ||= {}

      case options
      when Hash                  #主要的入口开始=>
        options = options.reverse_merge(:locals => {})
        if options[:layout]      #:layout 入口
          _render_with_layout(options, local_assigns, &block)
        elsif options[:file]     #:file 文件入口
          template = self.view_paths.find_template(options[:file], template_format)
          template.render_template(self, options[:locals])
        elsif options[:partial]  #:partial 局部模板入口
          render_partial(options)
        elsif options[:inline]   #:inline 入口
          InlineTemplate.new(options[:inline], options[:type]).render(self, options[:locals])
        elsif options[:text]     #不需要处理
          options[:text]
        end
      when :update #ajax用的
        update_page(&block)
      else #可以看出来,在render的时候不写:partial => "xxx"则默认使用partial。
           #从团队合作和代码管理的角度来说,不建议这么写。直接抛异常算不算好呢?
        render_partial(:partial => options, :locals => local_assigns)
      end
    end

 

不论渲染什么,最终都要调用template的render_template方法。

而这个方法,会去找到相应的view模板处理器handler(例如ERB的handler),调用compile(template)方法,返回需要的字符串,再处理后,返回最终的结果。

 

详细分析ActionView::Base#render,如何使用layout等

 

1)#:file 

template = self.view_paths.find_template(options[:file], template_format)

template.render_template(self, options[:locals])

调用了ActionView::Template的render_template方法,这个方法又调

用了ActionView::Renderable的render(view, local_assigns),使用了compile(local_assigns),

继而使用了compile!(render_symbol, local_assigns)

在compile!(render_symbol, local_assigns)这里做了很多事情(略),结果是找到了正确的handler(拿erb来说)处理了view文件的内容,

把结果的erb.src生成的代码+local_assigns代码合在一起作为source,

用作ActionView::Base::CompiledTemplates.module_eval(source, filename, 0)

ActionView::Base::CompiledTemplates是在ActionViewBase中定义的过度模块,包含入这个模块的方

法(上面module_eval)会被include进ActionViewBase中

    module CompiledTemplates #:nodoc:
      # holds compiled template code
    end
    include CompiledTemplates

这样,local_assigns中的内容最终用作了方法内变量,避免全局变量的干扰。

 

compile!的细节:

生成了一个方法字符串,包括:local_assigns的变量等;包括erb解析的文件的src字符串内容,等。

例子:

实验的一个例子1:

render :file => "users/index", :locals => {:xxx_xxx_a => 300, :xxx_xxx_b => 400}

生成的一个source的内容,字符串:

"          def _run_erb_app47views47users47index46html46erb_locals_xxx_xxx_a_xxx_xxx_b(local_assigns)             old_output_buffer = output_buffer;xxx_xxx_a = local_assigns[:xxx_xxx_a];xxx_xxx_b = local_assigns[:xxx_xxx_b];;@output_buffer = '';  __in_erb_template=true ; @output_buffer.concat "users list"; @output_buffer           ensure             self.output_buffer = old_output_buffer           end "

整理一下,去掉字符串包装,最终好看的形式是:

def _run_erb_app47views47users47index46html46erb_locals_xxx_xxx_a_xxx_xxx_b(local_assigns)
  old_output_buffer = output_buffer
  ;xxx_xxx_a = local_assigns[:xxx_xxx_a]
  ;xxx_xxx_b = local_assigns[:xxx_xxx_b]
  ;
  ;@output_buffer = ''
  ;  __in_erb_template=true
  ; @output_buffer.concat "users list"
  ; @output_buffer
ensure
  self.output_buffer = old_output_buffer
end 

 

我自己实验的一个例子2:

render :file => "layout/application_layout" 

生成的一个source的内容:

大致同上,就不贴了

整理一下,最终好看的形式是:

def _run_erb_app47views47layouts47application_layout46html46erb(local_assigns)
  old_output_buffer = output_buffer
  ;
  ;@output_buffer = ''
  ;  __in_erb_template=true
  ; @output_buffer.concat "<html> \ n \ n<head> \ n <title> \ n "
  ; @output_buffer.concat(( " #{params[:controller]}##{params[:action]}" ).to_s)
  ; @output_buffer.concat "\n  </title>\n</head>\n\n\n<dody>\n\n  <div style=\"background-color: #ffebcd;\">\n    <h2>PAGE HEAD</h2>\n  </div>\n\n  <div>\n    "
  ; @output_buffer.concat(( yield ).to_s)
  ; @output_buffer.concat "\n  </div>\n\n  <div style=\"background-color: #f5f5dc;\">\n    <h2>PAGE FOOT</h2>\n  </div>\n\n</dody>\n\n\n</html>"
  ; @output_buffer
ensure
  self.output_buffer = old_output_buffer
end

 

生成的方法名,是怎么规定的呢?(47表示/,46表示.)

    def method_name(local_assigns)
      if local_assigns && local_assigns.any?
        method_name = method_name_without_locals.dup
        method_name << "_locals_#{local_assigns.keys.map { |k| k.to_s }.sort.join('_')}"
      else
        method_name = method_name_without_locals
      end
      method_name.to_sym
    end

 

生成的方法什么时候使用呢?在ActionView::Renderable#render方法:

    #ActionView::Renderable#render
    def render(view, local_assigns = {})
      compile(local_assigns)

      view.with_template self do
        view.send(:_evaluate_assigns_and_ivars)
        view.send(:_set_controller_content_type, mime_type) if respond_to?(:mime_type)

        view.send(method_name(local_assigns), local_assigns) do |*names| #在这里使用!!!
          ivar = :@_proc_for_layout
          if !view.instance_variable_defined?(:"@content_for_#{names.first}") && view.instance_variable_defined?(ivar) && (proc = view.instance_variable_get(ivar))
            view.capture(*names, &proc) #如果定义了proc的layout,会执行这里
          elsif view.instance_variable_defined?(ivar = :"@content_for_#{names.first || :layout}") #会走这里
            view.instance_variable_get(ivar)
          end
        end
      end
    end

view.send(method_name(local_assigns), local_assigns) do |*names| #在这里使用!!!

这里用send调用了生成的方法。

注意这里有个&block,如果生成的方法有yield的话,这个block就会执行,否则就不执行了。

看上面的例子,例子1没有yield,这样block不会被击中,仅仅执行方法。

例子2中有yield,不仅方法被执行,block也会执行。

 

进一步分析

如果在layout中使用的yield没有参数的话(yield),这样block接收的参数*names为空;

ivar = :"@content_for_#{names.first || :layout}" #ivar默认使用layout字符串生成@content_for_layout

这时会默认返回@content_for_layout这个实例变量的值,返回值插入buffer中:

  ; @output_buffer.concat(( yield ).to_s)

 

如果yield使用了参数,例如使用了yield :body_left,这个时候erb生成的src会类似这个样子:

  ; @output_buffer.concat(( yield :body_left ).to_s)

这样block中的参数个数是1,names.first会是:body_left #这样ivar为@content_for_body_left

这个时候会执行view.instance_variable_get(ivar)并返回,返回值插入yield :body_left的位置。

 

小结:

#1 :file的内容是在compile!里面用erb处理并返回了src代码,之后在module_eval执行,得到了的结果存入了@output_buffer中。

#2 没有使用layout。其实render :file就是去解析view模板并执行,作用是单一的,并不区分layout这个东西,layout属于控制端的事情,根据需要来定制。

#3 layout中,yield(:xxx = :layout)的位置,会用实例变量@content_for_xxx来代替

   例如:如果页面有内容<% @content_for_body_left = "I am Fantaxy!" %>,

        那么"I am Fantaxy!"会插入yield :body_left的位置。(注意是实例变量)

 

2)#:layout

    当渲染的页面有layout的时候,options会包含这两个参数:

    :file => view文件的路径

    :layout => 模板的路径

          _render_with_layout(options, local_assigns, &block)方法:

      def _render_with_layout(options, local_assigns, &block) #:nodoc:
        partial_layout = options.delete(:layout) #得到了layout的名字,删除了layout参数

        if block_given?
          begin
            @_proc_for_layout = block
            concat(render(options.merge(:partial => partial_layout)))
          ensure
            @_proc_for_layout = nil
          end
        else
          begin # 暂且关注主要逻辑,从这里开始
            original_content_for_layout = @content_for_layout if defined?(@content_for_layout)
            @content_for_layout = render(options) #1 当删除了layout参数后,这里变成了渲染:file,并得到内容保存在了实例变量@content_for_layout

            if (options[:inline] || options[:file] || options[:text])
              @cached_content_for_layout = @content_for_layout
              render(:file => partial_layout, :locals => local_assigns) #2 这里把模板当成普通的:file渲染
            else
              render(options.merge(:partial => partial_layout))
            end
          ensure
            @content_for_layout = original_content_for_layout
          end
        end
      end

    #1 渲染过程

        @content_for_layout = render(options) #1 当删除了layout参数后,options里剩下了:file,

        这样去渲染:file,得到渲染的内容保存在了实例变量@content_for_layout,这个值用处见上面。

        render(:file => partial_layout, :locals => local_assigns) #2 这里把模板当成普通的:file渲染

        渲染的中间,因为有yield,需要用到@content_for_xxx,把@content_for_xxx插入yield的位置(默认使用@content_for_layout)

    #2 begin 和 ensure的使用是为了保持原来环境

       比如,原来就有个变量就叫@content_for_xxx,这里的hack不会影响其他使用。很好很强大!

 

3)render :partial => "xxx_file_path"

    代码:render_partial(options) 调用链:

    调用:ActionView::Partials模块的render_partial(options = {})

    核心调用:_pick_partial_template(partial_path).render_partial(self, options[:object], local_assigns)

    调用:RenderablePartial#render_partial(view, object = nil, local_assigns = {}, as = nil)方法

    调用:render_template(view, local_assigns)

    调用:render

  这里主要是用了Template的render方法,这个方法调用compile!,这个方法详细见前面。

 

4)render :inline => 'xxx_ERB_string'

    代码:InlineTemplate.new(options[:inline], options[:type]).render(self, options[:locals])

    因为InlineTemplate中include了Renderable

    最终调用的还是ActionView::Renderable#render方法

  这里有点不同,InlineTemplate.new里面,把source hack进去了。代码:

    #ActionView::InlineTemplate#initialize

    def initialize(source, type = nil)

      @source = source #这里!!!

      @extension = type

      @method_segment = "inline_#{@source.hash.abs}"

    end

  这样compile!方法从erb模板中去解析source这一步,在ActionView::TemplateHandlers::ERB#compile,就跳过去了。

  所以,:inline方法,比:text好的地方在于,:inline的值可以是erb格式的字符串。(谁这么用啊,是不是rails该好好精简了,花哨的不要)

 

总结:

 

#1 两个render方法位置不同,前者主导,前者调用后者

    ActionController::Base#render

    ActionView::Base#render

#2 ActionView::Base#render杂性和脉络

    view是给用户看的,需求变化差异很大,属于web中最杂乱的地方,不容易形成统一的模式。

    rails中为了使用上的简单,一致等,把erb,js,ajax等都柔和到一起;

    支持了layout;支持了rjs,xml,erb等模板;

  但复杂是慢慢填上去的,脉络一致,基本符合ERB模板渲染的过程。

  可以参考原理介绍:动手写rails(二)Rails_Ruby_ERB使用_模板_定制

 

            ||

           |  |

          |    |

====结束====

===           ===

==                ==

=                     =

|                       |

 

 

 

 

 

 

 

 

 

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics