文章来源:http://mypages.javaeye.com/blog/291991
应用Rails进行REST 开发
前言
Http协议除了 get 和 post ,还可以做更多的事情,一直以来,很多的开发人员都忘了这一点。
但是,如果你知道浏览器其实只支持 get 和 post, 那么你就不会感到惊奇了。
get 和 post 是http请求从客户端传到服务器端的两个方法。除了这两个,http协议还知道 put和delete 方法,这两个方法告诉服务器创建或者删除一个WEB的资源。
这个教程的目的,就是扩展开发人员的视线,去了解http协议的 put 和delete 方法。我们常说的 REST 这个术语,精华就是 http 协议中 get, post,put, delete 四个方法。Rails 从1.2版本开始,就支持 REST 这个技术了。
这个教程一开始会简短的介绍REST的背景和概念,接着介绍为什么要开发
REST风格的Rails应用。
使用 scaffolding,这是可以帮助我们产生 controller 和 model 细节的工具,对我们的进行REST应用的开发很有帮助。REST的应用中作用非常重大的路由技术,将会在接下来的章节介绍。“嵌套的资源”这个章节,会介绍一下REST的高级应用,告诉大家资源如何以 父 - 子(继承关系)的关系组合在一起,同时不违反的REST 风格的路由设计。教程的后面,还会介绍一些REST的内容,AJAX,REST风格的应用的测试方法,还有“ActiveResource”-- REST的客户端部分。
什么是REST?
REST这个术语,是Roy Fielding 在Ph.D.论文中提出来的,它的全称是“Representational State Transfer.”
REST描述了这么一个架构:利用标准的http 协议中的 get, post, put,delete 来请求和操作网络上的资源。
在REST中,资源的意思就是一个 基于URL实体,客户端可以通过 http协议来和它进行交互。这个资源可以用各种形式来展示给客户端,如 HTML,XML,RSS,主要依赖于客户端的调用方式。并不像以往的Rails开发那样,用REST方式,一个 url 不是指定一个 model 或者 action, 一个 url 仅仅是资源的本身而已。
在图1.1中,三个资源的URL的开头都是相同的,通过后面的不同的数字来保证这三个是不同的资源。
注意:URL并没有表明要对这三个资源进行什么操作。
Rails 应用中,一个资源是由 controller 和 model 组成的。那么从技术的角度来看,图 1.1中的3个资源"project",就是针对3个请求,而表现出来的 Project model(也就是ActiveRecord类了)的3个实例形式。
为什么使用REST
REST所带给我们的,是Rails 的理论上的提升,下面的一些特性,将会使我们清晰地了解这一点。
a) 简明的Url. REST 风格的URL是为了定位一个资源,而并不是调用一个action. 在REST中,URL经常以这种方式出现:先是controller的名称,然后是资源的id。请求的操作隐藏在URL 里,并且通过 http 协议来表示(get, post,put, delete 四个动作)。
b) 传回给客户端不同格式的内容。我们通过一种方式来编写 controller,其中的action应该都可以返回给客户端不同的格式的结果。对于同一个 action,即可以返回给客户端 html, 也可以返回给xml,也可以返回给 RSS,这依赖于客户端想要什么。REST应用就好像变得能处理客户端好多的命令。
c) 更少的代码。因为一个action 可以返回客户端的各种需要格式,这就减少了(DRY don’t repeat yourself),这也就让 controller 里的代码减少了。
d) 面向 CRUD 的controller. 所谓CRUD 就是Create,Retrieve,Update,Delete.controller 和 model 融合到一起,每个controller都会响应某个model的操作。
e) 让系统设计更简单。REST风格的开发方式,会使系统结构更加清晰,而且容易维护。为什么使用REST?
新玩意
如果你觉得 REST 让你之前所有的开发经验变得毫无作用,别担心,那是不可能的~ 因为REST仍然是基于 MVC 风格的。从技术角度来看,REST中的“新玩意”可以归纳为以下几点:
a) 在 controller 中的 respond_to 的用法。
b) 对于 link 和 form 的新的 helper 方法。
c) 在 controller redirect 中的 url 方法。
d) 在 routes.rb 文件中,新的路由定义方法。
实例应用
我们以《RapidWeb Development mit Ruby on Rails》中的一个例子“项目管理应用”来描述Rails的REST方面的特性。
首先创建一个rails的应用:(原书中的rails为1.2)
> rails ontrack
创建 开发 和 测试 的数据库。
> mysql -u rails -p
Enter password: *****
mysql> create database ontrack_development;
mysql> create database ontrack_test;
mysql> quit
基于REST的rails 应用可以使用新的脚手架(scaffold)命令 scaffold_resource 来方便的创建。可以传递给生成器这样一些参数:在本例中,就是资源的名字 “project”, 和 这个model 的字段名字以及类型。字段的类型是必须的,因为我们要使用migration,以及在视图中显示这些字段。
> cd ontrack
> ruby script/generate scaffold_resource project name:string desc:text
生成器不仅生成了 model, controller, view, 还生成了完整的migration脚本,以及在 routes.rb 里生成了一个映射:map.resources :projects ,对于新生成controller来说,这是用于REST方面的最后一个映射项目。
创建表:
> rake db:migrate
如果我们来看看刚才生成的ProjectController,会发现其实并没有什么新鲜的东西,无非也就是这么一些操作:创建(create),读取 (retrieve),更新(update),删除(delete)这些操作。需要强调注意:这些操作都是针对Project这个资源的。 Controller 和 Action 看起来都很普通,但是仔细看一下,每个Action 都会有一些注释,这些注释表明了 url 和 http 所使用的动作。这些注释所体现的,就是 REST风格的 URL。接下来的章节,我们会仔细分析一下这些URL的内容。
REST 风格的 URL
REST风格的URL,并不像以往的Rails 应用一样,是由 controller/action/model id 所组成的,例如 /projects/show/1 。相反,REST风格的URL仅仅由 controller 和资源的id 所组成,例如/projects/1。
注意:我们一直再强调“资源”这个词。URL中没有了 action,我们也就看不到该对资源进行什么操作了。
“/projects/1”这个URL到底应该是显示一个资源,还是应该删除一个资源?答案来自我们之前提到的 http 协议的4个动作。
下面的列表可以展示 http 协议的4个动作是如何和REST风格的URL所关联的,并且什么样的组合,对应什么样的action:
在Action 中使用 respond_to
我们已经知道,我们可以通过一个 指定资源id的URL 和http协议的动作的组合,来调用一个 action。这使得一个URL看起来非常简洁:一个URL就指定了哪个资源要被操作,而不像以往那样去指定一个Action。
那么到底有什么样的需求,会让我们去使用这种风格的URL呢?一个 REST的action可以应付不同的客户端所需要的不同的信息格式。对于一个WEB迎来说,典型的客户端当然就是浏览器了,但是别忘了,对于一个web service 来说,它需要的则是 xml 格式的信息;对于一个RSS阅读器来说,它需要的则是 RSS格式的信息。对于客户端的请求,我们已经使用 scaffold 生成器生成了 CRUD 4个方法来处理。下面的代码片断展示了 “show” 这个 action 中 respond_to 的使用方法:
Listing 1.2: ontrack/app/controllers/projects controller.rb
# GET /projects/1
# GET /projects/1.xml
def show
@project = Project.find(params[:id])
respond_to do |format|
format.html # show.rhtml
format.xml { render :xml => @project.to_xml }
end
end
respond_to 方法是用了代码块(block)技术,在这个例子中,代码块(block)部分处理了2种格式的信息:html 和 xml。针对客户端不同的请求,会执行代码块(block)中不同的部分。例如如果客户端请求的是html 信息,那么会执行“format.html”,如果客户端请求的是xml 信息,那么会执行“format.xml”部分。
如果 format.html 代码块里是空的,那么默认就显示 show.rhtml。
控制 respond_to 可以通过2种方式:一是在 http-header 里面;二是在URL后面追加一些东西,也就是改变URL的样式。
改变URL的样式
第二种控制 action 返回不同格式的信息的方法,就是改变URL的样式。假设我们没有删除 id=1 这个资源project,那么我们通过以下这个方式来在浏览器里显示这个资源:http://localhost:3000/projects/1.xml
这里MAC的用户要注意,这种情况下 firefox 表现的不错,但是Safari就差些,因为Safari 会忽略xml 格式的信息。甚至 firefox 会把xml 显示的非常漂亮!到这里,我们已经知道一个controller 和 URL是如何工作的,在接下来的2个章节里,我们会学习如何在 controller 和 view 里使用和构造这种REST风格的URL。
好了,今天我们就介绍到这里,到现在为止都还是理论上的东西,下节将开始说view、controller中的rest风格。
REST 风格的URL 和View
View是系统界面和用户之间的一个表现,用户通过链接和按钮来和系统进行交互。传统上Rails的开发人员使用 link_to 这个helper 方法来构造一个链接,这个方法需要一个 hashmap, hashmap 由 controller 和 action 组成;此外,还可以传递一些其他的参数。例如:
link_to :controller => "projects", :action => "show", :id => project
=>
<a href="/projects/show/1">Show</a>
我们马上就意思到,这个link_to 方法并不能很好的用于我们的REST思想:REST不会在URL里包含action。
那么重要的就是通过链接和按钮,我们要把 http 协议的4个动作和URL一起传递给服务器。所以,我们会看到Rails的改进之处:我们仍然使用 link_to 去创建链接,但是,我们不会再使用hashmap, 而是使用一个“path”的方法。首先用一个例子,来说明如何创建一个链接去调用 controller 的 show action。请看好,我
们不会再使用 controller, action, 和 id 了:
link_to "Show", project_path(project)
=>
<a href="/projects/1">Show</a>
关于 path 和下面要说的url 这两类helper 方法,请大家千万不要疑惑他们是从哪来的。我们可以这么认为,是Rails 动态地创造了他们。我们只要使用就可以了!
传统的link_to 所生成的链接中包含了controller和action,相对比,使用新的 “project_path” 所创建的链接,只包含了controller 和 资源的id –毫无疑问,这是一个 REST风格的URL。因为链接默认的是一个“Get”请求,Rails 能够知道这一点,所以就会去调用 show action。对于每一个资源,rails 都会有7个标准的 path 方法,这些可以从表1.2中看到。
进一步看看这个表,我们也会发现4个http 动作,并不足以包含全部的CRUD操作。前2个方法使用Get的方式会工作的很好,但是对于new_project_path 和 edit_project_path 就不同了。
New 和 Edit
用户如果点击一个“新建”链接,那么会使用Get动作来对服务器发送一个请求。下面的例子表明,生成的链接是由 controller 和一个“new”action 组成的。
link_to "New", new_project_path
=>
<a href="/projects/new">New</a>
这是对REST思想的一种破坏?或许乍看之下确实如此。但是如果你仔细看,那么一切都会清晰,“new”并不是一个CURD的action,它更像一个建立一个新的资源之前的准备的动作。真正的CRUD 中的create被调用,是在新的form被提交的以后才执行的。这个链接当然也没有资源的id—因为资源还没有被创建。一个链接如果没有资源的id,那么就不应该被称为REST的URL,因为REST的URL总是会指定一个资源的id。所以,这个 “new” action 仅仅因该用来显示一个新的form的页面而已。
对于 edit_project_path ,也是同样的道理。它引用了一个资源,但是仅仅是在调用 update action 之前的准备工作。真正的update action 是在页面被提交以后才执行的。edit_project_path 和 new_project_path 唯一的区别就是前者需要使用一个资源的id。按照REST的规则,资源的id放在controller 的后面:/project/1 。但是如果仅仅使用Get 动作来提交这个URL,那么Rails将会认为你要调用的是show action。为了防止这一点,edit_project_path 方法扩展了一下生成的链接,例如:
link_to "Edit", edit_project_path(project)
=>
<a href="/projects/1;edit">Edit</a>
这样,我们就能理解为什么允许 edit_project_path 和 new_project_path生成的链接里带有 action 了,因为他们两个都不是REST的 CRUD URL,他们仅仅是准备工作。还有其它的一些URL和这两个很相似,我们会在后面的章节介绍。
在 form 中使用 path 方法:Create 和 Update
传统的方式上,我们使用 form_tag 或 form_for 来创建一个form:
<% form_for :project, @project, :url => { :action => "create" } do |f|
%>
...
<% end %>
在REST应用中,这个 :url hashmap 会被 path 方法给取代:
“project_path” 创建新的资源所使用的form
“project_path(:id)”编辑一个资源所使用的form
a) 创建资源所使用的form
form 使用 post 动作向服务器提交信息,“project_path”方法并不会有资源id作为参数,这样,生成的URL就应该是“/projects”这个样子。当提交到服务器以后,就会调用 create action。
form_for(:project, :url => projects_path) do |f| ...
=>
<form action="/projects" method="post">
b) 编辑一个资源所使用的form
按照REST的思想,一个更新的操作是使用http协议的PUT动作来发送的。但是,正如我们所知道的,浏览器只明白 Post和Get动作。解决的办法就是使用
form_for 方法里的 :html 参数。
form_for(:project, :url => project_path(@project),
:html => { :method => :put }) do |f| ...
=>
<form action="/projects/1" method="post">
<div style="margin:0;padding:0">
<input name="_method" type="hidden" value="put" />
</div>
Rails 生成了一个隐藏的字段来代替http的put 动作。提交以后,Rails 会检查这个变量,然后判断是否去调用update方法。
删除
恐怕我们已经发觉了,用于显示和删除一个资源,所使用的path方法都一样:
link_to "Show", project_path(project)
link_to "Destroy", project_path(project), :method => :delete
唯一的不同就是 删除的时候,使用了一个变量 :method,用它来表示http的DELETE动作。因为浏览器不支持DELETE动作,所以,Rails 会生成一些javascript来解决这个问题:
link_to "Destroy", project_path(project), :method => :delete
=>
<a href="/projects/1"
οnclick="var f = document.createElement(’form’);
f.style.display = ’none’; this.parentNode.appendChild(f);
f.method = ’POST’; f.action = this.href;
var m = document.createElement(’input’);
m.setAttribute(’type’, ’hidden’);
m.setAttribute(’name’, ’_method’);
m.setAttribute(’value’, ’delete’); f.appendChild(m);f.submit();
return false;">Destroy</a>
这段javascript 会生成一个form,把 http 的DELETE动作放在隐藏变量里传递给服务器,然后,Rails 会判断这个变量,决定是否去调用destroy 方法。
好了,今天就介绍到这,明天我们来说说Controller中的URL方法。
Controller 里的URL 方法
在View中,我们已经使用了一些新的helper 方法(也就是path方法)来生成了REST风格的URL,那么controller 自然也需要一些新的东西来处理redirect 等请求。在controller中,我们使用“url”helper 方法,来生成正确的REST风格的URL。
project_url 对应 project_path
projects_url 对应 projects_path
和 “path” 方法向对比,“url” 方法生成了一个完整的URL地址,包括协议,主机,端口,以及路径。
project_url(1)
=>
"http://localhost:3000/projects/1 "
projects_url
=>
"http://localhost:3000/projects "
在Rails 应用的controller里,”url” 方法用在redirect_to 方法里,取代传统的 controller/action 的方式。
redirect_to :controller => "projects", :action => "show",:id => @project.id
在REST应用中应该这么写:
redirect_to project_url(@project)
对于这一点,你可以把 destroy action 作为一个例子去看看:在一个资源被删除以后,使用 project_url 去显示全部的资源,而不是像以往一样使用controller,action 作为参数。
Listing 1.3: ontrack/app/controllers/projects controller.rb
def destroy
@project = Project.find(params[:id])
@project.destroy
respond_to do |format|
format.html { redirect_to projects_url }
format.xml { head :ok }
end
end
REST 风格的路由
到目前为止,我们介绍了REST的内容,以及在链接,form,controller中所适用的一些新的helper方法。但是我们没有解释那些helper方法是从哪来的?决定那些方法存在的,以及指定那些方法会调用哪个controller的哪个action,就是一个文件,那就是 /config/routes.rb。
map.resources :projects
这个配置是由我们上面适用 scaffold 生成器生成的。生成器生成了一个路由,当处理请求时,controller 需要这个路由才能知道调用哪个action。此外,resources 生成了path 和 url 的helper方法去操作“project”这个资源。
projects projects_url, projects_path
project project_url(id), project_path(id)
new_project new_project_url, new_project_path
edit_project edit_project_url(id), edit_project_path(id)
习惯
要进行REST风格的开发,就必须遵循REST方式的命名习惯,多针对 CRUD四个操作而言。下面的link_to 将会产生如下的html:
link_to "Show", project_path(project)
=>
<a href="/projects/1">Show</a>
不管是link_to 方法中,还是生成的html中,都没有去指定要调用的action,Rails 会知道,如果使用Get方式来调用这个URL,那么就是去调用show这个action。因此,controller里就必须有一个名字为“show”的 action。对于index, update, delete,create, destroy,new,edit,也都是相同的习惯,所以,每一个REST的controller 都必须实现这几个方法。
定制路由
定制路由
通过以下一些选项,REST的路由可以去适应应用的一些特殊需求:
:controller. 指定使用哪一个controller
:path prefix. 生成的URL的前缀。
:name prefix. Helper方法的前缀。包括 url方法和path方法。
:singular. 对于一个路由,命名一个唯一的名字。
下面的例子创建了一个路由,用于新建一个Sprint资源,sprint的信息我们会在下面的章节中介绍。
map.resources :sprints,
:controller => "ontrack",
:path_prefix => "/ontrack/:project_id",
:name_prefix => "ontrack_"
在这个URL中,我们适用了 :path_prefix,意味着每一个URL都必须以/ontrack/+project id 开始,对应的controller 应该是 OntrackController。因此这个URL http://localhost:3000/ontrack/1/sprints 根据路由的规
则,会调用 OntrackController的index 方法,而这个URL“http://localhost:3000/ontrack/1/sprints/1
”则会调用 show 方法。:path_prefix 定制了URL的格式,那么 :name_prefix 则会修改helper的方法的名字:
ontrack_sprints_path(1)
=>
/ontrack/1/sprints
or
ontrack_edit_sprint_path(1, 1)
=>
/ontrack/1/sprints/1/edit
好了,今天就说到这,下章我们来说说嵌套资源的REST风格。
嵌套的资源
当适用嵌套的资源的时候,REST的开发会变得更加有趣。在这个章节,你会更加明白简洁的URL的URL的重要性,也会对REST的理念有更清晰的理解。
嵌套的资源,也就是所说的父—子关系的资源。在Rails中,也就是一种model的关系:1对多关系。在我们这个 ontrack 的例子项目中,就好像projects 和 iterations 的关系一样。嵌套的REST controller 仍然负责处理某一个资源的操作,但是对于一个“子”controller来说,它还必须获得“父”资源的信息。
听起来很复杂,不过阅读完这个章节,你很快就会完全明白的。
根据Rails 的REST方式,Rails 将资源的这种主—从关系反映到URL里,并且保持了URL简洁这一重要的特性。在这个ontrack例子里,我们会通过两个资源 project 和 iteration来描述这一点。
首先,我们创建 iteration 这个资源,并且创建 iterations 这个表。
> ruby script/generate scaffold_resource iteration name:string /
start:date end:date project_id:integer
> rake db:migrate
Projects 和 Iterations 是“1 对多”的关系,所以我们要修改一下model:
Listing 1.4: ontrack/app/models/project.rb
class Project < ActiveRecord::Base
has_many :iterations
end
Listing 1.5: ontrack/app/models/iteration.rb
class Iteration < ActiveRecord::Base
belongs_to :project
end
除了创建了 model, controller 和 view, 生成器同时在 config/routes.rb 里,创建了一个路由的定义项:
map.resources :iterations
这个路由项和资源project 的非常类似,不过我们别忘了 iteration 和 project的关系。但是很明显,这个路由项并没有考虑这一点。例如,new_iteration_path方法生成了一个URL “/iterations/new”,并没有包含这样一个重要的信息:这个iteration 应该属于哪个 project?所以,我们应该意识到,如果没有一个“父”资源,那么一个“子”资源是没有任何意义的!
Rails 会把这种主—从的关系反映到URL里,所以,我们需要修改一下默认生成的路由项:
map.resources :projects do |projects|
projects.resources :iterations
end
现在这个路由项成为了一个嵌套的资源了,而且你要操作iteration这个资源,就必须基于project 这个资源之上。与之相对应的URL应该是下面这个样子:
/project/:project_id/iterations
/project/:project_id/iterations/:id
例如,如果我输入了这个URL
http://localhost:3000/projects/1/iterations
将会调用 IterationController 的 index 方法,在这个方法里,也可以通过所提交的参数 :project_id 来得到资源project。值得注意的是,URL关联一个资源的这个特性,其实就是等同于下面的关系:
/projects/1/iterations <=> Project.find(1).iterations
嵌套的URL仍然是简洁的URL――URL中仍然只表明的资源,而没有action。简单的说,如果一个资源使用2个REST风格的URL构成,那么它就是一个嵌套的资源。下面这个调用 show action 的URL能让我们清晰的了解这一点:
http://localhost:3000/projects/1/iterations/1
嵌套资源在controller 的代码
新生成的IterationController 并不知道它现在已经要处理嵌套的资源了――这意味着每个方法,都至少应该得到“父”资源project。所以,现在的index方法,仍然是显示全部的 iterations,尽管URL已经表明应该显示的是某一个project下的全部iterations:
Listing 1.6: ontrack/app/controllers/iterations controller.rb
def index
@iterations = Iteration.find(:all)
respond_to do |format|
format.html # index.rhtml
format.xml { render :xml => @iterations.to_xml }
end
end
我们必须重新写index方法,以保证我们只拿某一个project下的iterations。
Listing 1.7: ontrack/app/controllers/iterations controller.rb
def index
project = Project.find(params[:project_id])
@iterations = project.iterations.find(:all)
...
end
我们必须要让controller里全部的方法都能工作在 以/projects/:project_id 为前缀的URL上。这就意味着,我们不仅要修改 index方法,create, update 等等方法也必须进行修改。下面的章节我们会逐步介绍。
在 “path” 和 “url” helper 方法里使用参数
在 config/routes.rb 里新增加的资源,不仅仅只是增加了一个新的路由的定义,同时也自动地增加了新的helper 方法。正如定义的路由那样,新的helper方法需要一个 project-id作为参数。例如 通过 “iterations_path”这个helper 方法,来得到某一个project 下的全部的iterations。Helper 方法的名字并不是以嵌套的方式命名的,所不同的只是传递的参数不一样。对于嵌套式的资源来说,“子”资源的helper 方法的参数,通常都是“父”资源的资源id,在这个例子里就是 project的id。
下面作为例子,我们就来创建一个链接,这个链接可以显示一个project下的全部 iterations。
link_to "Iterations", iterations_path(project)
=>
<a href="/projects/1/iterations">Iterations</a>
其中 iterations_path 的参数“project”就是一个资源的对象。为了更好的理解这个方法的作用,我们把它放到一个页面里来看看:Listing 1.8: ontrack/app/views/projects/index.rhtml
...
<% for project in @projects %>
<tr>
<td><%=h project.name %></td>
<td><%=h project.desc %></td>
<td><%= link_to "Iterations", iterations_path(project) %></td>
<td><%= link_to "Show", project_path(project) %></td>
<td><%= link_to "Edit", edit_project_path(project) %></td>
<td><%= link_to "Destroy", project_path(project),
:confirm => "Are you sure?", :method => :delete %></td>
</tr>
<% end %>
...
那么如果我们传递给 iterations_path 错误的参数会怎么样呢?那将会导致所有的功能都实效,而且页面的显示也会不正常。例如下面这个现实全部iterations 的页面:
Listing 1.9: ontrack/app/views/iterations/index.rhtml
...
<% for iteration in @iterations %>
<tr>
<td><%=h iteration.name %></td>
<td><%=h iteration.start %></td>
<td><%=h iteration.end %></td>
<td><%= link_to "Show", iteration_path(iteration) %></td>
<td><%= link_to "Edit", edit_iteration_path(iteration) %></td>
<td><%= link_to "Destroy", iteration_path(iteration),
:confirm => "Are you sure?", :method => :delete %></td>
</tr>
<% end %>
...
我们看到,第一个参数现在都是 iteration对象。这就导致所有的方法都失效了---原因很明显,因为在 /config/routes.rb 里,我们定义的是第一个参数应该是 project id, 而不是 iteration id。如果想要这个页面显示正常,需要作如下修改:
Listing 1.10: ontrack/app/views/projects/index.rhtml
...
<% for iteration in @iterations %>
<tr>
<td><%=h iteration.name %></td>
<td><%=h iteration.start %></td>
<td><%=h iteration.end %></td>
<td><%= link_to "Show", iteration_path(iteration.project,
iteration) %></td>
<td><%= link_to "Edit", edit_iteration_path(iteration.project,
iteration) %></td>
20 1 RESTful Rails
<td><%= link_to "Destroy", iteration_path(iteration.project,
iteration), :confirm => "Are you sure?",
:method => :delete %></td>
</tr>
<% end %>
...
为了让参数的顺序正确,我们还可以用另一种显示指定参数的方式:
iteration_path(:project_id => iteration.project, :id => iteration)
如果您觉得用对象作为参数不够清晰,那么就可以考虑一下这个方式。
增加新的Iteration
我们仍然是在当前的例子中增加这个功能。为了实现这个功能,我们只需要简单的修改一下 ProjectController 的 index.rhtml :
Listing 1.11: ontrack/app/views/projects/index.rhtml
...
<% for project in @projects %>
<tr>
<td><%=h project.name %></td>
<td><%=h project.desc %></td>
<td><%= link_to "Iterations", iterations_path(project) %></td>
<td><%= link_to "Show", project_path(project) %></td>
<td><%= link_to "Edit", edit_project_path(project) %></td>
<td><%= link_to "Destroy", project_path(project),
:confirm => "Are you sure?", :method => :delete %></td>
<td><%= link_to "New Iteration", new_iteration_path(project)
%></td>
</tr>
<% end %>
...
这里我们使用了 “new_iteration_path”这个helper 方法,并且把project 这个对象作为参数传了进去。这个 helper 方法会生成如下的html语句:
link_to "New Iteration", new_iteration_path(project)
=>
<a href="/projects/1/iterations/new">New iteration</a>
如果您点击这个链接,那么就会调用 IterationController 的 new 方法,在这个方法里,你就可以得到 project id ( 在这个例子里就是“1”)。这样,用于创建新的iteration的form 就可以使用这个project id 了:
Listing 1.12: ontrack/app/views/iterations/new.rhtml
<% form_for(:iteration,
:url => iterations_path(params[:project_id])) do |f| %>
...
<% end %>
=>
<form action="/projects/1/iterations" method="post">
这个“params[:project_id]”其实也可以省略,Rails 会自动处理这个变量,也就是说,上面的代码,和下面的是等效的:form_for(:iteration, :url => iterations_path)因为我们之前在 /config/routes.rb 里定义了路由,这样就确保 使用post方式提交 “/projects/1/iterations”链接时,一定会调用IterationController 的 create 方法。
下面,我们要修改一下 IterationController里的create 方法,以保证我们创建的iteration 是基于某一个project 之上的:
Listing 1.13: ontrack/app/controllers/iterations controller.rb
1 def create
2 @iteration = Iteration.new(params[:iteration])
3 @iteration.project = Project.find(params[:project_id])
4
5 respond_to do |format|
6 if @iteration.save
7 flash[:notice] = "Iteration was successfully created."
8 format.html { redirect_to iteration_url(@iteration.project,
9 @iteration) }
10 format.xml { head :created, :location =>
11 iteration_url(@iteration.project, @iteration) }
12 else
13 format.html { render :action => "new" }
14 format.xml { render :xml => @iteration.errors.to_xml }
15 end
16 end
17 end
在第“3”行,我们使用了“project_id”这个参数,在第“8”行和第“11”行,我们使用了 “url”helper 方法。
下面我们还需要修改一些显示,编辑iteration的链接—因为我们必须把iteration和project 关联在一起。
Listing 1.14: ontrack/app/views/iterations/show.rhtml
...
<%= link_to "Edit", edit_iteration_path(@iteration.project, @iteration)
%>
<%= link_to "Back", iterations_path(@iteration.project) %>
编辑 Iteration
为了能够编辑 iteration,至少需要修改2个地方。1〉在视图中的form_for 方法的参数,目前的参数只有 iteration 一个,还需要增加 projectid。
form_for(:iteration,
:url => iteration_path(@iteration),
:html => { :method => :put }) do |f|
需要修改成:
form_for(:iteration,
:url => iteration_path(params[:project_id], @iteration),
:html => { :method => :put }) do |f|
我们还需要修改 update 方法,修改的目的是一样的。
Listing 1.15: ontrack/app/controllers/iterations controller.rb
1 def update
2 @iteration = Iteration.find(params[:id])
3
4 respond_to do |format|
5 if @iteration.update_attributes(params[:iteration])
6 flash[:notice] = "Iteration was successfully updated."
7 format.html { redirect_to iteration_url(@iteration) }
8 format.xml { head :ok }
9 else
10 format.html { render :action => "edit" }
11 format.xml { render :xml => @iteration.errors.to_xml }
12 end
13 end
14 end
第“7”行需要修改成:
format.html { redirect_to iteration_url(@iteration.project,@iteration) }
到目前为止,新增加的资源 Iteration的操作大部分都可以正常工作了,但是还有一些细节的地方我们没有处理。下一回将讲一下如何自定义Action。
自定义 Action
我们已经知道,在 /config/routes.rb 里定义的路由,会自动生成对资源的CRUD的操作。但是我们如何处理那些并不是CRUD的操作?下面我们就用一个例子来说明这一点。例如我们在 ProjectController里有一个close的方法。这个close并不是真正的删除一个资源,而只是把给这个资源设置一个标志:表示这个
资源被关闭了。
首先修改一下数据库:
> ruby script/generate migration add_closed_to_projects
exists db/migrate
create db/migrate/003_add_closed_to_projects.rb
Listing 1.16: ontrack/db/migrate/003 add closed to projects.rb
class AddClosedToProjects < ActiveRecord::Migration
def self.up
add_column :projects, :closed, :boolean, :default => false
end
def self.down
remove_column :projects, :closed
end
end
rake db:migrate
现在,我们在IteratinController的index.rhtml上创建一个 close 的链接。
Listing 1.17: ontrack/app/views/projects/index.rhtml
<% for project in @projects %>
<tr id="project_<%= project.id %>">
<td><%=h project.name %></td>
<td><%= link_to "Show", project_path(project) %></td>
...
<td><%= link_to "Close", <WHICH_HELPER?> %></td>
</tr>
<% end %>
现在有2个问题摆在我们面前:
1.使用 http 协议的哪个动作来发送这个请求呢?
2.对于这个链接,该如何生成那些 helper方法呢?
因为这个 close 动作并不是CRUD中的任何一个,所以Rails 也不知道该用http的哪个来做这个事情。不过既然 close 也是 update 中的一种,所以应该使用post来发送这个请求。我们还是得在 /config/routes.rb 里定义这个路由,当然定义完路由之后,就会有相应的path和url的helper方法了。因为这个close的操作,仍然是针对projects 这个资源的,所以,我们可以在定义路由的时候,使用一个名字叫“member”的hashmap,这个hashmap 的key,就是自定义action的名字,hashmap的value,就是所使用的http的动作。
map.resources :projects, :member => { :close => :post }
hashmap 的value可以使用 :get, :put, :post, :delete, :any。如果使用了:any,那么可以用http的任何动作来发送这个请求。
定义完这个路由后,我们就可以使用helper方法了:
<td><%= link_to "Close", close_project_path(project) %></td>
因为我们定义的是“:member => { :close => :post }”,所以,这个请求只能以post的方式来发送,如果使用其它方式如“get”,那么请求就是无效的。为了安全起见,我们还是把它改成用按钮的方式来发送,幸运的是我们可以使用Rails 提供的button_to 来做这件事情:
<td><%= button_to "Close", close_project_path(project) %></td>
=>
<td>
<form method="post" action="/projects/1;close" class="button-to">
<div><input type="submit" value="Close" /></div>
</form>
</td>
现在我们要做的就是写完 ProjectController中的 close 方法:
Listing 1.18: ontrack/app/controllers/projects controller.rb
def close
respond_to do |format|
if Project.find(params[:id]).update_attribute(:closed, true)
flash[:notice] = "Project was successfully closed."
format.html { redirect_to projects_path }
format.xml { head :ok }
else
flash[:notice] = "Error while closing project."
format.html { redirect_to projects_path }
format.xml { head 500 }
end
end
end
除了“:member”,我们还可以使用“:collection”,“:new”。
“:collection”的用途是:所操作的资源不是一个,而是很多个。下面是一个用“:collection”方式得到一个资源的列表的例子:
map.resources :projects, :collection => { :rss => :get }
--> GET /projects;rss (maps onto the #rss action)
所以,有的时候,“:member”更多的是更新一个资源,而“:collection”是得到一堆资源。
对于“:new”,一般用于那些还没有被保存的资源:
map.resources :projects, :new => { :validate => :post }
--> POST /projects/new;validate (maps onto the #validate action)
我们是否仍然“DRY”(Don’t Repeat Yourself)?
我们是否为了“DRY”原则?似乎是这样的:我们不仅在controller里定义了action,同时在 /config/routes.rb 里也定义了一遍。
作为替换REST风格的调用的方式,您可以用传统的方式来调用一个方法:
<%= link_to "Close", :action => "close", :id => project %>
但是别忘了,即使用传统的方式,你也得在/config/routes.rb里定义一个路由:“map.connect ’:controller/:action/:id’”。
自定义信息格式
目前 respond_to 可以返回如下的信息格式:
respond_to do |wants|
wants.text
wants.html
wants.js
wants.ics
wants.xml
wants.rss
wants.atom
wants.yaml
end
你可以通过增加新的MIME类型的信息来扩展这个功能。假设您已经开发了一个“PIM”应用系统,现在你希望把地址信息用”vcard” 格式来传送。要实现这一共能,首先你需要注册新的信息格式在
/config/environment.rb。
Mime::Type.register "application/vcard", :vcard然后,我们来修改一下show action,使得返回的信息以vcard 的格式来传送。
def show
@address = Address.find(params[:id])
respond_to do |format|
format.vcard { render :xml => @address.to_vcard }
...
end
end
这个 to_vcard 方法不是 ActiveRecord 的标准方法,所以必须按照 vcard的标准来实现(RFC2426)。如果实现正确的话,那么通过下面的URL,就可以得到正确的信息:“http://localhost:3000/addresses/1.vcard”。
在REST 中使用AJAX
在REST风格的系统中使用AJAX?非常简单,可以说这一小节没什么新鲜的玩意要学习。您还是使用以前所使用的 remote 系列的helper 方法,只不过传递的参数需要改变,现在使用 path helper 方法,而不是以前所使用的 contoller,action 。下面的例子会让您更清晰的明白这一点:
link_to_remote "Destroy", :url => project_path(project),
:method => :delete
=>
<a href="#" οnclick="new Ajax.Request("/projects/1",
{asynchronous:true, evalScripts:true, method:"delete"});
return false;">Async Destroy</a>
给您提醒一下:千万千万记得导入相应的ajax javascript 文件,不然当您的ajax无效,而气得把键盘砸坏的时候,我们就无能为力了。导入相应的javascript 文件相当的简单:
Listing 1.19: ontrack/app/views/layouts/projects.rhtml
<head>
<%= javascript_include_tag :defaults %>
...
</head>
这个“Destroy”链接将会调用ProjectsController的destroy方法。从逻辑上来说现在一切正常:用户点这个链接,系统删除相应的资源。不过我们还是漏了一点:在 respond_to 中,我们应该增加新的返回类新,也就是javascript类型。
Listing 1.20: ontrack/app/controllers/projects controller.rb
def destroy
@project = Project.find(params[:id])
@project.destroy
respond_to do |format|
format.html { redirect_to projects_url }
format.js # default template destroy.rjs
format.xml { head :ok }
end
end
可以看出来,唯一的改变就是增加了“format.js”。因为这个“format.js”并不是一个要被执行的代码块,所以,Rails 会按照标准显示destroy.rjs。
Listing 1.21: ontrack/app/views/projects/destroy.rjs
page.remove "project_#{@project.id }"
这个 rjs 文件从当前的浏览页面中删除了 “project_ID”这个DOM元素,为了让这个删除起到效果,我们就需要在显示 project 上进行修改:
Listing 1.22: ontrack/app/views/projects/index.rhtml
...
<% for project in @projects %>
<tr id="project_<%= project.id %>">
这是一个遵循DRY原则和减少对当前系统的修改的一个好例子!也体现了REST的优势,只需要在controller里增加一行,就可以处理javascript请求了。
同时也告诉了我们一个REST编程的原则:在 respond_to 外实现逻辑处理,能够极大地降低重复的代码。
测试
不管开发REST风格的应用是多么的让我们激动,我们也不能忘记最重要的一个朋友:测试!
之前我们写了那么多代码,但是一次单元测试测试都没运行过!下面我们来运行一下吧!
> rake
...
Started
EEEEEEE.......
好消息是,所有的单元测试和功能测试都可以运行。坏消息是,关于IterationsController的7个功能测试,全部失败!
如果测试用例抛出异常,那么很明显的—这里存在一些错误。我们遇到的错误也很明显:所有的IterationsController的测试用例都是 scaffold 来生成的,并没有一个“父”资源的关联—还记得吗,我们已经让iterations 资源成为了projects 资源的“子”资源。
为了让我们的测试用例通过,我们必须给每个测试方法都增加project_id。例如:
Listing 1.23: ontrack/test/functional/iterations controller test.rb
def test_should_get_edit
get :edit, :id => 1, :project_id => projects(:one)
assert_response :success
end
当然了,你需要加载必要的fixtures:
fixtures :iterations, :projects
改完全部的测试用例以后,我们发现还是有2个测试用例无法通过:
test_should_create_iteration
test_should_update_iteration
失败的代码来自这行“assert_redirected_to iteration_path(assigns(:iteration))”。
错误是非常显然的:我们已经知道iteration_path的第一个参数应该是project id。我们同样需要修改一下:
assert_redirected_to iteration_path(projects(:one), assigns(:iteration))
另外,在使用 redirect 断言的时候,path helper 方法的使用,是REST和非REST风格应用的唯一区别。
REST 风格的客户端:ActiveResource
我们总是把 ActiveResource 和 REST一起提及。ActiveResource 是一个Rails 的库,用来开发基于REST的WEB服务客户端。这种基于REST的客户端,也是适用 http 的4个标准的动作来和服务器通信。
ActiveResource 并不是Rails 1.2 的一部分,但是您可以使用svn 从网站下载它的代码:
> cd ontrack/vendor
> mv rails rails-1.2
> svn co http://dev.rubyonrails.org/svn/rails/trunk rails
ActiveResource把客户端资源抽象成一个类,这个类继承自ActiveResource::Base。例如通过下面的例子,我们来调用服务器上的project 资源:
require "activeresource/lib/active_resource"
class Project < ActiveResource::Base
self.site = "http://localhost:3000 "
end
可以看到,我们导入了ActiveResource 的库,然后,服务器的地址赋给了类的变量 site。这个 Project 类,把服务器上的资源抽象成了一个客户端的类,这就让开发人员觉得他们就好像操作一个ActiveRecord 一样。
例如,我们用一个project id 和 find 方法去请求服务器上的一个资源:
wunderloop = Project.find 1
puts wunderloop.name
这个 find 方法会执行标准的GET动作:GET /projects/1.xml
然后服务器返回xml格式的信息。客户端把xml信息转化成一个ActiveResource对象 wunderloop,就好像一个ActiveRecord对象,可以得到和改变它的任何属性。那么我们如何去更新一个资源呢?
wunderloop.name = "Wunderloop Connect"
wunderloop.save
save 方法会是用put 动作向服务器传递信息。
PUT /projects/1.xml
刷新一下你的浏览器看看,那条记录肯定被改变了。和 find,save一样简单,创建一个新的资源也是非常方便:
bellybutton = Project.new(:name => "Bellybutton")
bellybutton.save
新的资源将会以post方式传递给服务器,并且保存到数据库里。
POST /projects.xml
刷新浏览器,会看到新建立的资源。最后,我们来看一下删除一个资源。
bellybutton.destroy
destroy方法将会以DELETE方式发送给服务器,并且删除这个资源。
DELETE /projects/2.xml
ActiveResource 使用http的4个动作来和服务器交互。对于REST的资源,它提供了非常好的抽象,此外,在ActiveRecord中的许多方法,在ActiveResource中仍然找得到。例如查找一个资源的全部记录:
Project.find(:all).each do |p|
puts p.name
end
相信使用ActiveResource可以开发出很好的松耦合的系统,我们不如马上去下载ActiveResource的代码,亲自体验一下吧!
大结局
这个世界并不是非得需要REST。有很多的解决方案可以考虑,并且可以很容易实现。大概更多的时候,是您可能现在正处于某个项目的中期,这时,您发现了Rails这个新特性。我想如果您此时就开发一个单独的模块,并且使用REST的风格,是毫无问题的。如果您要准备开始一个全新的项目,那么不妨考虑一下使用
REST,理由十分明显:清晰的架构,更少的代码,多个客户端的支持。
终于结束了,有关rails2.0 rest风格的新特性建议看一篇文章叫"Rolling with Rails 2.0 - The First Full Tutorial "。http://www.javaeye.com/topic/162536