Rails 大放异彩
Ruby on Rails 是一个 Web 应用程序开发框架,它承诺并提供了一个强大、高效且有趣的平台,用于构建动态网站。框架可以被认为是一个库——应用程序使用的函数集合——但它不仅仅是这样,它还是代码的约束系统。为什么约束会是一件好事?因为通过为特定目的接受约束,您实际上可以通过将精力集中在手头的问题上来激发创造力。Rails 框架是一组约束,可以实现有效的 Web 开发。为了感受它的工作原理,让我们看看构成 Rails 的各个部分。
像大多数 Web 应用程序框架一样,Rails 遵循模型-视图-控制器 (MVC) 设计模式,该模式将您的代码划分为三个逻辑层。模型层由域对象组成,由数据库支持,而 Rails 中负责这项工作的组件是 ActiveRecord。请注意 ActiveRecord 的三个主要功能:关联、回调和验证。关联允许您定义 ActiveRecord 类之间的关系,例如一对一、一对多和多对多。下面是它的样子
class User < ActiveRecord::Base has_many :projects has_one :address belongs_to :department end
通常需要配置的详细信息(表名、外键名等等)会自动推断,并且数据库中每列的对象属性都会自动创建。Rails 将此称为约定优于配置。回调为您的对象生命周期提供了一组强大的钩子,您可以在其中添加行为。例如,当用户记录首次保存时,发送欢迎电子邮件
class User < ActiveRecord::Base after_create :send_welcome_email after_update :update_audit_log end
验证是一种特殊的回调,它使标准数据验证例程变得轻而易举
class User < ActiveRecord::Base validates_presence_of :name validates_format_of :phone, :with => /^[0-9]{3}-[0-9]{3}-[0-9]{4}$/i validates_confirmation_of :email validates_acceptance_of :terms_of_service, :message => "must be accepted" validates_inclusion_of :age, :in => 0..99 end
通过将您的关联、回调和验证规则保留在 ActiveRecord 类定义中,您可以更轻松地创建可靠、可维护的代码。
ActionPack 有两个紧密协作的子组件:ActionController 和 ActionView。ActionController 类定义操作——可从 Web 访问的公共方法。操作总是以两种方式之一结束:要么是重定向(发回的 HTTP 响应头,导致客户端被转发到另一个 URL),要么是渲染(发回客户端的一些内容,通常是 HTML 文件)。当操作执行渲染时,将调用 ActionView。看看一个示例控制器,包含三个操作
class MessagesController < ActionController::Base def list @messages = Message.find :all end def show @message = Message.find params[:id] end def create @message = Message.create params[:message] redirect_to :action => :show, :id => @message.id end end
第一个操作使用 ActiveRecord 对象查找数据库中的所有消息,然后渲染模板 messages/list.rhtml。第二个操作按 ID 查找一条特定的消息并显示它。第三个操作也使用 ActiveRecord 对象,这次是为了保存从 HTML 表单传递的参数。然后,它发送一个 HTTP 重定向响应,将用户发送回 show 操作。
控制器和操作使用路由映射到 URL。默认路由是 :controller/:action/:id,因此在没有任何额外配置的情况下,上述操作的 URL 将是 /messages/list、/messages/show/1 和 /messages/create。
除了操作之外,控制器还可以具有过滤器(允许您中断操作)和缓存(允许操作更快地执行)。例如
class MessagesController < ActionController::Base before_filter :authenticate, :except => :public caches_page :public caches_action :show, :feed end
ActionView 是 Rails 用于格式化应用程序输出(通常是 HTML 文件)的系统。主要机制是 ERB,即嵌入式 Ruby,任何使用过类似 PHP 或 JSP 语法的开发人员都会熟悉它。任何带有 .rhtml 扩展名的模板文件都可以嵌入 Ruby 代码片段,位于 <% %> 和 <%= %> 标签内。第一种类型不输出任何内容,第二种类型会输出。例如
<% for message in @messages %> <h2><%= message.title %></h2> <% end %>
您还可以创建模板局部视图来提取常用的标记块,而辅助方法是模板中可用的 Ruby 函数,用于提供方便的功能,例如极其简单的 Ajax。最后,称为布局的特殊模板可以容纳整个项目通用的标记(例如 HTML 标头和页脚)。
Ruby on Rails 的第一个公开发布版本是 0.5 版,发布于 2004 年 7 月。一年多以后(几乎每一行代码都已更改),Ruby on Rails 1.0 于 2005 年 12 月发布。在这个里程碑之前,进行了密集的润色和测试,以确保它是一个可靠的版本——因此您可能会认为 Rails 核心团队自那时以来一直在轻松前行,享受其软件的巨大成功。
您可能会这样认为,但您错了。事实上,他们丝毫没有放慢脚步,Rails 的下一个主要版本刚刚发布。这是迄今为止最大的版本,包含 500 多个增强功能、修复程序和调整。500 个更改中的大多数都巧妙地润色了现有功能,但其中一些是明星功能,有望改变您的应用程序的开发方式。我仔细研究了更改日志,找到了最有趣的部分,它们可以归为三个主要类别:强大的 Ajax、更丰富的域模型和简易 Web 服务。
可以说,Rails 1.1 中最重要的新功能重新定义了 Rails 处理 Ajax 的方式。Rails 已经对创建 Ajax 应用程序提供了一流的支持——它的工作原理是向页面发送少量 HTML 片段以进行插入。现在,它还可以将 JavaScript 返回到浏览器进行评估。这意味着一步更新多个页面元素变得轻而易举。
关键在于,JavaScript 不是手工编写的,而是可以由 Rails 使用 Ruby 语法生成的。这就是 RJS,即 Ruby 生成的 JavaScript,发挥作用的地方。除了 .rhtml(Ruby HTML)模板外,您还可以创建 .rjs(Ruby JavaScript)模板。在其中,您可以编写 Ruby 代码,这些代码将生成 JavaScript 代码,这些代码作为 Ajax 调用的结果发送,并由浏览器评估。
让我们看一个例子,了解如何使用它。在线商店 IconBuffet 将 RJS 用于其购物车(请访问 www.iconbuffet.com/products/amsterdam 试用)。当商品添加到购物车时,需要更新三个单独的页面元素以反映更改。在 RJS 之前,这将需要十几行 JavaScript 代码和多次往返服务器。但现在,只需一次传递即可完成,无需自定义 JavaScript 代码。
添加到购物车按钮使用标准的 Ajax 链接辅助方法,就像以前一样
<%= link_to_remote "Add to Cart", :url => { :action => "add_to_cart", :id => 1 } %>
单击链接会触发 add_to_cart 操作,该操作会更新会话并渲染其模板 add_to_cart.rjs
page[:cartbox].replace_html :partial => 'cart' page[:num_items].replace_html :partial => 'num_items' page["product_#{params[:id]}"].addClassName 'incart'
模板被渲染为 JavaScript,该 JavaScript 被发送回浏览器并进行评估,从而相应地更新三个页面元素。您可能想知道这个 page 对象是从哪里来的——它被传递给 RJS 模板以表示 JavaScriptGenerator,它有很多绝招
1) 弹出一个 JavaScript 对话框
page.alert 'Howdy'
2) 替换元素的 outerHTML
page.replace :element, "value"
3) 替换元素的内容
page.replace_html :element, "value"
4) 插入文本
page.insert_html :bottom, :list, '<li>Last item</li>'
5) 使用以下方式模拟重定向
window.location.href: page.redirect_to url_for(...)
6) 调用 JavaScript 函数
page.call :alert, "Hello"
7) 赋值给 JavaScript 变量
page.assign :variable, "value"
8) 调用效果
page.visual_effect :highlight, 'list' page.visual_effect :toggle, "posts" page.visual_effect :toggle, 'comment', :effect => :blind
9) 显示元素
page.show 'status-indicator'
10) 隐藏元素
page.hide 'status-indicator', 'cancel-link'
11) 通过 ID 引用元素
page['blank_slate'] page['blank_slate'].show
12) 使用 CSS 选择器获取元素
page.select('p') page.select('p.welcome b').first page.select('p.welcome b').first.hide
13) 插入一些 JavaScript 代码
page << "alert('hello')"
14) 使其可拖动
page.draggable 'product-1'
15) 使其可放置
page.drop_receiving 'wastebasket', :url => { :action => 'delete' }
16) 使其可排序
page.sortable 'todolist', :url => { action => 'change_order' }
17) 延迟执行
page.delay(20) { page.visual_effect :fade, 'notice' }
也可以使用 Enumerable 方法,它们将生成等效的 JavaScript 代码
page.select('#items li').collect('items') do |element| element.hide end
这将生成以下 JavaScript 代码
var items = $$('#items li').collect(function(value, index) ↪{ return value.hide(); });
除了在视图目录中拥有 .rjs 文件之外,您还可以编写内联 RJS。例如
def create # (handle action) render :update do |page| page.insert_html :bottom, :list, '<li>Last item</li>' page.visual_effect :highlight, 'list' end end
当然,您不希望使用大量特定于视图的代码污染您的控制器,因此您还可以编写可以从 update 块调用的 RJS 辅助方法。例如
module ApplicationHelper def update_time page.replace_html 'time', Time.now.to_s(:db) page.visual_effect :highlight, 'time' end end class UserController < ApplicationController def poll render :update { |page| page.update_time } end end
调试 RJS 可能很棘手,因为如果发生 Ruby 异常,浏览器中不会显示任何错误。为了解决这个问题,请设置config.action_view.debug_rjs = true,您将通过 alert() 收到 RJS 异常的通知。
您可能已经注意到,RJS 模板的输出利用了 Prototype 的一个很棒的新功能:Element 类的方法被混合到由 $() 和 $$() 引用的所有 HTML 元素中。这意味着您现在可以编写Element.show('foo'),而不是编写$('foo').show()。这是一个小的更改,使编写 JavaScript 代码更自然且更像 Ruby。可用的方法有 visible()、toggle()、hide()、show()、visualEffect()、remove()、update(html)、replace(html)、getHeight()、classNames()、hasClassName(className)、addClassName(className)、removeClassName(className)、cleanWhitespace()、empty()、childOf(ancestor)、scrollTo()、getStyle(style)、setStyle(style)、getDimensions()、makePositioned()、undoPositioned()、makeClipping() 和 undoClipping()。
Ruby 生成的 JavaScript 还使用了 Prototype 的另一个出色的新功能,即 Selector 类及其对应的 $$() 函数。与 $() 函数一样,$$() 用于引用 HTML 元素,但此函数通过 CSS 选择器字符串匹配元素。例如
// Find all <img> elements inside <p> elements with class // "summary", all inside the <div> with id "page". Hide // each matched <img> tag. $$('div#page p.summary img').each(Element.hide) // Attributes can be used in selectors as well: $$('form#foo input[type=text]').each(function(input) { input.setStyle({color: 'red'}); });
如果您现在还不相信,那就听我的,RJS 和 Prototype 的新功能将彻底改变 Rails 中 Ajax 的实现方式。
到目前为止,我们已经了解了 Rails 控制器层和视图层的进步。现在让我们转向 ActiveRecord,它在这个版本中也受到了很多关注。首先是新型关联。
在 1.1 之前,Rails 使用 has_and_belongs_to_many 支持多对多关系。例如
class Author < ActiveRecord::Base has_and_belongs_to_many :books end class Book < ActiveRecord::Base has_and_belongs_to_many :authors end
这在一定程度上工作正常。困难在于当您需要关联本身的数据或行为时。解决方案是为关联创建一个显式的连接模型。看看这个替代方案
class Author < ActiveRecord::Base has_many :authorships has_many :books, :through => :authorships end class Authorship < ActiveRecord::Base belongs_to :author belongs_to :book end class Book < ActiveRecord::Base has_many :authorships has_many :authors, :through => :authorships end Author.find(:first).books.find(:all, :include => :reviews)
has_many 的新 :through 选项允许您指定显式的关联连接模型,因此您可以拥有 has_and_belongs_to_many 的简易性,但可以获得 Authorship 模型的 ActiveRecord 的全部功能。
:through 选项也可以在中间关联是 has_many 的情况下使用。例如
class Firm < ActiveRecord::Base has_many :clients has_many :invoices, :through => :clients end class Client < ActiveRecord::Base belongs_to :firm has_many :invoices end class Invoice < ActiveRecord::Base belongs_to :client end
如果没有 :through 选项,获取公司的所有发票将需要多次 SQL 命中数据库或自定义 SQL 查询。现在,ActiveRecord 会自动处理连接,并留下一个干净的 API 来访问关联。
另一个新的关联选项(进一步丰富您的域模型)是多态关联。这解决了模型可能与多个其他模型共享关系的问题。通过多态关联,模型定义了一个抽象关联,它可以表示任何其他模型,而 ActiveRecord 会跟踪详细信息。看看这个例子
class Address < ActiveRecord::Base belongs_to :addressable, :polymorphic => true end class User < ActiveRecord::Base has_one :address, :as => :addressable end class Company < ActiveRecord::Base has_one :address, :as => :addressable end
任何有 SQL 经验的开发人员都遇到过“n+1 查询”问题,其中查找一组记录,每条记录都有一个相关记录,会导致大量查询数据库。解决方案是 SQL JOIN 语句,但手工编写它们很快就会变得复杂,尤其是在多个连接之后。Rails 1.1 通过级联、无限深度的预先加载显着减少了这种痛苦。现在,像这样的查询Author.find(:all, :include=> { :posts => :comments })将通过单个查询获取所有作者、他们的帖子以及属于这些帖子的评论。例如
Author.find :all, :include => { :posts => :comments } Author.find :all, :include => [ { :posts => :comments }, :categorizations ] Author.find :all, :include => { :posts => [ :comments, :categorizations ] } Company.find :all, :include => { :groups => { :members => :favorites } }
ActiveRecord 的下一个主要新功能是嵌套的 with_scope。此功能使您对 ActiveRecord 对象的操作更加清晰易懂——对于具有安全隐患的代码尤其重要。这是一个例子
Developer.with_scope :find => { :conditions => "salary > 10000", :limit => 10 } do # SELECT * FROM developers WHERE (salary > 10000) LIMIT 10: Developer.find :all # parameters are merged Developer.with_scope :find => { :conditions => "name = 'Jamis'" } do # SELECT * FROM developers WHERE (( salary > 10000 ) AND ( name = 'Jamis' )) LIMIT 10 Developer.find :all end # inner rule is used. (all previous parameters are ignored) Developer.with_exclusive_scope :find => { :conditions => "name = 'Jamis'" } do # SELECT * FROM developers WHERE (name = 'Jamis'): Developer.find :all end end
ActiveRecord 的最后一个主要新增功能为访问计算和统计信息提供了方便的语法,而无需编写自定义 SQL。例如
Person.count Person.count :conditions => "age > 26" Person.count :include => :job, :conditions => "age > 26 AND job.salary > 60000" Person.average :age Person.maximum :age Person.minimum :age, :having => "min(age) > 17", :group => :last_name Person.sum :salary, :group => :last_name
Rails 1.1 中第三大类更改涉及创建 Web 服务——具体来说,是接受 HTTP 协议的某些方面,以便可以非常轻松地实现 REST 风格的 API。
等式的第一个部分是您的操作的新方法 respond_to。此方法解析从客户端发送的 HTTP Accept 标头,以便一个操作可以返回多种响应格式。例如
class MessagesController < ActionController::Base def list @messages = Message.find :all respond_to do |type| type.html # using defaults, which will render messages/list.rhtml type.xml { render :xml => @messages.to_xml } # generates XML and sends it with the right MIME type type.js # renders index.rjs end end end
在此示例中,请求 /messages/list 的典型浏览器将像往常一样获得数据的 HTML 版本。但是对同一 URL 的 Ajax 请求可能会发送 application/javascript 的 Accept 标头——触发使用 RJS 模板。然而,另一个客户端可能希望以 Web 服务 API 的形式与您的应用程序交互,因此它请求 application/xml,并且同一操作也处理该格式。如果您想知道向 Web 应用程序添加 API 有多难,那么现在从未如此简单。
上面的示例包含 render 方法的新选项 :xml。它的工作方式与 render(:text => text) 完全相同,但将 content-type 设置为 application/xml,并将 charset 设置为 UTF-8。您可以使用 :content_type 选项手动指定 content-type 标头。例如
render :action => "atom.rxml", :content_type => "application/atom+xml"
数组、哈希和 ActiveRecord 现在都具有 to_xml 方法,并且每个对象都具有 to_json 方法。这些强大的新增功能意味着只需按几下键即可提供应用程序数据的机器可读版本。例如
message.to_xml message.to_xml(:skip_instruct => true, :skip_attributes => [ :id, bonus_time, :written_on, replies_count ]) firm.to_xml :include => [ :account, :clients ] [1,2,3].to_json => "[1, 2, 3]" "Hello".to_json => "\"Hello\"" Person.find(:first).to_json => "{\"attributes\": {\"id\": \"1\", \"name\": \"Scott Raymond\"}}"
上面的示例演示了您可以多么轻松地启用只读 API,但是如果您也想接受来自 API 的输入呢?嗯,这非常简单
class MessagesController < ActionController::Base def create @message = Message.create params[:message] redirect_to :action => :show, :id => @message.id end end
但是等等——这难道不是与非 API 版本的操作相同的代码吗?确实如此,Rails 现在检查传入 POST 的 HTTP content-type 标头,并相应地将输入解析到 params 对象中——就像数据来自 Web 表单一样。默认情况下,使用 application/xml content type 提交的请求由创建一个 XmlSimple 哈希处理,该哈希的名称与提交的 XML 的根元素相同。XML 数据是自动处理的,但是如果您想处理其他格式呢?输入可插拔参数解析器
ActionController::Base.param_parsers['application/atom+xml'] = Proc.new do |data| node = REXML::Document.new data { node.root.name => node.root } end
显然,我们仅仅触及了 Rails 1.1 新功能的皮毛——更不用说 Rails 作为一个整体了。但您现在应该对 Rails 的最新添加有所了解。有关更深入的信息和社区(包括书籍、截屏视频、文档、教程、演示应用程序、Weblog、邮件列表和 IRC 频道),请访问 rubyonrails.com。
Scott Raymond 是 Rails 项目的贡献者,也是一名专业的 Rails 开发人员和顾问。他从事 Web 应用程序创建已有十年之久——担任过从实习生到 IT 总监的各种角色,客户范围从独立摇滚乐队到财富 100 强跨国公司。他在 scottraymond.net 上写作,将在 2006 年 6 月的 RailsConf 上发表演讲,并将在今年晚些时候与 O'Reilly 出版一本著作。Scott 拥有堪萨斯大学语言学学士学位。