# 7.3 注冊失敗
雖然上一節大概介紹了[圖 7.12](#fig-signup-form) 中表單的 HTML 結構(參見[代碼清單 7.15](#listing-signup-form-html)),但并沒涉及什么細節,其實注冊失敗時才能更好地理解這個表單的作用。本節,我們會在注冊表單中填寫一些無效的數據,提交表單后,頁面不會轉向其他頁面,而是返回“注冊”頁面,顯示一些錯誤消息,如[圖 7.14](#fig-signup-failure-mockup) 中的構思圖所示。
圖 7.14:注冊失敗時顯示的頁面構思圖
## 7.3.1 可正常使用的表單
回顧一下 [7.1.2 節](#a-users-resource)的內容,在 `routes.rb` 文件中設置 `resources :users` 之后([代碼清單 7.3](#listing-users-resource)),Rails 應用就可以響應[表 7.1](#table-restful-users)中符合 REST 架構的 URL 了。其中,發送到 /users 地址上的 `POST` 請求由 `create` 動作處理。在 `create` 動作中,我們可以調用 `User.new` 方法,使用提交的數據創建一個新用戶對象,嘗試存入數據庫,失敗后再重新渲染“注冊”頁面,讓訪客重新填寫注冊信息。我們先來看一下生成的 `form` 元素:
```
<form action="/users" class="new_user" id="new_user" method="post">
```
[7.2.2 節](#signup-form-html)說過,這個表單會向 /users 地址發送 `POST` 請求。
為了讓這個表單可用,首先我們要添加[代碼清單 7.16](#listing-first-create-action) 中的代碼。這段代碼再次用到了 `render` 方法,上一次是在局部視圖中([5.1.3 節](chapter5.html#partials)),不過如你所見,在控制器的動作中也可以使用 `render` 方法。同時,我們在這段代碼中介紹了 `if-else` 分支結構的用法:根據 `@user.save` 的返回值,分別處理用戶存儲成功和失敗兩種情況([6.1.3 節](chapter6.html#creating-user-objects)介紹過,存儲成功時返回值為 `true`,失敗時返回值為 `false`)。
##### 代碼清單 7.16:能處理注冊失敗的 `create` 動作
app/controllers/users_controller.rb
```
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(params[:user]) # 不是最終的實現方式 if @user.save
# 處理注冊成功的情況
else
render 'new'
end
end
end
```
留意上述代碼中的注釋——這不是最終的實現方式,但現在完全夠用。最終版會在 [7.3.2 節](#strong-parameters)實現。
我們要實際操作一下,提交一些無效的注冊數據,這樣才能更好地理解[代碼清單 7.16](#listing-first-create-action) 中代碼的作用,結果如[圖 7.15](#fig-signup-failure) 所示,底部完整的調試信息如[圖 7.16](#fig-signup-failure-rails-debug) 所示。([圖 7.15](#fig-signup-failure) 中還顯示了 Web 控制臺,這是個 Rails 控制臺,只不過顯示在瀏覽器中,用來協助調試。我們可以在其中查看用戶模型,不過這里我們想審查 `params`,可是在 Web 控制臺中無法獲取。)
圖 7.15:注冊失敗圖 7.16:注冊失敗時顯示的調試信息
下面我們來分析一下調試信息中請求參數哈希的 `user` 部分([圖 7.16](#fig-signup-failure-rails-debug)),以便深入理解 Rails 處理表單的過程:
```
"user" => { "name" => "Foo Bar",
"email" => "foo@invalid",
"password" => "[FILTERED]",
"password_confirmation" => "[FILTERED]"
}
```
這個哈希是 `params` 的一部分,會傳給用戶控制器。[7.1.2 節](#a-users-resource)說過,`params` 哈希中包含每次請求的信息,例如向 /users/1 發送請求時,`params[:id]` 的值是用戶的 ID,即 1。提交表單發送 `POST` 請求時,`params` 是一個嵌套哈希。嵌套哈希在 [4.3.3 節](chapter4.html#hashes-and-symbols)中使用控制臺介紹 `params` 時用過。上面的調試信息說明,提交表單后,Rails 會構建一個名為 `user` 的哈希,哈希中的鍵是 `input` 標簽的 `name` 屬性值([代碼清單 7.13](#listing-signup-form)),鍵對應的值是用戶在字段中填寫的內容。例如:
```
<input id="user_email" name="user[email]" type="email" />
```
`name` 屬性的值是 `user[email]`,表示 `user` 哈希中的 `email` 元素。
雖然調試信息中的鍵是字符串形式,不過卻以符號形式傳給用戶控制器。`params[:user]` 這個嵌套哈希實際上就是 `User.new` 方法創建用戶所需的參數。我們在 [4.4.5 節](chapter4.html#a-user-class)介紹過 `User.new` 的用法,[代碼清單 7.16](#listing-first-create-action) 也用到了。也就是說,如下代碼:
```
@user = User.new(params[:user])
```
基本上等同于
```
@user = User.new(name: "Foo Bar", email: "foo@invalid",
password: "foo", password_confirmation: "bar")
```
在舊版 Rails 中,使用
```
@user = User.new(params[:user])
```
就行了,但默認情況下這種用法并不安全,需要謹慎處理,避免惡意用戶篡改應用的數據庫。在 Rails 4.0 之后的版本中,這行代碼會拋出異常(如[圖 7.15](#fig-signup-failure) 和[圖 7.16](#fig-signup-failure-rails-debug) 所示),增強了安全。
## 7.3.2 健壯參數
我們在 [4.4.5 節](chapter4.html#a-user-class)提到過“批量賦值”——使用一個哈希初始化 Ruby 變量,如下所示:
```
@user = User.new(params[:user]) # 不是最終的實現方法
```
上述代碼中的注釋[代碼清單 7.16](#listing-first-create-action) 中也有,說明這不是最終的實現方式。因為初始化整個 `params` 哈希十分危險,會把用戶提交的所有數據傳給 `User.new` 方法。假設除了前述的屬性,用戶模型中還有一個 `admin` 屬性,用來標識網站的管理員。(我們會在 [9.4.1 節](chapter9.html#administrative-users)加入這個屬性。)如果想把這個屬性設為 `true`,要在 `params[:user]` 中包含 `admin='1'`。這個操作可以使用 `curl` 等命令行 HTTP 客戶端輕易實現。如果把整個 `params` 哈希傳給 `User.new`,那么網站中的任何用戶都可以在請求中包含 `admin='1'` 來獲取管理員權限。
舊版 Rails 使用模型中的 `attr_accessible` 方法解決這個問題,在一些早期的 Rails 應用中可能還會看到這種用法。但是,從 Rails 4.0 起,推薦在控制器層使用一種叫做“健壯參數”(strong parameter)的技術。這個技術可以指定需要哪些請求參數,以及允許傳入哪些請求參數。而且,如果按照上面的方式傳入整個 `params` 哈希,應用會拋出異常。所以,現在默認情況下,Rails 應用已經堵住了批量賦值漏洞。
本例,我們需要 `params` 哈希包含 `:user` 元素,而且只允許傳入 `name`、`email`、`password` 和 `password_confirmation` 屬性。我們可以使用下面的代碼實現:
```
params.require(:user).permit(:name, :email, :password, :password_confirmation)
```
這行代碼會返回一個 `params` 哈希,只包含允許使用的屬性。而且,如果沒有指定 `:user` 元素還會拋出異常。
為了使用方便,可以定義一個名為 `user_params` 的方法,換掉 `params[:user]`,返回初始化所需的哈希:
```
@user = User.new(user_params)
```
`user_params` 方法只會在用戶控制器內部使用,不需要開放給外部用戶,所以我們可以使用 Ruby 中的 `private` 關鍵字[[9](#fn-9)]把這個方法的作用域設為“私有”,如[代碼清單 7.17](#listing-create-action-strong-parameters) 所示。(我們會在 [8.4 節](chapter8.html#remember-me)詳細介紹 `private`。)
##### 代碼清單 7.17:在 `create` 動作中使用健壯參數
app/controller/users_controller.rb
```
class UsersController < ApplicationController
.
.
.
def create
@user = User.new(user_params) if @user.save
# 處理注冊成功的情況
else
render 'new'
end
end
private
def user_params
params.require(:user).permit(:name, :email, :password, :password_confirmation) end
end
```
順便說一下,`private` 后面的 `user_params` 方法多了一層縮進,目的是為了從視覺上容易辨認哪些是私有方法。(經驗證明,這么做很明智。如果一個類中有很多方法,容易不小心把方法定義為“私有”,在相應的對象上無法調用時會覺得非常奇怪。)
現在,注冊表單可以使用了,至少提交后不會顯示錯誤了。但是,如[圖 7.17](#fig-invalid-submission-no-feedback),提交無效數據后,(除了只在開發環境中顯示的調試信息之外)表單沒有顯示任何反饋信息,容易讓人誤解。而且也沒真正創建一個新用戶。第一個問題在 [7.3.3 節](#signup-error-messages)解決,第二個問題在 [7.4 節](#successful-signups)解決。
圖 7.17:提交無效信息后顯示的注冊表單
## 7.3.3 注冊失敗錯誤消息
處理注冊失敗的最后一步,要加入有用的錯誤消息,說明注冊失敗的原因。默認情況下,Rails 基于用戶模型的驗證,提供了這種消息。假設我們使用無效的電子郵件地址和長度較短的密碼創建用戶:
```
$ rails console
>> user = User.new(name: "Foo Bar", email: "foo@invalid",
?> password: "dude", password_confirmation: "dude")
>> user.save
=> false
>> user.errors.full_messages
=> ["Email is invalid", "Password is too short (minimum is 6 characters)"]
```
如上所示,`errors.full_message` 對象是一個由錯誤消息組成的數組([6.2.2 節](chapter6.html#validating-presence)簡介過)。
和上面的控制臺會話類似,在[代碼清單 7.16](#listing-first-create-action) 中,保存失敗時也會生成一組和 `@user` 對象相關的錯誤消息。如果想在瀏覽器中顯示這些錯誤消息,我們要在 `new` 視圖中渲染一個錯誤消息局部視圖,并把表單中每個輸入框的 CSS 類設為 `form-control`(在 Bootstrap 中有特殊意義),如[代碼清單 7.18](#listing-f-error-messages) 所示。注意,這個錯誤消息局部視圖只是臨時的,最終版會在 [11.3.2 節](chapter11.html#creating-microposts)實現。
##### 代碼清單 7.18:在注冊表單中顯示錯誤消息
app/views/users/new.html.erb
```
<% provide(:title, 'Sign up') %>
<h1>Sign up</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
<%= form_for(@user) do |f| %>
<%= render 'shared/error_messages' %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
<%= f.label :password %>
<%= f.password_field :password, class: 'form-control' %>
<%= f.label :password_confirmation, "Confirmation" %>
<%= f.password_field :password_confirmation, class: 'form-control' %>
<%= f.submit "Create my account", class: "btn btn-primary" %>
<% end %>
</div>
</div>
```
注意,在上面的代碼中,渲染的局部視圖名為 `shared/error_messages`,這里用到了 Rails 的一個約定:如果局部視圖要在多個控制器中使用([9.1.1 節](chapter9.html#edit-form)),則把它存放在專門的 `shared/` 文件夾中。所以我們要使用 `mkdir`([表 1.1](chapter1.html#table-unix-commands))新建 `app/views/shared` 文件夾:
```
$ mkdir app/views/shared
```
然后像之前一樣,在文本編輯器中新建局部視圖 `_error_messages.html.erb` 文件。這個局部視圖的內容如[代碼清單 7.19](#listing-errors-partial) 所示。
##### 代碼清單 7.19:顯示表單錯誤消息的局部視圖
app/views/shared/_error_messages.html.erb
```
<% if @user.errors.any? %>
<div id="error_explanation">
<div class="alert alert-danger">
The form contains <%= pluralize(@user.errors.count, "error") %>.
</div>
<ul>
<% @user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
```
這個局部視圖的代碼使用了幾個之前沒用過的 Rails/Ruby 結構,還有 Rails 錯誤對象上的兩個新方法。第一個新方法是 `count`,它的返回值是錯誤的數量:
```
>> user.errors.count
=> 2
```
第二個新方法是 `any?`,它和 `empty?` 的作用相反:
```
>> user.errors.empty?
=> false
>> user.errors.any?
=> true
```
第一次使用 `empty?` 方法是在 [4.2.3 節](chapter4.html#objects-and-message-passing),用在字符串上;從上面的代碼可以看出,`empty?` 也可用在 Rails 錯誤對象上,如果錯誤對象為空返回 `true`,否則返回 `false`。`any?` 方法就是取反 `empty?` 的返回值,如果對象中有內容就返回 `true`,沒內容則返回 `false`。(順便說一下,`count`、`empty?` 和 `any?` 都可以用在 Ruby 數組上,[11.2 節](chapter11.html#showing-microposts)會好好利用這三個方法。)
還有一個比較新的方法是 `pluralize`,在控制臺中默認不可用,不過我們可以引入 `ActionView::Helpers::TextHelper` 模塊,加載這個方法:[[10](#fn-10)]
```
>> include ActionView::Helpers::TextHelper
>> pluralize(1, "error")
=> "1 error"
>> pluralize(5, "error")
=> "5 errors"
```
如上所示,`pluralize` 方法的第一個參數是整數,返回值是這個數字和第二個參數組合在一起后,正確的單復數形式。`pluralize` 方法由功能強大的“轉置器”(inflector)實現,轉置器知道怎么處理大多數單詞的單復數變換,甚至很多不規則的變換方式:
```
>> pluralize(2, "woman")
=> "2 women"
>> pluralize(3, "erratum")
=> "3 errata"
```
所以,使用 `pluralize` 方法后,如下的代碼:
```
<%= pluralize(@user.errors.count, "error") %>
```
返回值是 `"0 errors"`、`"1 error"` 或 `"2 errors"` 等,單復數形式取決于錯誤的數量。這樣可以避免出現類似 `"1 errors"` 這種低級的錯誤(這是網絡中常見的錯誤之一)。
注意,[代碼清單 7.19](#listing-errors-partial) 還添加了一個 CSS ID,`error_explanation`,可用來樣式化錯誤消息。([5.1.2 節](chapter5.html#bootstrap-and-custom-css)介紹過,CSS 中以 `#` 開頭的規則是用來給 ID 添加樣式的。)出錯時,Rails 還會自動把有錯誤的字段包含在一個 CSS 類為 `field_with_errors` 的 `div` 元素中。我們可以利用這些 ID 和類為錯誤消息添加樣式,所需的 SCSS 如[代碼清單 7.20](#listing-error-messages-css) 所示。在這段代碼中,使用 Sass 的 `@extend` 函數引入了 Bootstrap 中的 `has-error` 類。
##### 代碼清單 7.20:錯誤消息的樣式
app/assets/stylesheets/custom.css.scss
```
.
.
.
/* forms */
.
.
.
#error_explanation {
color: red;
ul {
color: red;
margin: 0 0 30px 0;
}
}
.field_with_errors {
@extend .has-error;
.form-control {
color: $state-danger-text;
}
}
```
添加[代碼清單 7.18](#listing-f-error-messages) 和[代碼清單 7.19](#listing-errors-partial) 中的代碼,以及[代碼清單 7.20](#listing-error-messages-css) 中的 SCSS 之后,提交無效的注冊信息后,會顯示一些有用的錯誤消息,如[圖 7.18](#fig-signup-error-messages) 所示。因為錯誤消息是由模型驗證生成的,所以如果以后修改了驗證規則,例如電子郵件地址的格式,或者密碼的最短長度,錯誤消息會自動變化。(注意,因為我們添加了存在性驗證,而且 `has_secure_password` 方法會驗證是否有密碼(密碼是否為 `nil`),所以,如果用戶沒有輸入密碼,目前會出現重復的錯誤消息。我們可以直接處理錯誤消息,去掉重復的消息,不過,[9.1.4 節](chapter9.html#successful-edits)添加 `allow_nil: true` 之后,會自動解決這個問題。)
圖 7.18:注冊失敗后顯示的錯誤消息
## 7.3.4 注冊失敗的測試
在沒有完全支持測試的強大 Web 框架出現以前,開發者不得不自己動手測試表單。例如,為了測試注冊頁面,我們要在瀏覽器中訪問這個頁面,然后分別提交無效和有效的注冊信息,檢查各種情況下應用的表現是否正常。而且,每次修改應用后都要重復這個痛苦又容易出錯的過程。
幸好,使用 Rails 可以編寫測試,自動測試表單。這一節,我們要編寫測試,確認在表單中提交無效的數據時表現正確。[7.4.4 節](#a-test-for-valid-submission)會編寫提交有效數據時的測試。
首先,我們要為用戶注冊功能生成一個集成測試文件,這個文件名為 `users_signup`(沿用使用復數命名資源名的約定):
```
$ rails generate integration_test users_signup
invoke test_unit
create test/integration/users_signup_test.rb
```
([7.4.4 節](#a-test-for-valid-submission)測試注冊成功時也使用這個文件。)
測試的主要目的是,確認點擊注冊按鈕提交無效數據后,不會創建新用戶。(對錯誤消息的測試留作[7.7 節](#sign-up-exercises)。)方法是檢測用戶的數量。測試會使用每個 Active Record 類(包括 `User` 類)都能使用的 `count` 方法:
```
$ rails console
>> User.count
=> 0
```
現在 `User.count` 的返回值是 `0`,因為我們在 [7.2 節](#signup-form)開頭還原了數據庫。和 [5.3.4 節](chapter5.html#layout-link-tests)一樣,我們要使用 `assert_select` 測試相應頁面中的 HTML 元素。注意,只能測試以后基本不會修改的元素。
首先,我們使用 `get` 方法訪問注冊頁面:
```
get signup_path
```
為了測試表單提交后的狀態,我們要向 `users_path` 發起 `POST` 請求([表 7.1](#table-restful-users))。這個操作可以使用 `post` 方法完成:
```
assert_no_difference 'User.count' do
post users_path, user: { name: "",
email: "user@invalid",
password: "foo",
password_confirmation: "bar" }
end
```
這里用到了 `create` 動作中傳給 `User.new` 的 `params[:user]` 哈希([代碼清單 7.24](#listing-signup-flash))。我們把 `post` 方法放在 `assert_no_difference` 方法的塊中,并把 `assert_no_difference` 方法的參數設為字符串 `'User.count'`。執行這段代碼時,會比較塊中的代碼執行前后 `User.count` 的值。這段代碼相當于先記錄用戶數量,然后在 `post` 請求中發送數據,再確認用戶的數量沒變,如下所示:
```
before_count = User.count
post users_path, ...
after_count = User.count
assert_equal before_count, after_count
```
雖然這兩種方式的作用相同,但使用 `assert_no_difference` 更簡潔,而且更符合 Ruby 的習慣用法。
把上述代碼放在一起,寫出的測試如[代碼清單 7.21](#listing-a-test-for-invalid-submission) 所示。在測試中,我們還調用了 `assert_template` 方法,檢查提交失敗后是否會重新渲染 `new` 動作。檢查錯誤消息的測試留作練習,參見 [7.7 節](#sign-up-exercises)。
##### 代碼清單 7.21:注冊失敗的測試 GREEN
test/integration/users_signup_test.rb
```
require 'test_helper'
class UsersSignupTest < ActionDispatch::IntegrationTest
test "invalid signup information" do
get signup_path
assert_no_difference 'User.count' do
post users_path, user: { name: "",
email: "user@invalid",
password: "foo",
password_confirmation: "bar" }
end
assert_template 'users/new'
end
end
```
因為在編寫集成測試之前已經寫好了應用代碼,所以測試組件應該能通過:
##### 代碼清單 7.22:**GREEN**
```
$ bundle exec rake test
```
- Ruby on Rails 教程
- 致中國讀者
- 序
- 致謝
- 作者譯者簡介
- 版權和代碼授權協議
- 第 1 章 從零開始,完成一次部署
- 1.1 簡介
- 1.2 搭建環境
- 1.3 第一個應用
- 1.4 使用 Git 做版本控制
- 1.5 部署
- 1.6 小結
- 1.7 練習
- 第 2 章 玩具應用
- 2.1 規劃應用
- 2.2 用戶資源
- 2.3 微博資源
- 2.4 小結
- 2.5 練習
- 第 3 章 基本靜態的頁面
- 3.1 創建演示應用
- 3.2 靜態頁面
- 3.3 開始測試
- 3.4 有點動態內容的頁面
- 3.5 小結
- 3.6 練習
- 3.7 高級測試技術
- 第 4 章 Rails 背后的 Ruby
- 4.1 導言
- 4.2 字符串和方法
- 4.3 其他數據類型
- 4.4 Ruby 類
- 4.5 小結
- 4.6 練習
- 第 5 章 完善布局
- 5.1 添加一些結構
- 5.2 Sass 和 Asset Pipeline
- 5.3 布局中的鏈接
- 5.4 用戶注冊:第一步
- 5.5 小結
- 5.6 練習
- 第 6 章 用戶模型
- 6.1 用戶模型
- 6.2 用戶數據驗證
- 6.3 添加安全密碼
- 6.4 小結
- 6.5 練習
- 第 7 章 注冊
- 7.1 顯示用戶的信息
- 7.2 注冊表單
- 7.3 注冊失敗
- 7.4 注冊成功
- 7.5 專業部署方案
- 7.6 小結
- 7.7 練習
- 第 8 章 登錄和退出
- 8.1 會話
- 8.2 登錄
- 8.3 退出
- 8.4 記住我
- 8.5 小結
- 8.6 練習
- 第 9 章 更新,顯示和刪除用戶
- 9.1 更新用戶
- 9.2 權限系統
- 9.3 列出所有用戶
- 9.4 刪除用戶
- 9.5 小結
- 9.6 練習
- 第 10 章 賬戶激活和密碼重設
- 10.1 賬戶激活
- 10.2 密碼重設
- 10.3 在生產環境中發送郵件
- 10.4 小結
- 10.5 練習
- 10.6 證明超時失效的比較算式
- 第 11 章 用戶的微博
- 11.1 微博模型
- 11.2 顯示微博
- 11.3 微博相關的操作
- 11.4 微博中的圖片
- 11.5 小結
- 11.6 練習
- 第 12 章 關注用戶
- 12.1 “關系”模型
- 12.2 關注用戶的網頁界面
- 12.3 動態流
- 12.4 小結
- 12.5 練習