探索 Ruby on Rails
当我发现我的朋友 Doug Fales 终于抽出时间学习了 Ruby,并使用 Ruby on Rails (RoR) 搭建了一个博客时,我知道我已经离开卫星图像和拍字节的模板引擎世界太久了。似乎我阅读的每个博客都在宣称 Rails 是 Web 框架的新霸主,或者谴责它是无处不在的开发者的祸害。现在,我通常认为任何同时引起如此多 赞美、抗议 和 反思 的东西,一定有其过人之处,而且有传言说 Dave Thomas 正在编写一本关于 RoR 的 书,这更加激励了我尽快了解我能了解的一切。所以我安装了 Rails,快速浏览了一些教程,开始阅读源代码,并给 Doug 打电话,直接从当事人那里了解情况。
Linux Journal: Doug,请简单介绍一下你自己,你从事哪种开发工作?
Doug Fales: 我在 Mercury Interactive 的工作是编写 Java 代码。我接触过许多不同的技术,因为我工作的应用程序是一个基于 Web 的监控工具。我正在编写需要与 SNMP stuff、JMX stuff 或一些自定义供应商库接口的 Java 代码,但同时,由于它是基于 Web 的,我也在为我们提出的新功能编写 UI。
LJ: 工作环境如何?
DF: 我主要开发的笔记本电脑是 Windows XP Pro,然后我的桌子下有几台机器——一台是 SuSE 9.0 Linux,另一台是 Windows 机器。我们使用 IntelliJ 用 Java 编写代码。
LJ: 在家呢,你喜欢用什么工具?
DF: 嗯,我刚收到一台 G5 桌面电脑作为生日礼物,所以我对此感到非常兴奋。我真的很喜欢 Linux 和 BSD,所以这就是我尝试进行开发的系统。我选择的编辑器是 vim。
LJ: 我知道你刚刚开始使用 Ruby on Rails 搭建博客。在那之前你做过很多 Ruby 编码吗?
DF: 没有。我在 2004 年秋季旁听了一门网络安全课程,因为你总是向我宣扬 Ruby,所以我决定使用 Ruby 完成其中一项家庭作业,一个使用了大量 OpenSSL stuff 的电子邮件客户端。
LJ: 控制台还是 GUI?
DF: 它是基于 GUI 的。我使用了 Ruby-Tk。
LJ: 你的第一个 Ruby 程序是 GUI?
DF: 是的。
LJ: 雄心勃勃。你最初选择 Rails 而不是其他框架来搭建博客有什么特别的原因吗?
DF: 在我的职业生涯中,我已经见过足够多的脚本语言,也学习了足够多的语言,以至于我不会在新脚本语言一出现就跑去学习它。它真的必须有特别之处才能让我想要学习它。你是让我接触 Ruby 的催化剂,而且由于 Rails 的炒作在同一时间开始达到临界点,这就是吸引我进入 Rails 的原因。
LJ: 好的,所以你显然必须设置一个 Rails 环境,我相信人们想知道这需要多少工作。请快速介绍一下你为了设置它所做的工作。
DF: 嗯,我开始在我的旧 450MHz SuSE Linux 机器上设置它,我熬夜了一两个晚上才让它运行起来,但是,你知道,在 Linux 上,很多我需要的东西都已经存在了。
LJ: 我想在这里指出,已经有很多非常好的教程可用,包括 Curt Hibbs 的 OnLAMP 文章,包括 第一部分 和 第二部分。Amy Hoy 对这些文章的后续 对于任何想要开始使用 Rails 的人来说也是很好的读物。
LJ: 使用 Rails 的第一步是什么?将一些概念转化为代码的第一步是什么?
DF: 这是一个很好的问题,因为它提出了关于 Rails 的一个重要观点,如果你是从不同的框架或不同的编程环境来到 Rails,你可能会觉得很陌生。有很多代码生成,并且有很多样板代码是生成或已经存在的。因此,使用 Rails 的第一步是使用你的应用程序名称运行 rails 命令,在你想提供应用程序的目录中。例如,输入
~ > cd /var/www/htdocs/www.mysite.com && rails mycoolwebapp
然后,整个层次结构就为你创建好了。
LJ: 接下来呢?
DF: 在这个层次结构中,有几个主要的目录,作为 Rails 应用程序开发者,你将在其中花费大部分时间。主目录称为 app,它是你的 Web 应用程序将要放置的地方。
LJ: 那么,我可以想象你可能有一个表来表示你的博客帖子,类似于
create table blog_posts ( id serial, post text, created_on timestamp, primary key (id) );
你如何在 Rails 中处理这个表?
DF: 在你创建应用程序后,你将生成一个名为 BlogPost 的模型。
LJ: Rails 非常重视 MVC,是吗?
DF: 当然;整个框架(见图 1)都是基于这种范例的。
LJ: 那么你如何生成这个模型?
DF: 首先,这也是 Rails 中唯一的配置步骤,你将编辑 ./config/database.yml 文件以反映你选择的数据库——MySQL、PostgreSQL、SQLite,等等。基本上就是用户名和密码 stuff。接下来,在 script 目录中有一个名为 generate 的脚本。你给这个脚本一个参数,说明你想生成的对象类型,例如 model 或 controller,以及第二个参数,说明你正在生成的对象的名称。在我们的例子中,它将是
~ > ./script/generate model BlogPost
LJ: 嗯。让我理清一下:你运行了 rails 命令,它本身就是一个代码生成器,最初是为了用一堆东西填充目录层次结构。然后,其中一个文件,generate 脚本,用于为这个特定的应用程序生成更多代码?
DF: 正确。
LJ: 你是如何配置表到模型类的映射的?
DF: 你不需要配置。基本上,rails 生成的类是完全空的。它看起来会像这样
class BlogPost < ActiveRecord::Base end
因为这个类继承自 ActiveRecord::Base,所以它知道如何在运行时在数据库中找到它的表。
LJ: 但是我们刚才讨论的是一个名为 "blog_posts" 的表?
DF: 是的,它的命名——这是你提出的另一个重点——它是根据它包含的内容命名的:blog_posts,复数。而你为该表生成的模型被称为 BlogPost,单数。Rails 有一个智能的 Inflector 类来完成所有这些翻译,但如果你愿意,你可以覆盖它。
LJ: 我明白了,Rails 比我更懂英语。你的类是空的,那么你如何配置数据库到实例方法的映射?
DF: 再次强调,你不需要配置;ActiveRecord::Base 为你处理了这个问题。它在运行时动态响应方法,所以如果你在 BlogPost 的实例上调用方法 category,它当然代表 blog_posts 表的单行,它知道如何获取该列。它知道如何从该行查找字段 "category"。
LJ: 如果你更改了你的 blog_posts 表,比如说你添加了一列,比如帖子输入的时间?
DF: 如果你遵循约定并在 blog_posts 表中添加了一个名为 created_on 的字段,你将免费获得很多东西。首先,表中的任何字段都会自动动态确定 getter 和 setter 方法,即使该列是稍后添加的。其次,某些列名对 Rails 具有特殊意义,created_on 就是其中之一。具有该名称的列由 Rails 自动处理,方法是在创建该行的事务时间插入创建时间。
LJ: 你如何知道哪些列名是特殊的?
LJ: 这些访问表列的自动方法,它们返回字符串是因为它们是通用的吗?
DF: 不。Rails 检查数据库模式并将列映射到适当的类型。因此,int 返回为 Ruby Fixnum,timestamp 返回为 Ruby Time 对象,等等。
LJ: 你仍然没有谈到编写任何代码。
DF: 没错。到目前为止,没有任何东西需要编写[代码]。
LJ: 嗯,你的数据库现在非常无聊;你只有一个表。Rails 如何处理表之间的关系?
DF: 我认为,这是 ActiveRecord 的真正魔力开始的地方。反射数据库以给模型类 getter 和 setter 确实很酷,但是智能地处理表间关系则更上一层楼。博客中的评论是这方面的一个很好的例子。通常,一篇博文会有几个与之相关的评论,假设你的读者不太懒于发表评论。同样,评论总是属于一篇博文。那么,在数据库中,实现这一点的一种常见方法是在其中一个表中使用外键。例如,我的 blog_comments 表有一列标记为 blog_post_id,因为 ActiveRecord 理解这种命名约定。我需要做的另一件事是将我的 BlogPost 模型与我的 BlogComment 模型连接起来,是在每个模型中添加两行代码
class BlogComment < ActiveRecord::Base belongs_to :blog_post end
和
class BlogPost < ActiveRecord::Base has_many :blog_comments, :order => "date" end
当然,这假设了如下模式
create table blog_comments ( id serial, comment text, blog_posts_id int, primary key (id), foreign key (blog_posts_id) references blog_posts (id) );
在我这样做并稍微玩了一下代码之后,我意识到它有多酷。BlogPost 的实例自动拥有一个名为 comments 的新方法,该方法将产生一个评论数组,这些评论的外键 blog_post_id 与 BlogPost 实例的 ID 匹配。BlogComment 实例也是如此;一个名为 post 的方法通过外键提供了与该评论关联的 BlogPost 实例。
LJ: 那么,如果 Rails 被告知两个表是相关的,它会动态生成 join selects 吗?
DF: 是的。
LJ: 这几乎就像配置。
DF: 嗯,是的,但是因为它是在像 Ruby 这样成熟的编程语言而不是像 XML 这样的数据语言中完成的,所以它可以做更多的事情。例如,如果我们正在使用一个不支持外键的数据库,例如 SQLite,并且你像这样定义了关系
class BlogPost < ActiveRecord::Base has_many :blog_comments, :order => "date", :dependent => true end
Rails 会为你处理级联删除。也就是说,删除 BlogPost 将导致其所有关联的 BlogComments 也被删除,即使底层数据库可能不强制执行外键或级联删除。
LJ: 但是,设计良好的类肯定可以理解某种程度上指示级联删除的 XML 吗?
DF: 是的,没错,但是程序员能够将所有东西——模型、视图、控制器——都放在一种语言中,这真的很棒。在 Rails 中,你不需要将状态和行为拆分到单独的文件中;你可以使用同一种语言在一个文件中描述所有内容。此外,在实践中,甚至只需要采取几个这样的配置步骤,因为 Rails 模型对象旨在在运行时反射数据库。毫不夸张地说,你的整个控制器类最终可能会比单个 XML 配置文件更短。
LJ: 所以我理解你可以使用这个模型类在 Ruby 中传递数据,并让它神奇地写入数据库。你如何将其呈现给用户并允许他们与之交互?
DF: 从控制器开始可能是最简单的。基本上,Rails 根据 URL 决定将方法发送到哪个对象。因此,当诸如
http://guod.net/blog/postlist
这样的请求进来时,它最终会调用类似这样的东西
BlogController.postlist
LJ: 那么,URL 被解包以决定将哪个方法发送到哪个控制器类。这是否有一些安全问题?如果你最终调用了一些你不想在控制器对象上公开的方法怎么办?这看起来非常不安全。
DF: 这是一个很好的观点。如果你没有意识到这一点,如果你将各种功能作为公共方法添加到你的控制器类中,那么它可能会不安全,你正在向世界公开所有这些功能。通常,你将你不希望公开的代码放在私有方法中。
LJ: 基本上,Rails 利用 Ruby 的访问控制和内省来知道应该向 Web 界面公开什么。它是怎么知道的?你需要配置吗?
DF: 你不能只调用控制器对象上的任何公共方法,从根 Object 类继承的公共方法将被过滤和拒绝。但是添加到控制器类的任何公共方法都会自动作为操作可用。与其说是配置,不如说是约定,而约定是,你添加到控制器类的任何公共方法都是一个操作,可以通过格式正确的 URL 访问。Rails 甚至可以做到这一点,这证明了 Ruby 的动态性。你可以做一些疯狂的事情,比如
~ > cat a.rb class BaseClass def BaseClass::method_added methodname puts "<#{ methodname }> should be accessable" end end class DerivedClass < BaseClass def an_added_method end end ~ > ruby a.rb <an_added_method> should be accessable
而 Rails 从各个角度都利用了这一点。
LJ: 你不是从头开始编写这个控制器的,对吗?
DF: 你猜对了。generate 脚本也用于生成你的控制器类、web_services 等等。通常,你只需生成类,然后从那里开始编辑。
LJ: 我理解 URL 被映射到控制器对象上的方法调用,但是这最终是如何输出 HTML 的呢?
DF: 标准的 Rails HTML 生成机制是 ERb 模板。ERb,正如你所知,代表嵌入式 Ruby,它是一种将少量 Ruby 代码嵌入到 HTML 文档中的简单方法。因此,对于控制器中的每个操作,你通常都会有一个相应的“视图”,它由一个 ERb/HTML 模板文件 (.rhtml) 组成,该文件的名称与操作的名称相同。
LJ: 你是否必须设置某种数据结构供模板访问?
DF: 不用。视图可以访问控制器的实例变量。这很像它们是友元类,因为实例数据只是在视图模板中可用,就像它在同一个作用域中一样。
LJ: 那么,渲染视图实际上就像在控制器对象上调用方法一样——它的所有内部状态都可供视图访问?
DF: 差不多。
LJ: 我现在有了一个概念,即数据库中的每个表都将有一个模型,而你博客中的每个页面都将有一个与之关联的控制器和视图类。这种安排或多或少看起来像图 2。我想知道你最终得到了多少个类,多少行代码?
DF: 嗯,为了让一些东西正常工作,可能介于 400 到 600 行我自己的代码和六个控制器类之间。但是,这包括相当多的东西,例如身份验证和管理页面。显然,这个数字可以改进——我是一个 Ruby 新手——但我对代码库的大小感到满意。
LJ: 人们喜欢你的博客吗?
DF: 我认为人们喜欢它;我的意思是我的程序员朋友会阅读它。它没什么特别的,我不会取代 WordPress 或 blogger.com,但我的伙伴们很喜欢阅读它和发表评论,我的父母也在慢慢接受。我认为人们喜欢它主要是因为上面一直都有我女儿的照片。
LJ: 你的程序员朋友呢?他们对你使用 Ruby on Rails 进行开发有什么看法?
DF: 我的很多朋友都非常感兴趣。我的一些在读研究生的朋友已经开始在他们的一些项目中使用 Ruby。我甚至有一个朋友用 Ruby 编写了一个 iCal 库,现在正计划使用 Rails 重写他自己的网站。
LJ: 但是一些大牌开发者似乎对所有关于 Rails 的炒作做出了相当负面的回应。你认为这是为什么?
DF: 我认为对任何技术的恐惧都不是真正基于现实的,因为技术到底是什么?它只是另一种解决问题的方式。但我听到了你在说什么,我读到了一些我认为完全没有根据事实的评论。我认为其中一些可能是因为有些人真的投入到一个框架或其他框架中,并且可能害怕 Rails 的势头可能会威胁到他们的权威或机会,如果它要接管“web-app-framework-land”的很多市场份额。所以有这个原因,然后还有整个事情,如果这成为“做事的方式”,那么他们将不得不重新开始学习一种全新的语言和框架。
LJ: 你在学习 Rails 的时候,学习 Ruby 是否很困难?
DF: 不,我不觉得困难。
LJ: 我记得前几天你和我一起进行代码走查时,你说你甚至不知道如何分配哈希元素。我认为你编写了一个完整的博客,却甚至不知道像这样的语言基础知识,这真是太神奇了。这说明了 Rails 的设计方式,你不需要成为语言大师就可以用它做一个相当复杂的 Web 应用程序。
DF: 是的,这就是 Rails 可以提供的优势之一:Ruby。这太棒了。人们忽略了这一点——这门语言太棒了。用它编写代码很有趣。Rails 框架本身也很有趣,而且开发速度非常快。