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使用_模板_定制
||
| |
| |
====结束====
=== ===
== ==
= =
| |