应用Rails进行REST 开发

文章来源:http://mypages.javaeye.com/blog/291991

应用Rails进行REST 开发

前言
    Http协议除了 get post ,还可以做更多的事情,一直以来,很多的开发人员都忘了这一点。


   
但是,如果你知道浏览器其实只支持 get post, 那么你就不会感到惊奇了。

    get
post http请求从客户端传到服务器端的两个方法。除了这两个,http协议还知道 putdelete 方法,这两个方法告诉服务器创建或者删除一个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的内容,AJAXREST风格的应用的测试方法,还有“ActiveResource”-- REST的客户端部分。

什么是
REST?
    REST这个术语,是Roy Fielding Ph.D.
论文中提出来的,它的全称是“Representational State Transfer.”

    REST描述了这么一个架构:利用标准的http 协议中的 get, post, put,delete
来请求和操作网络上的资源。

    REST中,资源的意思就是一个 基于URL实体,客户端可以通过 http协议来和它进行交互。这个资源可以用各种形式来展示给客户端,如 HTMLXMLRSS,主要依赖于客户端的调用方式。并不像以往的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:

我们可以看出来,除了 POST 动作,其他三个URL都是相同的,原因很简单,因为要创建的那个资源还不存在呢。既然有三个URL都是相同的,那该怎么区分呢?其实是 http 协议的4个动作决定该调用哪个action。我们没有使用action,这就使得 我们不会写多余的 URL 和资源了。现在我们只需要2个URL: /projects/1 和 /projects ,如果是传统的方式,我们需要 /projects/new , /projects/show/1 , /projects/delete/1,/projects/update/1 4个URL.

有一点需要注意,之前我们也提到过,浏览器只能理解 POST 和 Get 两个动作,所以,当输入 http://localhost:3000/projects/1 的时候,会调用 show这个Action.所以,Rails 提供了一个辅助的方案来声称一个用于删除一个资源的链接:Delete 这个动作被放在一个隐藏的提交字段里(hidden field)提交给服务器;在创建新的资源的时候,也是适用相同的方法。这些内容都会在以下的章节里介绍。


在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中看到。

每一个path 方法都关联一个 http 协议的动作,一些请求(例如show,create),可以通过http协议的 Get 或 Post 传递给服务器;但是有一些请求,如 update,delete,则需要一些其他的方式(如使用隐藏的变量)传递给服务器,因为浏览器并不知道 PUT和DELETE动作。接下来的章节我们会仔细的介绍。

进一步看看这个表,我们也会发现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

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值