Rails RESTful 實作

Update(2006/12/4): 一些小修改跟 link_to 的 web accessibility 補充。

Update(2006/12/11): 請接著看系列文章下一集: Rails RESTful 相關工具

Rails RESTful 彈第三篇,我想 David Heinemeier Hansson 的這篇投影片是很好的開場白:  Discovering a world of Resources on Rails

本來這篇想寫的仔細點,不過後來我發現AWDwR第二版講 Resource-Based Routing 的頁數還不少(十幾頁吧,還蠻詳細的),要用 Rails 的人應該都去買一本,我想我這裡就不巨細靡遺了… :p

在 Rails 中是如何實作 RESTful 支援的? 首先是在 Resource Routes 上,在 routes.rb 加入

map.resources :users

這樣的宣告將在自動對應 URL路徑跟 Controller 的 action ,而有以下的結果 :

GET: /users => [:action => ‘index’]
GET: /users.xml => [:action => ‘index’, :format => ‘xml’]
GET: /users/1 => [:action => ‘show’, :id => 1]
GET: /users/1;edit => [:action => ‘edit’, :id => 1]
GET: /users/1.xml => [:action => ‘show’, :id => 1, :format => ‘xml’]
POST: /users => [:action => ‘create’]
PUT: /users/1 => [:action => ‘update’, :id => 1]
DELETE: /users/1 => [:action => ‘destroy’, :id => 1]

也就是使用 named routes 來實作出 verb-oriented controllers,單一個 resource 根據 HTTP verb 而有不同的行為。

Resource method 也接受一些進階參數,例如:

map.resources :articles,
  :collection => {:sort => :put}, #客製 method 給群集,並指定用哪種 HTTP verb ( :get/:post/:put/:delete或:any)
  :member => {:deactivate => :delete}, # 客製 method 給某元素
  :new => {:preview => :post}, #客製 method 給新元素
  :controller => ‘articles’, #指定 Controller
  :singular => ‘article’, #指定用單數
  :path_prefix => ‘/teams/:team_id’, #在routes前加上route variables,舉例如下。
  :name_prefix => ‘my_’ #在產生的 helper 前加prefix,通常配合 path_prefix 或 nested 使用避免名稱衝突。

上述的 path_prefix 可以改用Nested 用法,例如:

map.resources :teams do |teams|
    teams.resources :players
end 

產生的對應效果是

GET: /teams/13/players/1 => [:controller => ‘players’, :action => ‘show’, :team_id => 13, :id => 1]

除了產生對應 routes 的 action 給 Controller,最方便的是自動產生了一些 helpers 給 views,例如 (這裡的team,player根據你的resource name而自行替換,注意單複數):

helpers HTTP Verb 產生的Path 對應的Action
teams_path GET /teams index
team_path(id) GET /teams/1 show
new_team_path GET /teams/new new
teams_path POST /teams create
edit_team_path(id) GET /teams/1;edit edit
team_path(id) PUT /teams/1 update
team_path(id) DELETE /teams/1 destroy

而 Nested Resource 的用法舉例:

helpers 產生的Path
players_path(@team) /teams/1/players
player_path(@team, @player ) /teams/1/players/5

有了這些 helper,我們就可以這樣產生按鈕、超連結、Ajax超連結跟表單 :

button_to “Destroy”, team_path(@team), :confirm => “Are you sure?”, :method => :delete

link_to “Destroy”, team_path(@team), :confirm => “Are you sure?”, :method => :delete

link_to_remote “Destroy”, :url => team_path(@team), :confirm => “Are you sure?”, :method => :delete

form_for :team, @team, :url => team_path(@team), :html => { :method => :put } do |f| …

其中因為瀏覽器不支援 PUT 跟 DELETE,所以是 link_to 是用 Javascript + DOM 在按下動作時生成表單 form 跟 _method 參數用 HTTP POST 來模擬,而 form_for 是在 HTML 中加上隱藏的 _method 參數。若你關心 web accessibility 議題,那麼 link_to 除了HTTP GET之外都應改用 button_to,因為這樣即使 Javascript 關閉了還可以正確送出(因為 button_to 的 _method 參數是 hard-code 在HTML form裡)。anyway… 這些都被 helper 隱藏起來了,用起來不用擔心,要指定 HTTP verb 時只需設定 :method 參數即可。

整理一下Resourse的 named routes,可以分成三種情況: collection 群集、member 某個元素、new 新元素,每種又可以搭配不同 HTTP verb,然後與 Controller 裡的 action 配對。例如預設 /teams + GET 對應 action index、/teams + POST 對應 action create、/teams/1 預設對應 action show、 /teams/new 預設對應 action new等。

現實狀況中除了 GET/POST/PUT/DELETE 這些 HTTP verb之外,對該 resourse 還會需要其它客製處理,因此這三類分別還可以加 method 上去,首先在 routes.rb 對該 resource 加設定: 指定哪種情況(:collection/:member/:new)、搭配的 Controller action 是哪個、用什麼 HTTP verb,這樣就會有新的 named routes 出來了,舉例如下:

客製出 helpers 產生的 Path 對應的Action Map Options的設定 使用的HTTP Verb
sort_tags_path /tags;sort sort :collection => { :sort => :put } PUT
deactivate_tag_path(id) /tag/1;deactivate deactivate :member => { :deactivate => :delete } DELETE
preview_new_tag_path /tags/new;preview preview :new => { :preview => :post } POST

你可能會覺得 new 的角色有點奇怪,想了一下應該這樣看: new是針對一個(資料庫中)還不存在且沒有id的新元素,而 member 是針對已經存在有id的。

RESTful 三位一體的一角是 format 格式,所以你可以用 respond_to 來設定不同格式要怎麼處理:

respond_to { |wants| wants.all | .text | .html | .js | .ics | .xml | .rss | .atom | .yaml }

它會根據 client 的請求(HTTP header的 Accept 或 Content-Type 或 URL中副檔名的不同)來決定回應的格式,例如:

helpers 產生的Path
formatted_teams_path(:xml) /teams.xml
formatted_team_path(id,:rss) /teams/1.rss
formatted_players_path(@team,:atom) /teams/1/players.atom
formatted_player_path(@team,@player, :js) /teams/1/players/5.js
formatted_player_path(:team_id => 1, :id => 5, :format => :js ) /teams/1/players/5.js

這樣看下來 Rails RESTful 除了有其重要意義之外,也帶來了非常多的慣例來簡化開發程序。看似規則多,其實組合彈性大,好處很多(Consistency/Simplicity/Discoverability)。不過魔術變多了,在一般沒有 RESTful 概念的前提下,一開始要上手的難度也提高了(會有很多why這樣做的疑問…:p)。

by the way… G小R裝用功 也寫了一篇 Rails RESTful 初探 可以參考看看。

參考資料:

參與討論

2 則留言

  1. 我那篇只是粗淺的摸一摸,然後把看到的東西記錄一下而已
    相較之下這篇真是太詳細了 又讓我搞懂了一些原本不清楚的東西 獲益良多

發佈留言

發表迴響