ruby

领取游戏新手卡来http://5433.com

导航

Rails进行REST开发

应用Rails进行REST开发

这篇文档翻译自《RESTful Rails Development》,只为了交流和经验分享而用.

前言

    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的客户端部分。

    在我们开始之前,再啰嗦最后一句:要读这个教程,最少你要懂一点Rails开发的基本知识,否则的话,先去学习学习吧:)

 

1.1 什么是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个实例形式。

 

1.2 为什么使用REST?

    问得好!我们已经使用MVC模式开发Rails应用2年了,为什么要使用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风格的开发方式,会使系统结构更加清晰,而且容易维护。

   

    接下来的章节中,我们会用例子来逐步的让您明白上面所描述的这些特性。

 

1.3 有什么新玩意?

 

    如果你觉得 REST 让你之前所有的开发经验变得毫无作用,别担心,那是不可能的~ 因为REST仍然是基于 MVC 风格的。从技术角度来看,REST中的“新玩意”可以归纳为以下几点:

    a) 在 controller 中的 respond_to 的用法。

    b) 对于 link 和 form 的新的 helper 方法。

    c) 在 controller redirect 中的 url 方法。

    d) 在 routes.rb 文件中,新的路由定义方法。

 

一旦你了解了 REST ,而且经常使用它,那么自然而然,你就会设计一个REST的应用了!

 

 

1.4 准备

      接下来,我们要以我们的一本书《RapidWeb Development mit Ruby on Rails》中的一个例子“项目管理应用”来描述Rails的REST方面的特性。我们并不会编写整个应用,但是我们会使用相同的技术去描述REST的内容。

      下面,我们开始吧!首先创建一个rails的应用:

> rails ontrack

然后,我们来创建 开发 和 测试 的数据库。

> mysql -u rails -p

Enter password: *****

mysql> create database ontrack_development;

mysql> create database ontrack_test;

mysql> quit

 

 

1.4.1 Rails 1.2

 

      假设并不是所有的人都希望在系统里安装rails 1.2――因为那样的话,所有的应用都必须跑在rails 1.2 之下了。所以我们建议只是让这个例子使用 rails 1.2。参考文档,rails 1.2 的 tag 是 rel _1-2-1。所以我们使用 rake 命令来让 ontrack 项目使用 1.2 的版本。

> cd ontrack

> rake rails:freeze:edge TAG=rel_1-2-1

 

1.5 Resource Scaffolding

 

      基于REST的rails 应用可以使用新的脚手架(scaffold)命令 scaffold_resource 来方便的创建。可以传递给生成器这样一些参数:在本例中,就是资源的名字 “project”, 和 这个model 的字段名字以及类型。字段的类型是必须的,因为我们要使用migration,以及在视图中显示这些字段。

 

> cd ontrack

> ruby script/generate scaffold_resource project name:string desc:text

exists app/models/

exists app/controllers/

exists app/helpers/

create app/views/projects

exists test/functional/

exists test/unit/

 

create app/views/projects/index.rhtml

create app/views/projects/show.rhtml

create app/views/projects/new.rhtml

create app/views/projects/edit.rhtml

create app/views/layouts/projects.rhtml

create public/stylesheets/scaffold.css

create app/models/project.rb

create app/controllers/projects_controller.rb

create test/functional/projects_controller_test.rb

create app/helpers/projects_helper.rb

create test/unit/project_test.rb

create test/fixtures/projects.yml

create db/migrate

create db/migrate/001_create_projects.rb

route map.resources :projects

 

      生成器不仅生成了 model, controller, view, 还生成了完整的migration 脚本,以及在 routes.rb 里生成了一个映射:map.resources :projects   ,对于新生成controller来说,这是用于REST方面的最后一个映射项目。我们现在先不深入研究 routes.rb, 还是先一步一步地看看我们刚才用生成器说生成的东西吧!

1.6 Model

      就好像我们之前提过的,在Rails 里,REST的资源是由 controller 和 model来组成的。所谓model,就是一个普通的 ActiveReocrd类,继承自 ActiveRecord::Base:

 

class Project < ActiveRecord::Base

end

 

所以,对于model 这部分来说,没什么新鲜的东西。不过,别忘了创建这个表:

> rake db:migrate

 

1.7 Controller

 

      生成的controller “ProjectsController” 是一个具有 CRUD行为的controller,它可以操作 “Project” 这个资源(还记得刚才创建的 projects 表吧?)。

      这就表明,一个controller 属于某一个特定的资源,并且具有一些符合标准的动作来执行 CRUD的操作。例如:

 

Listing 1.1: ontrack/app/controllers/projects controller.rb

class ProjectsController < ApplicationController

# GET /projects

# GET /projects.xml

def index...

# GET /projects/1

# GET /projects/1.xml

def show...

# GET /projects/new

def new...

# GET /projects/1;edit

def edit...

# POST /projects

# POST /projects.xml

def create...

# PUT /projects/1

# PUT /projects/1.xml

def update...

end

# DELETE /projects/1

# DELETE /projects/1.xml

def destroy...

end

 

      如果我们来看看刚才生成的ProjectController,会发现其实并没有什么新鲜的东西,无非也就是这么一些操作:创建(create),读取(retrieve),更新(update),删除(delete)这些操作。需要强调注意:这些操作都是针对Project 这个资源的。Controller 和 Action 看起来都很普通,但是仔细看一下,每个 Action 都会有一些注释,这些注释表明了 url 和 http 所使用的动作。这些注释所体现的,就是 REST风格的 URL。接下来的章节,我们会仔细分析一下这些URL的内容。

 

1.7.1 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)提交给服务器;在创建新的资源的时候,也是适用相同的方法。这些内容都会在以下的章节里介绍。

 

1.7.2 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的样式。

      这两种方式我们都会介绍的。

 

1.7.3 http-header 中使用 accept 变量。

 

      我们先来看看控制respond_to 的第一种方式。设置 http-header 的方式很简单,你可以使用一个工具“curl”。好,我们首先启动 webrick:

 

 

> ruby script/server webrick

=> Booting WEBrick...

=> Rails application started on http://0.0.0.0:3000

=> Ctrl-C to shutdown server; call with --help for options

[2006-12-30 18:10:50] INFO WEBrick 1.3.1

[2006-12-30 18:10:50] INFO ruby 1.8.4 (2005-12-24) [i686-darwin8.6.1]

[2006-12-30 18:10:50] INFO WEBrick::HTTPServer#start: pid=4709 port=3000

 

在浏览器中会看到如下的效果:

 

然后,我们使用 curl 这个工具来请求 project 这个资源,并且想要得到xml 格式的信息。

 

> curl -H "Accept: application/xml" \

-i -X GET http://localhost:3000/projects/1

=>

HTTP/1.1 200 OK

Connection: close

Date: Sat, 30 Dec 2006 17:31:50 GMT

Set-Cookie: _session_id=4545eabd9d1bebde367ecbadf015bcc2; path=/

Status: 200 OK

Cache-Control: no-cache

Server: Mongrel 0.3.13.4

Content-Type: application/xml; charset=utf-8

Content-Length: 160

<?xml version="1.0" encoding="UTF-8"?>

<project>

<desc>Future of Online Marketing</desc>

<id type="integer">1</id>

<name>Wunderloop</name>

</project>

 

Rails的处理器调用了 show 这个action。因为我们已经在 http-header 里通过 “Accept: application/xml”指定了需要xml的信息,所以,“show” 这个 action 就调用了 format.xml,然后返回给我们xml的信息。

 

Curl 这个工具不仅可以方便的测试返回不同格式的情况,我们还可以用它来测试一些浏览器所不支持的命令,例如,我们要删除 id = 1 的这个资源:

 

 

> curl -X DELETE http://localhost:3000/projects/1

=>

<html><body>You are being

<a href="http://localhost:3000/projects">redirected</a>.

</body></html>

 

这次发送的请求使用了http协议的 DELETE方法。Rails 得到了http的这个动作,然后就会调用 destroy 方法。注意 URL看起来没什么不同,不同的是 http 包含的动作。

 

1.7.4 改变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。

 

 

1.8 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 就不同了。

 

1.8.1 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和这两个很相似,我们会在后面的章节介绍。

 

1.8.2 在 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方法。

 

1.8.3 删除

 

      恐怕我们已经发觉了,用于显示和删除一个资源,所使用的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"

onclick="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 方法。

 

 

1.9 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

 

1.10 REST风格的路由

 

      到目前为止,我们介绍了REST的内容,以及在 链接,form,controller中所适用的一些新的helper方法。但是我们没有解释那些helper方法是从哪来的?决定那些方法存在的,以及指定那些方法会调用哪个controller的哪个action,就是一个文件,那就是 /config/routes.rb。

 

map.resources :projects

 

这个配置是由我们上面适用 scaffold 生成器生成的。生成器生成了一个路由,当处理请求时,controller 需要这个路由才能知道调用哪个action。

      此外,resources 生成了path 和 url 的helper方法去操作“project”这个资源。

 

 

 

 

map.resources :projects

=>

Route Generated Helper

-----------------------------------------------------------

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)

 

1.10.1 习惯

 

      要进行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 都必须实现这几个方法。

 

1.10.2 定制路由

 

      通过以下一些选项,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

 

1.11 嵌套的资源

 

      当适用嵌套的资源的时候,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

 

1.11.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 等等方法也必须进行修改。下面的章节我们会逐步介绍。

 

1.11.2 在 “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)

 

      如果您觉得用对象作为参数不够清晰,那么就可以考虑一下这个方式。

 

 

 

1.11.3 增加新的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) %>

 

1.11.4 编辑 Iteration

 

      为了能够编辑 iteration,至少需要修改2个地方。1〉在视图中的form_for 方法的参数,目前的参数只有 iteration 一个,还需要增加 project id。

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的操作大部分都可以正常工作了,但是还有一些细节的地方我们没有处理,这个就留给您作为“练习”来完成吧!

 

 

1.12 自定义 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)

 

1.12.1 我们是否仍然“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’”。

 

 

1.13 自定义信息格式

 

      目前 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

 

 

1.14 在REST中使用AJAX

 

      在REST风格的系统中使用AJAX?非常简单,可以说这一小节没什么新鲜的玩意要学习。您还是使用以前所使用的 remote 系列的helper 方法,只不过传递的参数需要改变,现在使用 path helper 方法,而不是以前所使用的 contoller, action 。下面的例子会让您更清晰的明白这一点:

 

link_to_remote "Destroy", :url => project_path(project),

:method => :delete

=>

<a href="#" onclick="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 外实现逻辑处理,能够极大地降低重复的代码。

 

 

 

1.15 测试

 

      不管开发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风格应用的唯一区别。

 

1.16 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的代码,亲自体验一下吧!

 

1.17 大结局

 

      这个世界并不是非得需要REST。有很多的解决方案可以考虑,并且可以很容易实现。大概更多的时候,是您可能现在正处于某个项目的中期,这时,您发现了Rails这个新特性。我想如果您此时就开发一个单独的模块,并且使用REST的风格,是毫无问题的。如果您要准备开始一个全新的项目,那么不妨考虑一下使用REST,理由十分明显:清晰的架构,更少的代码,多个客户端的支持。

 

 

posted on 2010-02-28 17:03  最初的模样  阅读(2127)  评论(0编辑  收藏  举报