读书人

Rails源码翻阅(九)ActionView:Base_

发布时间: 2012-09-20 09:36:51 作者: rapoo

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_bufferensure  self.output_buffer = old_output_bufferend?

?

我自己实验的一个例子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_bufferensure  self.output_buffer = old_output_bufferend

?

生成的方法名,是怎么规定的呢?(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使用_模板_定制

?

? ? ? ? ? ? ||

? ? ? ? ? ?| ?|

? ? ? ? ? | ? ?|

====结束====

=== ? ? ? ? ? ===

== ? ? ? ? ? ? ? ?==

= ? ? ? ? ? ? ? ? ? ? =

| ? ? ? ? ? ? ? ? ? ? ? |

?

?

?

?

?

?

?

?

?

?

读书人网 >网络基础

热点推荐