RESTful应用程式
RESTful 应用程式
The first 90% of the code accounts for the first 90% of the development time. The remaining 10% of the code accounts for the other 90% of the development time. – Tom Cargill, 贝尔实验室的物件导向程式专家
请注意本章内容衔接前一章,请先完成前一章内容。
什么是 RESTful?
RESTful 路由设计是 Rails 的一项独到的发明,它使用了 REST 的概念来建立一整组的命名路由 (named routes) 。
什么是 REST 呢?表象化状态转变 Representational State Transfer ,简称 REST ,是 Roy Fielding 博士在 2000 年他的博士论文中提出来的一种软件架构风格。相较于 SOAP 、 XML-RPC 更为简洁容易使用,也是众多网络服务中最为普遍的 API 格式,像是 Amazon 、 Yahoo! 、 Google 等提供的 API 服务均有 REST 接口。
REST 有主要有两个核心精神:1. 使用 Resource 来当做识别的资源,也就是使用一个 URL 网址来代表一个 Resource 2. 同一个 Resource 则可以有不同的 Representations 格式变化。这一章的路由实作了 Resource 概念,而 Representation 则是用了respond_to方法来实作,稍候我们也会介绍如何使用。
关于 REST 的理论可以参考笔者整理的什么是REST跟RESTful?。不过,了解理论并不是在 Rails 中使用 RESTful 路由的前提条件,所以大可以跳过不甚理解没关系。我们只要知道它可以带来什么技术上的具体好处,以及如何使用就足够了。
RESTful 带给 Rails 最大的好处是:它帮助我们用一种比较标准化的方式来命名跟组织 Controllers 和 Actions 。在没有 RESTful 之前,我们上一章介绍了外卡路由设计方式,也就是一个个指定 Controller 和 Action ,虽然十分地简便,但是却没有什么准则。同一个 Action 让不同的开发者设计,就很可能放在不同的 Controller 之下,更常见的是让一个 Controller 放太多不相关的 Action ,造成单一 Controller 过于庞大。
将 RESTful 带入 Rails 路由系统的点子,出自它对应了 HTTP 动词 POST 、 GET 、 PATCH/PUT 、 DELETE 到资料的新增、读取、更新、删除等四项操作。一旦将 HTTP 动词考虑进来,如此我们就将上一章手工打造 CRUD 的路由
/events/create/events/show/1/events/update/1/events/destroy/1
变成
POST /events对应到 Controller 中的 create actionGET /events/1对应到 Controller 中的 show actionPATCH /events/1对应到 Controller 中的 update actionDELETE /events/1对应到 Controller 中的 destroy action
什么是 HTTP method ?在 HTTP 通讯协定中制定了九种动词 (Verbs) 来跟服务器沟通,分别是 HEAD 、 GET 、 POST 、 PUT 、 PATCH 、 DELETE 、 TRACE 、 OPTIONS 、 CONNECT 。其中最常见的就是 GET 和 POST : GET 用来读取资料,这个动作不应该造成任何资料变更。而 POST 用于送出资料,这个动作不会被快取。而因为 HTML 只能送出 GET 或透过表单送出 POST , Rails 为了突破这个限制,在 POST 加上一个隐藏参数
_method=PATCH或_method=DELETE就可以当做 PATCH 和 DELETE 请求了。HTTP GET 和其他动词最大的差别在于它被认为是一个纯读取、不会修改任何资料的操作,不像 POST 、 PATCH 、 DELETE 会修改服务器上的资料。我们一般用浏览器 GET 网页,可以回上一页或重新整理,但是 POST 网页要重新整理时,浏览器会提示你是否要在执行一次,就是这个道理。
Rails 用这套惯例来大大简化了路由设定。那程式该怎么写呢?我们在 config/routes.rb 加入以下一行程式:
resources :events
如此就会自动建立四个命名路由 (named routes) ,搭配四个 HTTP 动词,对应到七个 Actions 。它的实际作用,就如同以下的 routes.rb 设定:
get '/events' => "events#index", :as => "events"
post '/events' => "events#create", :as => "events"
get '/events/:id' => "events#show", :as => "event"
patch '/events/:id' => "events#update", :as => "event"
put '/events/:id' => "events#update", :as => "event"
delete '/events/:id' => "events#destroy", :as => "event"
get '/events/new' => "events#new", :as => "new_event"
get '/events/:id/edit' => "events#edit", :as => "edit_event"
用这张表格会更清楚:
| Helper | GET | POST | PATCH/PUT | DELETE |
|---|---|---|---|---|
| event_path(@event) | /events/1 show action | /events/1 update action | /events/1 destroy action | |
| events_path | /events index action | /events create action | ||
| edit_event_path(@event) | /events/1/edit edit action | |||
| new_event_path | /events/new new action |
输入bin/rake routes也会列出目前设定的路由规则有哪些:
$ bin/rake routes
Prefix Verb URI Pattern Controller#Action
events GET /events(.:format) events#index
POST /events(.:format) events#create
new_event GET /events/new(.:format) events#new
edit_event GET /events/:id/edit(.:format) events#edit
event GET /events/:id(.:format) events#show
PATCH /events/:id(.:format) events#update
PUT /events/:id(.:format) events#update
DELETE /events/:id(.:format) events#destroy
welcome GET /welcome(.:format) welcome#index
welcome_say_hello GET /welcome/say_hello(.:format) welcome#say
root GET / welcome#index
其中的 Prefix 指的是在 View 的 Helper 命名,搭配_path_ (相对网址,不带有<http://your_domain>)或_url(绝对网址,带有<http://your_domain>)结尾就可以组合出_Helper_方法,例如welcome _say_hello_path_ _方法会产生出 /welcome/say hello 这样的网址。一般来说在网站的情境内都会使用相对网址,会用到绝对网址的情境则是在寄出去 Email 里面的超连结。
另外,注意到这七个 Action 方法的名字, Rails 是定好的,无法修改。这一套惯例建议你背起来,你可以这样记忆:
- show 、 new 、 edit 、 update 、 destroy 是单数,对单一元素操作
- index 、 create 是复数,对群集操作
event _path(@event)_需要参数,根据_HTTP 动词决定 show 、 update 、 destroyevents _path_毋需参数,根据_HTTP 动词决定 index 、 create
因此,最后我们不写:
link_to event.name, :controller => 'events', :action => :show , :id => event.id
而改写成:
link_to event.name, event_path(event)
只需记得 resources 名称,就可以推导出一整组的 URL Helper 方法。 Rails 就是利用这样的高阶概念,来简化路由的设计。
浏览器支援 PATCH/PUT 跟 DELETE 吗? Rails 其实偷藏了
_method参数。 HTML 规格只定义了 GET/POST ,所以 HTML 表单是没有 PUT/DELETE 的。但是 XmlHttpRequest 规格(也就是 Ajax 用的)有定义 GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS 。
修改成一个RESTful版本的CRUD
根据上一节所学到 RESTful 技巧,接续上一章的 CRUD 应用程式,来改造成 RESTful 应用程式,相信各位读者可以从中发现到 RESTful 所带来的简洁好处。让我们开始动手修改吧:
步骤一
编辑 config/routes.rb ,加入一个 Resources :
resources :events
请加在上方, routes.rb 里面越上面的规则优先权较高。
步骤二
编辑 app/views/events/index.html.erb ,修改各个link_to的路径:
<% @events.each do |event| %>
<li>
<%= event.name %>
<%= link_to "Show", event_path(event) %>
<%= link_to 'Edit', edit_event_path(event) %>
<%= button_to 'Delete', event_path(event), :method => :delete, :data => { :confirm => "Are you sure?" } %>
</li>
<% end %>
</ul>
<%= link_to 'New Event', new_event_path %>
注意到删除的地方,我们多一个参数:method => :delete。非GET的操作,顾及网页亲和力我们顺道把link _to_ 改成用button_to。link_to如果浏览器的_JavaScript_没开,就会无法送出 _GET 之外的操作。button _to_ 就无此困扰,因为_Rails 是产生form标籤夹带_method_ 参数。建议你可以用浏览器打开_HTML 原始码观察看看 Rails 实际产生出来的 HTML 标籤,可以有更好的认识。额外加上的:confirm参数则会让 Rails 的 JavaScript 跳出确认视窗。
步骤三
编辑 app/views/events/show.html.erb ,修改link_to的路径:
<%= @event.name %>
<%= simple_format(@event.description) %>
<p><%= link_to 'Back to index', events_path %></p>
步骤四
修改 app/views/events/new.html.erb 的表单送出位置如下:
<%= form_for @event, :url => events_path do |f| %>
在本例中,你也可以完全省略
:url参数, Rails 可以根据@event推算出路由。
步骤五
修改 app/views/events/edit.html.erb 的表单送出位置如下:
<%= form_for @event, :url => event_path(@event), :method => :patch do |f| %>
:url和:method也可以省略, Rails 自动会根据@event是新建的还是修改来决定要不要使用PATCH。
步骤六
修改 app/controllers/events_controller.rb ,将 create Action 和 destroy Action 里的redirect_to改成
redirect_to events_url
而 update Action 中的redirect_to改成
redirect_to event_url(@event)
步骤七
一旦完成 RESTful 之后,我们在上一章一开始设定的外卡路由就用不到了,编辑 config/routes.rb 将以下程式注砍掉:
match ':controller(/:action(/:id(.:format)))', :via => :all
外卡路由虽然设定上很方便,但已经不被推荐使用,它让所有 Actions 都可以透过 GET 访问到,而有安全上的顾虑。我们希望限制接收表单的 create Action 只允许 POST 请求。
至于就完成了 RESTful 的改造,将来我们就不会再用到外卡路由了。会直接使用resources的方式来建立 CRUD 应用。
常见错误
Unknown action
明明有在 config/routes.rb 里面定义了 resources 路由,但是出现以下的 Unknown action 错误:
排除打错字之外,其原因多半是跟 routes.rb 里面的定义顺序有关。注意到在 routes.rb 里面,越上面的路由规则越优先,例如如果你定义成:
Rails.application.routes.draw do
match ':controller(/:action(/:id(.:format)))', :via => :all
resources :events
end
那么网址 /events/4 就会优先比对到:controller/:action而去找4这个 Action ,这就错了。
Routing Error
这错误通常发生在link_to里,它抱怨找不到适合的路由规则来产生网址:
如果你是用外卡路由,那么如以下程式乱给一个不存在的 Controller ,就会产生一样的错误了:
link_to "foobar", :controller => "No such controller", :action => "blah"
因为{ :controller => "No such controller", :action => "blah" }比对不出有这个路由规则。但是如果是用 RESTful 路由呢?那多半是因为参数传错了,例如:
link_to "Show", event_path(@foobar)
这个@foobar没有定义所以是nil,event _path(@foobar)_对_Rails 内部来说等同于{ :controller => "events", :action => "show", :id => nil },这就造成了找不到路由的错误,它必须知道:id才能知道是那一个活动的 show Action 网址。
使用respond_to
respond _to_ 可以让我们在同一个_Action 中,支援不同的资料格式,例如 XML 、 JSON 、 Atom 等。让我们来实作看看。
Atom 是一种基于 XML 的供稿格式,被设计为 RSS 的替代品,广泛应用于 Blog feed 。
步骤一
修改 app/controllers/events_controller.rb 的 index Action 加上 XML 、 JSON 和 Atom 的支援,其中to _xml_ 和to_json是_ActiveRecord_内建的方法,可以将 _ActiveRecord 物件快速地转成 XML 和 JSON 资料格式。也因为是纯资料格式,所以不像 HTML 需要 erb template 档案,我们可以直接在 Action 中直接呼叫render将内容回传给浏览器:
def index
@events = Event.page(params[:page]).per(5)
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @events.to_xml }
format.json { render :json => @events.to_json }
format.atom { @feed_title = "My event list" } # index.atom.builder
end
end
至于 Atom 格式比较复杂一点,可以使用 template 。这里使用了 builder 这个引擎可以用 Ruby 语法来产生 XML 。新增 app/views/events/index.atom.builder 档案,内容如下:
atom_feed do |feed|
feed.title( @feed_title )
feed.updated( @events.last.created_at )
@events.each do |event|
feed.entry(event) do |entry|
entry.title( event.name )
entry.content( event.description, :type => 'html' )
end
end
end
打开浏览器分别浏览看看 <http://localhost:3000/events.xml> 、 <http://localhost:3000/events.json> 、 <http://localhost:3000/events.atom> 这几个附档名不同的网址。
步骤二
修改 app/controllers/events_controller.rb 的 show Action 加上 XML 和 JSON 的支援,这回我们试试看比较手工的方式,用 Builder 格式来建构 XML ,以及手动组 Hash 再转成 JSON 字串:
def show
@event = Event.find(params[:id])
respond_to do |format|
format.html { @page_title = @event.name } # show.html.erb
format.xml # show.xml.builder
format.json { render :json => { id: @event.id, name: @event.name }.to_json }
end
end
编辑 app/views/events/show.xml.builder :
xml.event do |e|
e.name @event.name
e.description @event.description
end
打开浏览器分别浏览看看 <http://localhost:3000/events/1.xml> 、 <http://localhost:3000/events/1.json> 等网址。
产生 JSON 还有其他方式,除了呼叫
to_json或手动转 Hash 物件来自订格式之外, Rails 有内建 JSON 专用的 template 引擎叫做 jbuilder ,你可以产生一个 app/views/events/show.json.jbuilder 的档案来产生 JSON 。如果需求是要制作给第三方或手机的 Web APIs ,那么我们就会改用 jbuilder 样板的方式,这样比较好写和好维护。
步骤三
如果想要加上这些格式的超连结,可以在 URL Helper 中传入:format参数。让我们修改 app/views/events/index.html.erb 加上不同格式的超连结:
<% @events.each do |event| %>
<li>
<%= link_to event.name, event_path(event) %>
<%= link_to " (XML)", event_path(event, :format => :xml) %>
<%= link_to " (JSON)", event_path(event, :format => :json) %>
<%= link_to 'edit', edit_event_path(event) %>
<%= button_to 'delete', event_path(event), :method => :delete, :data => { :confirm => "Are you sure?" } %>
</li>
<% end %>
</ul>
<%= link_to 'new event', new_event_path %>
<%= link_to "Atom feed", events_path(:format => :atom) %>
行数统计
到目前为止,总共写了多少程式了呢? Rails 提供了一个简单的指令可以知道:
$ bin/rake stats
就会输出这样的表格:
+----------------------+-------+-------+---------+---------+-----+-------+
| Name | Lines | LOC | Classes | Methods | M/C | LOC/M |
+----------------------+-------+-------+---------+---------+-----+-------+
| Controllers | 86 | 61 | 2 | 7 | 3 | 6 |
| Helpers | 4 | 4 | 0 | 0 | 0 | 0 |
| Models | 2 | 2 | 1 | 0 | 0 | 0 |
| Libraries | 0 | 0 | 0 | 0 | 0 | 0 |
| Integration tests | 0 | 0 | 0 | 0 | 0 | 0 |
| Functional tests | 49 | 39 | 1 | 0 | 0 | 0 |
| Unit tests | 11 | 6 | 2 | 0 | 0 | 0 |
+----------------------+-------+-------+---------+---------+-----+-------+
| Total | 152 | 112 | 6 | 7 | 1 | 14 |
+----------------------+-------+-------+---------+---------+-----+-------+
Code LOC: 67 Test LOC: 45 Code to Test Ratio: 1:0.7
其中 LOC 是指不包含空行的行数。
如何除错?
如果是 Model 中的程式,你可以在命令列下输入 rails console ,然后在 Console 中呼叫看看 Model 的方法看看正确与否。而除错 Controller 和 Views 一个简单的方法是你可以使用 debug 这个 Helper 方法,例如在 app/views/events/show.html.erb 中插入:
<%= debug(@event) %>
这样就会输出@event这个值的详细内容。不过,更为常见的是使用 Logger 来记录资讯到 log/development.log 里。
关于Logger
在 Rails 环境中,你可以直接使用 logger 或是Rails.logger来拿到这个 Logger 物件,它有几个方法可以呼叫:
- logger.debug 除错用的讯息, Production 环境会忽略
- logger.info 值得记录的一般讯息
- logger.warn 值得记录的警告讯息
- logger.error 错误讯息,但还不到网站无法执行的地步
- logger.fatal 严重错误到网站无法执行的讯息
例如,你想要观察程式中变量 @event 的值,你可以插入以下程式到要观察的程式段落之中:
Rails.logger.debug("event: #{@event.inspect}")
接着开浏览器跑实际跑过这段程式,那么就会在rails server的标准输出中,看到这个除错讯息。或是你也可以另开一个指令视窗执行tail -f log/development.log来观察 log 档案。
在 Production 环境中, log/production.log 会逐渐长大,可以使用 logrotate 定期整理 Rails Log 档案。
我们会在测试一章进一步介绍如何撰写测试程式,撰写单元测试可以大大降低除错时间。
