Rails支援REST風格的軟體架構,REST全名REpresentational State Transfer,可譯為表徵狀態轉移,為 Roy Fielding 於2000年在他的論文 Architectural Styles and the Design of Network-based Software Architectures 中提及。
REST的架構由客戶端(Client)/伺服端(Server)組成,客戶端與伺服端之間的通訊機制是無狀態的(Stateless),客戶端對伺服端請求資源(Resource),伺服端的回應為資源的表徵(Representation),或稱為表現方式,也就是說,資源在REST中是可定址的(Addressed)概念,可能用檔案、文件、格式等來表現,代表資源目前或可能的狀態(State)。客戶端發出的請求,會獲得資源的最新狀態,如果一或多個請求獲取的狀態有了差異,客戶端就認定為發生了轉移(Transition),客戶端獲取的表徵,可能包括發生下一次狀態轉移的連結,請求方法與回應方式,是根據資源的表徵狀態將如何轉移(Transfer)而決定。
在REST的架構中,資源是可定址的(Addressed)概念,會有獨一無二的識別名稱(例如Web中的URI名稱),請求動作必須能表現出如何處理請求(例如HTTP中的GET、POST、PUT、DELETE等請求),而回應的內容型態與資源的概念是分離的,一個資源可以有多種內容型態來展現(狀態)。
在設計REST風格的架構時,許多人會提到REST Triangle,例如 Talk:Representational state transfer 中的這張圖:
在REST Triangle中有名詞(Nouns)、動詞(Verbs)與內容型態(Content Types),分別用以代表資源獨一無二的識別、對資源進行操作的動作,以及資源的表徵(表現方式)。以HTTP來說,URI就是處於名詞角色,為資源定義了識別名稱,HTTP具有一組有限的GET、POST、PUT與DELETE等方法來操作資源,而HTTP可以使用content-type標頭來定義資源表現方式。這些概念與REST概念不謀而合,REST架構基於HTTP 1.0,與HTTP1.1平行發展,符合REST最大實現就是WWW,整個Web就像是個狀態機,藉由連結不斷改變狀態,不過REST架構的風格與特定協定無關,雖然最初是使用HTTP來描述,但不限於HTTP。
更多REST的概念,可以參考 Representational state transfer。來看看HTTP如何實現REST概念,以 基本 CRUD 程式 中的例子來說,針對第一筆書籤而言,有以下的URL:
- /bookmarks/show/1
- /bookmarks/update/1
- /bookmarks/destroy/1
第一筆書籤就是一個資源(符合REST資源可以被定址的概念),如果搭配HTTP請求:
- GET /bookmarks/1
- PUT /bookmarks/1
- DELETE /bookmarks/1
那麼第一筆書籤的URL識別就是/bookmarks/1,而根據請求為GET、PUT或POST,就可以知道對該資源要作什麼樣的處理(符合REST中請求動作必須能表現出本身如何處理的概念)。
以上說明的是REST的基本實現概念。符合REST風格的實現稱為RESTful,Rails就以REST的概念來簡化路由設定,並使用respond_to方法來實現同一資源的不同呈現。
以 基本 CRUD 程式 為例,如果在routes.rb中如下設定:
- routes.rb
resources :bookmarks
# match ':controller(/:action(/:id(.:format)))' 這行不需要了,所以註解掉...
就會相當於routes.rb中作以下設定(當然你也可以自行視需求如下設定,而不一定要如上撰寫resources :bookmarks):
get '/bookmarks' => "bookmarks#index", :as => "bookmarks"
post '/bookmarks' => "bookmarks#create", :as => "bookmarks"
get '/bookmarks/:id' => "bookmarks#show", :as => "bookmark"
put '/bookmarks/:id' => "bookmarks#update", :as => "bookmark"
delete '/bookmarks/:id' => "bookmarks#destroy", :as => "bookmark"
get '/bookmarks/new' => "bookmarks#new", :as => "new_bookmark"
get '/bookmarks/:id/edit' => "bookmarks#edit", :as => "edit_bookmark"
這個慣例是Rails制定的,只能強記,不能更改,這麼作的好處除了符合REST風格外,產生的路由輔助方法,在需要產生鏈結的地方可以簡化。例如原先的index.html.erb:
- index.html.erb
<ul>
<% @pages.each do |page| %>
<li>
<%= link_to page.title, page.url %>
<%= link_to "Details", :controller => 'bookmarks', :action => 'show', :id => page %>
<%= link_to "Edit", :controller => 'bookmarks', :action => 'edit', :id => page %>
<%= link_to "Delete", :controller => 'bookmarks', :action => 'destroy', :id => page %>
</li>
<% end %>
</ul>
<%= link_to "New", :controller => 'bookmarks', :action => 'new' %>
可以簡化為:
- index.html.erb
<ul>
<% @pages.each do |page| %>
<li>
<%= link_to page.title, page.url %>
<%= link_to "Details", bookmark_path(page) %>
<%= link_to "Edit", edit_bookmark_path(page) %>
<%= link_to "Delete", bookmark_path(page), :method => :delete %>
</li>
<% end %>
</ul>
<%= link_to "New", new_bookmark_path %>
如果不寫:method,預設就是:get。對於new_html.erb:
- new.html.erb
<%= form_for @page, :url => { :controller => 'bookmarks', :action => 'create' } do |f| %>
...略
<% end %>
可改為:
- new.html.erb
<%= form_for @page, :url => bookmarks_path, :method => :post do |f| %>
...略
<% end %>
對於show.html.erb:
- show.html.erb
...略
<%= link_to "Home", root_path %>
<%= link_to "Edit", :controller => 'bookmarks', :action => 'edit', :id => @page %>
<%= link_to "Delete", :controller => 'bookmarks', :action => 'destroy', :id => @page %>
可修改為:
- show.html.erb
...略
<%= link_to "Home", root_path %>
<%= link_to "Edit", edit_bookmark_path(@page) %>
<%= link_to "Delete", bookmark_path(@page), :method => :delete %>
對於edit.html.erb:
- edit.html.erb
<%= form_for @page, :url => { :controller => 'bookmarks', :action => 'update', :id => @page } do |f| %>
...略
<% end %>
可修改為:
- edit.html.erb
<%= form_for @page, :url => bookmark_path(@page), :method => :put do |f| %>
...略
<% end %>
由於HTML網頁上,表單的method只能設定GET、POST,因此Rails中若設定:method為:get、:post以外的值時,如果是鏈結,會在<a>上加上個data-method屬性。例如index.html.erb產生的HTML為:<!DOCTYPE html>
<html>
<head>
<title>Bookmark</title>
<link href="/assets/application.css?body=1" media="screen" rel="stylesheet" type="text/css" />
<link href="/assets/bookmarks.css?body=1" media="screen" rel="stylesheet" type="text/css" />
<script src="/assets/jquery.js?body=1" type="text/javascript"></script>
<script src="/assets/jquery_ujs.js?body=1" type="text/javascript"></script>
<script src="/assets/bookmarks.js?body=1" type="text/javascript"></script>
<script src="/assets/application.js?body=1" type="text/javascript"></script>
<meta content="authenticity_token" name="csrf-param" />
<meta content="QSkysEtZsLhKIedQitVJaPb2p9ij5/rsQbxm97bjQas=" name="csrf-token" />
</head>
<body>
<ul>
<li>
<a href="https://openhome.cc">Openhome</a>
<a href="/bookmarks/1">Details</a>
<a href="/bookmarks/1/edit">Edit</a>
<a href="/bookmarks/1" data-method="delete" rel="nofollow">Delete</a>
</li>
...略
</ul>
<a href="/bookmarks/new">New</a>
</body>
</html>
實際上按下鏈結時,會以Ajax的方式,以指定的data_method來發送請求。
如果是表單,則會用隱藏欄位方式,額外發送一個_method參數。例如edit.html.erb產生的HTML為:
<!DOCTYPE html>
<html>
<head>
<title>Bookmark</title>
<link href="/assets/application.css?body=1" media="screen" rel="stylesheet" type="text/css" />
<link href="/assets/bookmarks.css?body=1" media="screen" rel="stylesheet" type="text/css" />
<script src="/assets/jquery.js?body=1" type="text/javascript"></script>
<script src="/assets/jquery_ujs.js?body=1" type="text/javascript"></script>
<script src="/assets/bookmarks.js?body=1" type="text/javascript"></script>
<script src="/assets/application.js?body=1" type="text/javascript"></script>
<meta content="authenticity_token" name="csrf-param" />
<meta content="QSkysEtZsLhKIedQitVJaPb2p9ij5/rsQbxm97bjQas=" name="csrf-token" />
</head>
<body>
<form accept-charset="UTF-8" action="/bookmarks/1"
class="edit_page" id="edit_page_1" method="post">
<div style="margin:0;padding:0;display:inline">
<input name="utf8" type="hidden" value="✓" />
<input name="_method" type="hidden" value="put" />
<input name="authenticity_token" type="hidden"
value="QSkysEtZsLhKIedQitVJaPb2p9ij5/rsQbxm97bjQas=" />
</div>
...略
<input name="commit" type="submit" value="Update" />
</form>
</body>
</html>
像index.html.erb使用鏈結,必須使用Ajax,也就是必須使用JavaScript才能完成任務,如果考慮客戶端關閉瀏覽器時,基本的CRUD也可以運作,則可以將link_to改為button_to。例如:
- index.html.erb
<ul>
<% @pages.each do |page| %>
<li>
<%= link_to page.title, page.url %>
<%= button_to "Details", bookmark_path(page), :method => :get %>
<%= button_to "Edit", edit_bookmark_path(page), :method => :get %>
<%= button_to "Delete", bookmark_path(page), :method => :delete %>
</li>
<% end %>
</ul>
<%= link_to "New", new_bookmark_path %>
如此產生的HTML中就會出現按鈕,而實際上會以表單方式來發送請求。例如產生的HTML為:
<!DOCTYPE html>
<html>
<head>
<title>Bookmark</title>
<link href="/assets/application.css?body=1" media="screen" rel="stylesheet" type="text/css" />
<link href="/assets/bookmarks.css?body=1" media="screen" rel="stylesheet" type="text/css" />
<script src="/assets/jquery.js?body=1" type="text/javascript"></script>
<script src="/assets/jquery_ujs.js?body=1" type="text/javascript"></script>
<script src="/assets/bookmarks.js?body=1" type="text/javascript"></script>
<script src="/assets/application.js?body=1" type="text/javascript"></script>
<meta content="authenticity_token" name="csrf-param" />
<meta content="QSkysEtZsLhKIedQitVJaPb2p9ij5/rsQbxm97bjQas=" name="csrf-token" />
</head>
<body>
<ul>
<li>
<a href="https://openhome.cc">Openhome</a>
<form action="/bookmarks/1" class="button_to" method="get">
<div><input type="submit" value="Details" /></div>
</form>
<form action="/bookmarks/1/edit" class="button_to" method="get">
<div><input type="submit" value="Edit" /></div>
</form>
<form action="/bookmarks/1" class="button_to" method="post">
<div>
<input name="_method" type="hidden" value="delete" />
<input type="submit" value="Delete" />
<input name="authenticity_token" type="hidden"
value="QSkysEtZsLhKIedQitVJaPb2p9ij5/rsQbxm97bjQas=" />
</div>
</form>
</li>
</ul>
<a href="/bookmarks/new">New</a>
</body>
</html>