RESTful 與 Rails


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 of nouns, verbs, and content types.

在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="&#x2713;" />
<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>