不可小视的ERB和capture
当前位置:首页 ----> Web开发 ----> Ruby/Python
关键词:ERB,capture,Rails,ActionView,capture helper Rails,layout,rhtml,users index,sitemsh,taglib,sitemesh,head,body,menu,html,content,block,helper,yield
liusong1111:
相关文章: X    Rails源码研究之ActionView:七,capture_helper  Rails源码研究之ActionView:一,基本架构和ERB    <img src=http://www.javaeye.com/images/icon_more.gif/>          回顾一个熟悉的场景: 在某个layout文件里(如layouts/application.rhtml):
是个占位符,当浏览/users/index时,它就被 index.rhtml(原始页面) 生成的内容替换了。  输出结果。sitemsh是用taglib做占位符。 细心的看官说,等等,人家sitemesh很体贴,把原始页面的head和body部份分别解析出来了,可以在模板的不同位置分别引用:
,代表的是整个的原始页面内容,咋达到sitemesh上面的效果啊? 别急,再想一个稍微复杂通用的需求:假如原始页面提供了侧栏菜单(side_menu)的html,则在模板指定位置显示出来,否则不显示。 这时,仅能解析出head和body就不够了,sitemesh是怎样做的呢? 原始页面:
模板页:
OK,干净利落,使用了个怪异的content标签,鼓捣几下子sitemesh API/taglib就搞定了。 论到rails layout出手了。 原始页面:
模板页:
嗯嗯,又一个干净利索, 用个接收block的helper方法,加上模板的yield,搞定。 可惜的是它不能自动解析head/body,还好如上炮制也不麻烦。 因此,sitemesh和layout都有能力在原始页里定义若干个片段,供模板引用。


liusong1111:
台后的故事 关键点有两处:content_for这个helper方法,模板中使用的yield。 简要的说,content_for将block执行的结果存在指定的变量里,模板yield时取到替换。 在继续往下看之前,强烈建议先瞅下俺以前的两个贴: http://www.javaeye.com/post/268222 (ERB机理) http://www.javaeye.com/post/268223 (template render机理) content_for/capture http://api.rubyonrails.com/classes/ActionView/Helpers/CaptureHelper.html 130行的源码,去掉一半以上的rdoc、空行,还有多少?(42行,原谅俺这个无聊滴人吧) 直接查看源码吧,魔术尽在其中:( actionpack/lib/action_view/helpers/capture_helper.rb )
 def content_for(name, content = nil, &block)
 eval "@content_for_#{name} = (@content_for_#{name} || '') + capture(&block)"
 end
content_for调用capture方法,对传入的block求值,并把返回的结果存在"@content_for_"开头的成员变量里(对上面例子就是@content_for_side_menu)。 假如没指定content_for的name,就是@content_for_layout。 (它那个参数content=nil没用到啊,为了向下兼容?) 请注意content_for是ActionView::Helpers::CaptureHelper模块中的方法,这个模块被默认引入到ActionView::Base,content_for在template中执行,生成的成员变量自然是template的。 我们深入瞧瞧capture是怎样实现的:
 def capture(*args, &block)
 # execute the block
 begin
 buffer = eval("_erbout", block.binding)
 rescue
 buffer = nil
 end
 
 if buffer.nil?
 capture_block(*args, &block)
 else
 capture_erb_with_buffer(buffer, *args, &block)
 end
 end
假如传入的block上下文里有_erbout这个局部变量,就执行capture_erb_with_buffer,否则执行capture_block。 http://www.javaeye.com/post/268222 (ERB机理) 分析过,打印erb.src得知, erb的执行其实是向局部变量_erbout(String类型)中输出结果。 因此,在erb环境下执行capture会到capture_erb_with_buffer,在独立环境下到capture_block。 先看简单的capture_block:
 def capture_block(*args, &block)
 block.call(*args)
 end
返回block的执行结果,没的说。 再看capture_erb_with_buffer:
 def capture_erb_with_buffer(buffer, *args, &block)
 pos = buffer.length
 block.call(*args)
 
 # extract the block 
 data = buffer[pos..-1]
 
 # replace it in the original with empty string
 buffer[pos..-1] = ''
 
 data
 end
咳,先记下_erbout的当前位置,调用block,把后来生成的那块东东拿出来作为返回值,并把原始位置以后的内容清除,免得影响后面,hack亚~ 至此,capture的手段就一清二楚了,看到还有人这么玩 http://wiki.rubyonrails.org/rails/pages/Nested


liusong1111:
layout的yield 。 先看layout是怎样render的,查看源码:actionpack/lib/action_controller/layout.rb ,瓦瓦,前几行就是传说中的经典代码(DHH在接受《超越Java》作者Bruce采访时谈到"为何AOP在Ruby中没有被采用",摆的就是这段代码。):
module ActionController 
 module Layout 
 def self.included(base)
 base.class_eval do
 alias_method :render_with_no_layout, :render
 alias_method :render, :render_with_a_layout
 end
 end
 end
end
查看action_controller.rb源码得知 ActionController::Base类 include了ActionController::Layout。 结合上面那段代码得知,当ActionController::Layout这个module被include时,会把所在类的原始的render方法改名为render_with_no_layout,并把它自己的render_with_a_layout改名成render,从而达到偷梁换柱不可告人的目的。这一招确实巧妙,对于template的使用者来说,只需知道template有render方法,不必知道layout的存在,layout通过mixin的方式对输出进行拦截-注入的(把template的render偷偷换成了render_with_a_layout,从而先调用原始的render将输出存储在成员变量里,又在同一个template变量下执行layout的render,从而将两者组装起来)。 render_with_a_layout方法的大致实现如下:
def render_with_a_layout(参数)
 if apply_layout?(参数) # 假如需要套用模板页
 layout = pick_layout(参数) # 决定用哪个模板
 content_for_layout = render_with_no_layout(参数) # 调用原始的render方法,并用变量存储输出结果
 add_variables_to_assigns # 复制成员变量
 @template.instance_variable_set("@content_for_layout", content_for_layout) # 将原始输出存在成员变量@content_for_layout里
 render_text(@template.render_file(layout)) # 执行模板页
 else
 render_with_no_layout(参数) 
 end
end
我们看到最后一步是对layout调用render_file,我在 http://www.javaeye.com/post/268223 (template render机理)中描述的第十步是不准确的: 引用 十、 compile_and_render_template中调send(method_name),从而调用到上面生成的方法_run_accounts_index_rhtml,返回ERB执行结果。 实际上它的真实代码是:
 send(method_name, local_assigns) do |*name|
 instance_variable_get "@content_for_#{name.first || 'layout'}"
 end
send方法多了local_assigns(这个我们不管)和block参数,这个block就是layout中yield的接收器,还记得我们在layout中怎样用yield吗?  取到了 @content_for_layout  取到了@content_for_side_menu yeah!~


liusong1111:
它的威力 * nested layouts, 如这个需求 http://www.javaeye.com/topic/83809  * 对页面片段的DSL,如portlet DSL(结合ActiveResource是不是更有前途呢) * 深入挖掘erb,实现专用模板、模板的模板... 约束: http://api.rubyonrails.com/classes/ActionView/Helpers/CaptureHelper.html 说: 引用 NOTE: Beware that content_for is ignored in caches. So you shouldn‘t use it for elements that are going to be fragment cached. 参考资料: Nested Layout: http://wiki.rubyonrails.org/rails/pages/Nested nested-layouts plugin(svn貌似连不上?): http://rubyforge.org/projects/nested-layouts/ “Content for Whom?” http://errtheblog.com/post/28


xvridan:
谢谢liusong1111写了不少关於rails内核的文章,看后大受裨益liusong1111提到的解决layout嵌套的方法是这样吗:2.在相应的视图中加上:ruby 代码 2. # 侧栏菜单的html Capture lets you extract parts of code which can be used in other points of the template or even layout file.Capture只在渲染这个模板页时有效:假如在A.rthml中定义了Capture,中个Capture只能在A.rthml和A的layout渲染A.rthml时使用。假如有B.rthml与A属于同一控制器而且使用同一个layout,这个B是不能使用这个Capture。不能使用Capture嵌套layoutsliusong1111 写道它的威力* nested layouts, 如这个需求 http://www.javaeye.com/topic/83809


liusong1111:
嘿嘿,俺写的东东不多,惭愧。多数情况下capture或您采用的partial template就够了吧. 直接用capture不能达到nested layout效果,但简单封装就可以了,看 nested-layouts 插件: http://mywheel.net/blog/index.php/2006/08/12/nested-layouts-in-ruby-on-rails/ svn://rubyforge.org/var/svn/nested-layouts/trunk (刚才试了下,svn可以用,代码就是blog上那点东东) 它也跟layout机制一样,把_erbout内容再捕获一次,重新写入。


zyyseu:
xvridan 写道谢谢liusong1111写了不少关於rails内核的文章,看后大受裨益 liusong1111提到的解决layout嵌套的方法是这样吗: 2.在相应的视图中加上: ruby 代码    2. # 侧栏菜单的html    Capture lets you extract parts of code which can be used in other points of the template or even layout file. Capture只在渲染这个模板页时有效: 假如在A.rthml中定义了Capture,中个Capture只能在A.rthml和A的layout渲染A.rthml时使用。假如有B.rthml与A属于同一控制器而且使用同一个layout,这个B是不能使用这个Capture。 不能使用Capture嵌套layouts liusong1111 写道它的威力 * nested layouts, 如这个需求 http://www.javaeye.com/topic/83809  请教一下楼主 portlet DSL这个怎样实现呢?感谢


liusong1111:
portlet DSL这个名词是raimundox老大在maillist里说的,俺没细考虑过,把它搬出来只为了说明一种模式的可行性:接收数个html片段,返回对其组装好的形式 -- 不是用ruby method,而是rhtml/erb实现。这样就比在helper里写大段字符串表达的html好看多了。在j2ee里可以类比 freemarker的macro 之于 taglib。 我也等着x老大on beach的时候来了兴致给大家分享portlet DSL的详情。 追加一点思考,用erb实现 "模板的模板" 的思想,其实在rails的无数generator中已广泛使用了。我们调ruby script/generate xx的时候,往往根据它的erb模板生成文件,而这些生成的文件,又不乏rhtml类型的,大家知道rhtml本身就是用erb表达的。 举个例子,restful_authentication这个generator的源码中有个activation.rhtml,内容如下:
使用这个generator给我们工程生成一些有用的文件,要运行ruby script/generate authenticated user sessions,查看生成的其中一个文件,也叫 activation.rhtml:
"处理,这样就实现了模板的模板。
原文出处:http://www.javaeye.com/topic/84116