← 上一章:Routes 下一章:CRUD 分解動作 - 簡易票選系統實作(上) →
Controller
向你的用戶說聲哈囉
接續前一章,Route 解讀網址之後,會把工作轉往指定的 Controller 及 Action。在這個章節我們將會試著在畫面上跟使用者說聲哈囉,熟悉一下 Route、Controller 以及 View 是怎麼運作的。
Controller 是幹嘛的
Controller 中文可翻譯成「控制器」,顧名思義,就是用來控制流程用的。它可能需要跟 Model 要資料,可能需要跟 View 要 HTML template 來玩填空遊戲,或是可能需要存取外部服務(例如金流串接)等,這大多是 Controller 要做的工作。
命名慣例
在 Rails 的慣例中,Controller 的命名會根據 Route 是使用複數的 resources
還是單數 resource
方法而定。如果在 Route 是使用複數型態,例如:
Rails.application.routes.draw do
resources :posts
resources :users
end
在沒有特別指定 Resources 的 controller
參數的情況下,預設會對到的 Controller 就會是 PostsController
或是 UsersController
這樣的複數型態;反之,如果使用的是單數 resource
,對到的就會是單數命名的 Controller。
第 0 步 - 新增 Controller
在開始之前,讓我們使用 Rails 內建的產生器做一個全新的 Controller:
$ bin/rails g controller pages
Running via Spring preloader in process 16503
create app/controllers/pages_controller.rb
invoke erb
create app/views/pages
invoke test_unit
create test/controllers/pages_controller_test.rb
invoke helper
create app/helpers/pages_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/pages.coffee
invoke scss
create app/assets/stylesheets/pages.scss
上面這行指令會幫你做出一個 PagesController
類別,以及一些其它對應的檔案及目錄。Controller 的內容如下:
class PagesController < ApplicationController
end
這個 Controller 除了繼承自 ApplicationController
類別之外,裡面什麼內容都沒有。所以如果上手之後,也不一定要用產生器來幫你產生 Controller,直接自己手動新增也行。
第 1 步 - 新增 Route
別忘了,使用者想要看到你網站上的內容,第一步是要問過 Route,所以我們先在 Route 上簡單的加上一條:
Rails.application.routes.draw do
get "/hello_world", to: "pages#hello"
resources :posts
resources :users
end
當使用者輸入 /hello_world
網址的時候,會交給 PagesController
的 hello
方法處理。(是的,其實網址跟 Controller 上的 Action 不一定要同名)
第 2 步 - 把文字印出來吧!
有了 Route 之後,接下來回到 Controller 把 hello
這個 Action 加上去:
class PagesController < ApplicationController
def hello
render plain: "<h1>你好,世界!</h1>"
end
end
在 hello
方法裡要把文字輸出到瀏覽器上,不是使用 return
也不是使用 puts
,而是使用 render
方法,後面的 plain
參數是指要輸出一個一般的文字內容到畫面上。
有些剛開始學 Rails 的新朋友可能會想這樣做:
class PagesController < ApplicationController
def hello
render plain: "<h1>你好,世界!</h1>"
puts "---- 你好 ----"
end
end
使用 puts
方法把資料直接輸出在畫面上,看起來很直覺,但這樣不會有效果。事實上並不是 puts
方法不能用,它的確可以把東西印出來,只是不是印在瀏覽器上給你看到,而是印在 Rails 的 log 裡,仔細看一下正在執行 rails server
的那個畫面是不是有這樣的東西:
第 3 步 - 把工作交給 View 吧
雖然在第 2 步可以直接在 Controller 的 Action 裡透過 render
方法把資料輸出在畫面上,但如果遇到比較複雜的 HTML 通常就不會用這個方式了。在 Controller 裡的 Action,如果沒有特別指定 render
方法或參數,它會到 app/views/
的目錄找「 Controller 名字」目錄裡的 Action 同名檔案。以這個例子來說,它會去找 app/views/pages/hello.html.erb
。
如果這個 hello.html.erb
檔案不存在,就自己手動建一個吧。既然輸出的事情交給 View,那原來 hello
這個 Action 的 render
方法就可以拿掉:
class PagesController < ApplicationController
def hello
end
end
就這樣空空的,然後編輯 app/views/pages/hello.html.erb
<h1>你好,世界</h1>
<h2>我是設計師也看得懂的檔案喔</h2>
重新整理,應該就會看到跟剛才的差別:
這樣的好處是不用把 HTML 都寫在 Controller 裡(實務上也很少人真的會這麼做),再來就是要跟設計師合作的時候也比較方便。
Params 參數
接下來看看怎麼傳參數給 Controller。當使用者輸入這樣的網址:
/hello_world?name=5xruby&price=100&staff=20
畫面的輸出雖然沒變,但後面跟的那串東西會被當做參數傳進一個特別的變數叫做 params
。這是 Rails 預先幫我們定義好的,它可以捕捉到這個頁面的資訊。讓我們在剛剛的 hello
Action 裡加一些料:
class PagesController < ApplicationController
def hello
render json: params
end
end
使用 render
方法,把 params
這個變數用 JSON
的方式印出來,可以看到這個結果:
Rails 會把剛剛後面那串東西,整理成一個類似 Hash 的東西,例如我只想要 name
參數:
class PagesController < ApplicationController
def hello
render plain: params["name"]
end
end
不管是 GET 或是 POST 方式傳過來的參數,都會被收集到這個 params
裡。
實作練習:BMI 計算器
大概知道 Route、Controller、View 以及 Params 的使用方法後,接下來我們來做一個可以計算 BMI(Body Mass Index,身體質量指數)的計算機。
第 0 步 - 新增 Controller 及 Route
先用產生器把 Controller 做出來:
$ bin/rails g controller bmi index
Running via Spring preloader in process 18198
create app/controllers/bmi_controller.rb
route get 'bmi/index'
invoke erb
create app/views/bmi
create app/views/bmi/index.html.erb
invoke test_unit
create test/controllers/bmi_controller_test.rb
invoke helper
create app/helpers/bmi_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/bmi.coffee
invoke scss
create app/assets/stylesheets/bmi.scss
跟前面稍微有點不一樣的是在 Controller 後面多加了 index
這個參數,這會自動幫你做幾件事:
1 - 幫你加上 Route
Rails.application.routes.draw do
get 'bmi/index'
get "hello_world", to: "pages#hello"
resources :posts
resources :users
end
多加了 get 'bmi/index
條路徑。但這樣的寫法跟其它的寫法樣式不太一樣,所以我把它改成:
Rails.application.routes.draw do
get "bmi", to: "bmi#index"
get "hello_world", to: "pages#hello"
resources :posts
resources :users
end
這時候輸入路徑 /bmi
應該可以看到這個畫面:
2 - 自動幫 Controller 加上 index
Action:
class BmiController < ApplicationController
def index
end
end
3 - 自動幫你產生 app/views/bmi/index.html.erb
檔案
內容如下:
<h1>Bmi#index</h1>
<p>Find me in app/views/bmi/index.html.erb</p>
第 1 步 - 建立表單
編輯 app/views/bmi/index.html.erb
如下:
<h1>BMI 計算機</h1>
<%= form_tag '/bmi/result' do %>
身高:<%= text_field_tag 'body_height' %> 公分<br />
體重:<%= text_field_tag 'body_weight' %> 公斤<br />
<%= submit_tag "開始計算" %>
<% end %>
這裡有幾個需要稍做說明的地方:
1. form_tag
會被轉換成 HTML 的 <form>
標籤。
2. text_field_tag
會被轉換成 HTML 的 <input type="text" />
標籤。
3. submit_tag
會被轉換成 HTML 的 <input type="submit" />
標籤。
以上這些方法都統稱為 View Helper
,更多相關的使用方法參考 Form Helper 的 API 手冊。這時候的畫面會長得像這樣:
在繼續之前,先讓我們檢視一下這一頁的原始碼,仔細看一下跟表單有關的部份,稍做整理如下:
<form action="/bmi/result" accept-charset="UTF-8" method="post">
<input name="utf8" type="hidden" value="✓" />
<input type="hidden" name="authenticity_token" value="3xKrFHZfr6YL8q4tVLCvwSZQPb9FIXJKd7YL5h+mASxNozNuXiwRle3vUSlGKUsNpfF7H1/cUP2El4qMNfdPZg==" />
身高:<input type="text" name="body_height" id="body_height" /> 公分<br />
體重:<input type="text" name="body_weight" id="body_weight" /> 公斤<br />
<input type="submit" name="commit" value="開始計算" data-disable-with="開始計算" />
</form>
這邊有一段名字叫做 authenticity_token
的隱藏 input 標籤,不只內容看起來像是亂碼,而且每次重新整理又會得到不一樣的值,這個是做什麼用的呢?
我年輕時候的工作是網路行銷,因為工作的關係,常常需要撰寫讓網友們票選或是填寫資料抽獎之類的程式。遇到稍微有點技術底子的活動參加者,只要檢視網頁的原始碼,就可以看得出來這個表單要用什麼方式(GET 或 POST)、要送到什麼地方,以及要送的資料欄位名稱。有心人士只要寫一個簡單的小程式,仿照原頁面送資料到指定的地方,就可能可以造成灌票或是大量留言、灌水的情況,影響活動的公平性。但若因此而再加一些驗證規則,反而又提高了一般參加者的的門檻。
如果這個活動網站是用 Rails 開發的,Rails 預設在處理表單的時候會檢查這個 authenticity_token
是不是由本站所產生的,如果沒有這個欄位,或是這個欄位的值經 Rails 核對後發現並不是本身所產生,就會出現這個錯誤訊息:
不管是 form_tag
或是下個章節才介紹的 form_for
,在產生 <form>
標籤的時候都會自動幫你加上並產生 authenticity_token
的欄位,確保比較不會太容易被有心人士所破壞。
第 2 步 - 新增 Route
這時候當我們按下送出的時候會得到 Routing Error
的錯誤訊息,那是因為我們還沒有這個路徑,所以現在來補做一下。在 Route 裡加上一行:
Rails.application.routes.draw do
get "bmi", to: "bmi#index"
post "bmi/result", to: "bmi#result"
get "hello_world", to: "pages#hello"
resources :posts
resources :users
end
這樣可讓 Route 可以接到 POST
並轉往 BmiController
的 result
Action。
第 3 步 - 計算
Route 有了,接下來就是把 result
Action 的內容補上去:
class BmiController < ApplicationController
def index
end
def result
height = params[:body_height].to_f / 100 # 把單位換算成公尺
weight = params[:body_weight].to_f
# BMI 計算公式: BMI = 體重(單位:公斤) / 身高平方(單位:公尺).
@bmi = (weight / (height * height)).round(2)
end
end
BMI 的計算公式還滿單純的,不過要注意的是:
- 透過
params
取得的資料預設型態是字串,所以需要使用.to_i
或.to_f
轉換成數字否則會發生錯誤。這裡因為需要使用除法計算到小數點以下所以使用.to_f
方法進行轉換。 - 計算完的結果存成實體變數
@bmi
,以便讓 View 可以取用。
第 4 步 - 呈現結果
最後一步,把結果印出來。編輯檔案 app/views/bmi/result.html.erb
(如果檔案不存在請直接手動建立):
<h1>您的 BMI 值為:<%= @bmi %></h1>
搞定!試玩一下,我輸入身高 178 公分、體重 80 公斤:
按下送出即可得到計算結果:
小結
雖然這個計算機的功能相當陽春,而且也很多地方需要改善,例如防呆機制,或是根據計算結果嘲諷一下 BMI 值過高的胖子。但如果你能理解這個例子裡 Route、Controller、View 之間的基本運作原理,當下回遇到更複雜的應用程式開發相信也是可以迎刃而解。
以上實作完整程式碼可在 https://github.com/kaochenlong/bmi_calculator 取得。
Comments