# 7.3 注册失败
虽然上一节大概介绍了[图 7.12](#fig-signup-form) 中表单的 HTML 结构(参见[代码清单 7.15](#listing-signup-form-html)),但并没涉及什么细节,其实注册失败时才能更好地理解这个表单的作用。本节,我们会在注册表单中填写一些无效的数据,提交表单后,页面不会转向其他页面,而是返回“注册”页面,显示一些错误消息,如[图 7.14](#fig-signup-failure-mockup) 中的构思图所示。
![signup failure mockup bootstrap](https://box.kancloud.cn/2016-05-11_5732bd14c7b0a.png)图 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 控制台中无法获取。)
![signup failure 3rd edition](https://box.kancloud.cn/2016-05-11_5732bd14dc4c7.png)图 7.15:注册失败![signup failure debug 3rd edition](https://box.kancloud.cn/2016-05-11_5732bd15054b5.png)图 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)解决。
![invalid submission no feedback](https://box.kancloud.cn/2016-05-11_5732bd15257db.png)图 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` 之后,会自动解决这个问题。)
![signup error messages 3rd edition](https://box.kancloud.cn/2016-05-11_5732bd1540109.png)图 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 练习