Archive for August, 2008

ActionView 的魔术:ERB & Binding

Rails 作为一个 MVC 框架,其核心包括三个模块:ActiveRecord,ActionController 和 ActionView。今天这篇博文的主角是 ActionView,解开模板系统的魔术。通常情況下,通过 scaffold 已经能够建立简单的、包含CRUD基本功能的页面,完全不需要手动修改 view 的代码。即使不使用 scaffold ,Rails 也提供了众多的辅助方法,创造一个功能丰富的动态页面简直是易如反掌。但是,会用不代表深入理解,最近有朋友问我这些问题:

  1. 为什么编辑一个对象需要在 Controller 创造一个实例变量
  2. View 通过什么方式访问这些实例变量的
  3. 那么多表单辅助方法,都是需要提供 object_name, method 两个参数,怎么就变成实例变量的值了

相信大部分的 Rails 程序员手边的书都是《Agile Web Development with Rails》,书中提到这一点的时候一笔带过,只是说 Rails 在这里用了一个小魔术。这里,我们就来揭开这个魔术吧!

Part 1. Template Files – 模板文件

当一个 action 需要返回一段 html 片段的时候,我们需要建立一个模板文件。根据不同的版本、请求类型,模板文件的文件名也各不相同,从早期版本的 action_name.rhtml ,到现在的 action_name.text.html.erb ,以及扩展的 rjs,在扩展名中都包含了一个关键字:r / erb。它就是 Rails 模板系统的关键:ERb。

Part 2. ERb – Ruby Templating

ERb – 嵌入式 Ruby (http://ruby-doc.org/stdlib/libdoc/erb/rdoc/index.html ),是 Ruby 语言提供的一个基本扩展。它支持在字符串中嵌入 ruby 代码片段。看上去似乎很神秘,其实我们每天都用到,下面这种形式一定不陌生吧?

<h1><%= @user.name %></h1>

没错,正是因为 ERb 的存在,使得模板中可以动态地引用对象的属性。

Part 3. Instance Variables of Ruby – Ruby 的实例变量

让我们回顾一下 Ruby 语言的基本要素之一:实例变量。通常我们通过

@time = Time.now

的形式创造一个实例变量。这里我们不重新解释对于“实例变量”的定义,但是必须牢记一点,正如字面所见,实例变量的作用域是当前实例内,也就是说, 只有在实例的内部,才可以直接对实例变量进行读写操作(扩展的访问子方法等等不在讨论范围之内)。那么,为什么 Rails Controller 里创建的实例变量能够在 View 里面访问呢,不是自相矛盾吗?

Ruby 作为一种动态语言,因为其“开放”的特点,使很多原本不可能的编程模式变为可能。比如,通过 Open Class 的特性,你可以动态的为对象注入新的方法定义,或者改写方法的逻辑,或者,通过不同的方法可以在对象的外部访问对象的内部的实例变量。经常使用 console 的朋友可能会了解其中的一种方式:Object#instance_variable_get / Object#instance_variable_set 方法。举个简单的例子:

class User
  def initialize( name )
    @name = name
  end
end

user = User.new( "Jack" )
user.name #=> raise NoMethodError
user.instance_variable_get('@name') #=> "Jack"
user.instance_variable_set('@name', 'Tom')
user.instance_variable_get('@name') #=> "Tom"

可以看到,在没有任何访问子的情况下,我们用这种方式对一个实例变量进行读写操作。

除了这种简单的方式,还有另外一种进阶的方式,也是 ERb 常用的一种方式:Binding

Part 4. Binding

Binding (http://ruby-doc.org/core/classes/Binding.html ) 是 Ruby 语言的自身的一个特性,在任何对象内,self.binding 方法都会返回一个当前对象关联的 binding 实例。不精确的说,binding 对象可以理解成为当前对象的完整的上下文环境。文档中已经包含了一些示例代码,帮助大家理解 Binding 对象的作用。最重要的一点是:既然是当前对象的完整上下文环境,自然就包括了对象的实例变量。

那么 Binding 在 ERb 中扮演一个什么样的角色?是一个运行环境的提供者。

回到我们最初的问题,Rails 在 ActionView 中使用了什么样的魔法?答案就是 ERb 和 Binding。首先,获得当前实例的 binding,自然,binding 内也包括了实例变量;紧接着,ERb 允许将模板的内容动态地,绑定到另一个环境中运行。我们依然用刚才的 User 的例子来说明这一点:

# A simple template string
template = "Hello, "
@user = User.new( "Jack" )
# Get binding
binder = self.send( :binding ) # calling a private method# Rendering template
puts ERB.new( template ).result( binder )
#=> "Helo, Jack"

虽然例子不是非常恰当,但是足以展示 Binding 和 ERB 的用法。我们可以看到,ERB#result 方法将模板字符串绑定在另外一个环境中运行,而这个环境包含了我们创建的 @user 实例变量,因此,模板中的 @user.name 得到了正确的值。

这就是 ERB 的真面目,也是为什么在 ActionView 中能够访问到 Controller 里实例变量的原因。感叹一下动态语言的强大吧!以上只是非常粗糙的讲述ERB的使用,Rails 所做的魔法远不止如此,如果有兴趣,可以查看 Rails 源代码,对于深入学习 Rails 框架也有很大的好处。

最后,补充一下 ERB 的应用场景。虽然在普通的需求中,ActionView 所做的已经足够,但是某些情况还是需要创造独立的模板系统。比如某个场景,客户要求提供一个完全自定义的模板系统,这个时候 ERB 就大显身手了。只需要将数据源载入实例变量中,并且在使用手册里列出可以访问的方法,即使完全不懂 Ruby 语言也可以写出使用这套简单的模板系统了。

Think & Express / 思考与表达

炎炎夏日,在车站等车实在不是一件美差,百无聊赖,只能一边等车,一边观察身边的人群。

这次观察的对象是一个中年妇女,正在打电话。根据通话的内容来判断,应该是对徐汇区的道路不是很熟悉,打电话给他丈夫问路。尽管听别人打电话不是很文明的行为,但是既然当事人不避讳什么,在车站扯开嗓门,也不设计敏感话题,我也没什么必要觉得自己是个坏叔叔 :)

好了,闲扯这么多,还是来看看她通话的内容吧。(经过整理)

  1. “去xxxx到底怎么走啊?”
  2. “我就在上次我们去的那条路上”
  3. “旁边?车站啊”
  4. “732,931,205……”(在抬头看着站牌)
  5. “哦,对了,旁边还有个工商银行”

这个场景发生的地点是在我家楼下的公共汽车站。很显然这个妇女没有得到她想要的答案,而她丈夫也没有搞清楚她现在到底在哪里。听着这种对话我不仅哑然失笑。为什么她的思考与表达能力如此之差呢?相信全世界没有人能根据她的这些描述知道她到底在哪里。我们来一句一句的分析:

2. 上次去的那条路,抛开人的记忆力不谈,即使是按照同一条路线,如果你是在公共汽车站等车的话,怎么也不可能一条马路笔直走吧?那么上次那条路到底是哪条?要知道,条条大路通罗马 3. 回答你在车站旁边并不能给对方的判断带来任何帮助。不过如果你回答在马路上,可能会有冷笑话的效果 4. 如果你仔细得看一下你报的车,会发现他们在徐家汇这一块几乎每个站都是一起的,谁知道你在哪一站?但是同样的,你仔细看看站牌,应该能看到这一站叫什么名字 5. 上海的工商银行遍地开花,除非你在另一个空间

唠叨了这么多废话,还是没有提供对方一些有用的信息。最简单的,刚才提到的站牌,每个公交站都有名字,路线+站名在全市是唯一的;其次,在刚才提到的工商银 行对面是一所医院,医院的名字自然也是唯一的;再其次,头顶上有一块牌子,清清楚楚的写着这是xx路和xx路交叉口,这也是全市唯一的。

身边有着如此之多可以作为唯一地点标识的信息,却还是无头苍蝇一般逮到啥说啥。唏哩哗啦说了半天,倒不如花一分钟时间仔细观察一下再打电话,相信就能得到一点指点了。

有时候,难的是在遇到问题,还能保持一定清醒的头脑来分析问题,解决问题。人类的思考能力,是建立在理性的基础上的。