本課程通過對控制器的學(xué)習(xí),了解 Rails 如何通過處理請求和作出相應(yīng)來控制邏輯的,并且完成網(wǎng)店中購物和支付流程。
控制器 Controller 是 MVC 中調(diào)度員的角色,它接收客戶端發(fā)送過來的請求,并且通過我們編寫的代碼作出相應(yīng),實(shí)現(xiàn)業(yè)務(wù)邏輯的控制。
本課時講解控制器中如何處理傳入的參數(shù)和相應(yīng),并且介紹在請求和相應(yīng)的過程中,如何處理請求參數(shù),使用 sesson,設(shè)置 etag 緩存和使用 csrf 確保數(shù)據(jù)來源安全。
Action Pack 是 Rails 種又一個核心的 Gem,它可以處理 web 請求,使用 routes 中定義的規(guī)則調(diào)用控制器(Controller)及方法(Action),并且自動判斷請求類型,做出對應(yīng)的相應(yīng)。
Rails 中的控制器,指的就是處理請求及做出相應(yīng)。
ActionDispatch::Request 類是對 web 請求的包裝類,它有兩個常用的方法:
request.headers["Content-Type"] # => "text/plain"
headers 包含了請求的頭信息。
request.parameters
它會返回請求的參數(shù),不過我們并不直接使用它,而是使用 params 方法獲得,這在稍后介紹。
Request 類的源代碼在這里。
ActionDispatch::Response 類代表了響應(yīng)結(jié)果,它也有常用的方法,不過我們更經(jīng)常用的是 Controller 中的 action 和回調(diào)。在一些測試代碼中,我們經(jīng)常使用 response 實(shí)例。
比如,我們測試商品刪除之后,會返回到商品列表,我們的測試代碼是:
RSpec.describe ProductsController, type: :controller do
...
describe "DELETE #destroy" do
it "redirects to the products list" do
product = Product.create! valid_attributes
delete :destroy, {:id => product.to_param}, valid_session
expect(response).to redirect_to(products_url)
end
end
end
Request 和 Response 在我們的業(yè)務(wù)邏輯代碼中并不不常用到,下面介紹的內(nèi)容,是我們在編寫控制器代碼時,經(jīng)常遇到的。
Controller 是控制器的概念,所謂控制,指在網(wǎng)絡(luò)傳輸中,接收參數(shù)和做出相應(yīng)。Controller 有兩種方式接收參數(shù):GET 和 POST。兩種方式均可通過 params 讀取傳遞的內(nèi)容。
在 Rails3之前的版本中,當(dāng)接收傳遞的參數(shù),用來更新資源屬性時,可以設(shè)定 Model 的屬性白名單,非報名單上的屬性不允許通過參數(shù)傳遞的方式修改,比如:
class User < ActiveRecord::Base
attr_accessible :name
end
在 Rails 4 之后,這個方法轉(zhuǎn)為 gem,不再是 Rails 4 的核心功能,但將在 Rails 5 中重新回到核心功能中?,F(xiàn)在,使用 permit 方法來過濾參數(shù)。使用 scaffold 創(chuàng)建的 Controller 默認(rèn)使用了該方法:
class ProductsController < ApplicationController
def create
@product = Product.new(product_params)
...
private
def product_params
params.require(:product).permit(:name, :price, :description)
end
end
permit 可以設(shè)定關(guān)聯(lián)關(guān)系的屬性:
params.require(:product).permit(:name, :price, :description, variants_attributes: [:price, :size, :id, :_destroy])
:id 和 :_destroy 適用于上一章介紹的 accepts_nested_attributes_for 方法。
Controller 響應(yīng)請求有多種結(jié)果,響應(yīng)返回 Status Code,常見的有 200(成功響應(yīng)),302(跳轉(zhuǎn)),404(未找到資源),500(內(nèi)部錯誤)。更多響應(yīng) Code 參考 3.3 視圖中的 AJAX 交互。
一個 controller 的 action 對應(yīng)一個請求,這樣可以保持我們業(yè)務(wù)邏輯代碼清晰,易維護(hù)。一個 action 可以響應(yīng)一個請求的多中類型,這在我們第三章里已經(jīng)有了介紹和演示。
Controller 使用 respond_to 方法,針對每一種請求類型,做出響應(yīng):
respond_to do |format|
if @product.save
format.html { redirect_to @product, notice: 'Product was successfully created.' }
format.json { render :show, status: :created, location: @product }
else
format.html { render :new }
format.json { render json: @product.errors, status: :unprocessable_entity }
end
format.js
end
當(dāng)我們處理多個資源時,每個資源的 create 和 update 等資源方法,大多都具備相同的邏輯代碼。除了特定的業(yè)務(wù)邏輯,他們都會響應(yīng)典型的資源操作。 Rails 4.2 之前提供了 respond_with 訪問,4.2 之后將它轉(zhuǎn)為一個 gem,我們安裝這個 gem:
gem "responders"
并且創(chuàng)建文件:
% rails g responders:install
create lib/application_responder.rb
insert config/application.rb
prepend app/controllers/application_controller.rb
insert app/controllers/application_controller.rb
create config/locales/responders.en.yml
默認(rèn),它只支持 :html,因?yàn)槲覀冄菔緯r,又使用到了 :json 和 :js,還有 :xml,我們將這些類型添加上:
class ApplicationController < ActionController::Base
self.responder = ApplicationResponder
respond_to :html, :xml, :json, :js
我們將剛才 respond_to 方法改成 respond_with,精簡重復(fù)的代碼(Dry up your code):
def create
@product = Product.create(product_params)
respond_with(@product)
end
在 6.4 I18n 中,我們講 I18n 文件做了整理,這里我們把 generator 創(chuàng)建的語言包,按照 6.4 一節(jié)中介紹的方式進(jìn)行管理,并且增加中文提示。如此,我們不必為每個資源創(chuàng)建、修改等操作各自編寫語言提示了。
從一個請求到另一個請求,Rails 使用 Session 來保存一些簡單的信息,比如 user_id 等。同時,也可以用 cookies 保存該信息。
當(dāng) Rails 項(xiàng)目創(chuàng)建的時候,它會有一個默認(rèn)的 cookie name,這在 config/initializers/session_store.rb 中:
Rails.application.config.session_store :cookie_store, key: '_rails-practice_session'
這里,我們用 cookie_store 來儲存 session,當(dāng)我們在項(xiàng)目中保存 session 的時候,數(shù)據(jù)會保存在這個 cookie 中。
http://wiki.jikexueyuan.com/project/rails-practice/images/chapter_5/1.png" alt="" />
在 Rails 2 之前,可以 decode 這個內(nèi)容,查看其中 session 的內(nèi)容:
require 'rack'
cookie = "WmQyNFliZnprd3..."
Rack::Session::Cookie::Base64::Marshal.new.decode(cookie)
=> {"session_id"=>"d3b17...", "user_id"=>"123", "_csrf_token"=>"rtkofT..."}
因?yàn)樵?Rails 3 中已經(jīng)增加了 secret_key_base,所以無法直接 decode 內(nèi)容了。
但是,如果單獨(dú)使用一個 cookie 來記錄數(shù)據(jù),默認(rèn)是不經(jīng)過任何加密的:
cookies[:name] = "Rails"
http://wiki.jikexueyuan.com/project/rails-practice/images/chapter_5/2.png" alt="" />
如果這個數(shù)據(jù)不想被暴露,需要單獨(dú)加密:
cookies.signed[:name] = "Rails"
cookies.permanent.signed[:name] = "Rails" [1]
permanent 會讓這個 cookie 有20年的有效時間。
Cookie 的 api 文檔在這里。
如果我們在 Cookie 中保存了過多數(shù)據(jù),會超出 cookie 的大小限制,這時我們可以更改 session 的保存方式,比如使用 redis,memcached 等。
Rails.application.config.session_store :redis_store, servers: {
host: "127.0.0.1",
port: 6379,
namespace: "store_session"}
在 6.2 緩存 中有其他詳細(xì)的介紹。
Controller 響應(yīng)的時候,header 中會包含 etag 屬性,根據(jù)這個屬性,瀏覽器會判斷該內(nèi)容是否修改。
headers['ETag'] = Digest::MD5.hexdigest(body)
但對 Rails 的布局和模板而言,經(jīng)常包含變動的內(nèi)容,比如登錄后會顯示用戶名稱,未登錄顯示登錄連接。 并且,body 可能會很大,md5 時間長。
我們可以針對資源,單獨(dú)增加 etag:
def show
fresh_when([@product, current_user.try(:id)])
end
也可以將它精簡:
class ProductsController < ApplicationController
etag { current_user.try(:id) }
...
def show
fresh_when(@product)
end
如果我們僅提供數(shù)據(jù),比如 api,可以去掉模板:
fresh_when @product, template: false
在 Controller 接收請求數(shù)據(jù)的時候,安全機(jī)制會處理跨站請求偽造(cross-site request forgery,簡稱 CSRF)。在我們的布局(layout)頁面,你可能已經(jīng)看到這樣一個輔助方法:
<%= csrf_meta_tags %>
打開頁面的源碼,我們可以看到:
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="O3Li25wJK0buXKRQRX4CzpAWheQIQ4VknCPe3KwNIFkIuUsbBApxl2jVVTd9IcmzR8oHLZI0qZpO39aLdNaBAQ==" />
當(dāng)我使用表單的輔助方法 form_for 和 form_tag 時,表單會自動創(chuàng)建一個隱藏控件
<input type="hidden" name="authenticity_token" value="GI5YwKDhQA4pMlLRaUlpHugYdL5ygNe3Co6TL8PvZDsrRfEAOOIa36+7o7ZRFqJjP8T2d+j3+0nYcpt4GzTFYw==">
當(dāng)我們使用 remote: true 時,這個控件又消失了,這樣是不是不安全?不,ujs 在提交的時候,為我們自動補(bǔ)充上了 authenticity_token 參數(shù)。
更多 Rails 安全問題,可以參考這里 http://guides.ruby-china.org/security.html。
注:
感謝 Rails 4 - Zombie Outlaws,本節(jié) 3,5 的內(nèi)容靈感來自。