Ruby Rails学习中:注册表单,注册失败,注册成功

时间:2023-03-08 20:44:22

接上篇

一. 注册表单

用户资料页面已经可以访问了, 但内容还不完整。下面我们要为网站创建一个注册表单。

1.使用 form_for

注册页面的核心是一个表单, 用于提交注册相关的信息(名字、电子邮件地址、密码和确认密码)。在 Rails中, 创建表单可以使用 form_for 辅助方法, 传入 Active Record 对象后, 使用该对象的属性构建一个表单。

回顾一下: 注册页面的地址是 /signup, 由 Users 控制器的 new 动作处理。首先, 我们要创建传给 form_for 的用户对象, 然后赋值给 @user 变量, 如下图所示:

(1).在 new 动作中添加 @user 变量

打开文件:app/controllers/users_controller.rb

Ruby Rails学习中:注册表单,注册失败,注册成功

然后添加个表单:

(2).用户注册表单

打开文件:app/views/users/new.html.erb

Ruby Rails学习中:注册表单,注册失败,注册成功

(3).注册表单的样式

打开文件:app/assets/stylesheets/custom.scss

Ruby Rails学习中:注册表单,注册失败,注册成功

最后出来的样子,是这样的:

Ruby Rails学习中:注册表单,注册失败,注册成功

2.注册表单的 HTML

为了更好的理解我们刚才定义的表单, 我们将其分成几段来看。先看外层结构——开头的 form_for方法和结尾的 end :

Ruby Rails学习中:注册表单,注册失败,注册成功

这段代码中有关键字 do , 说明 form_for 方法可以接受一个块, 而且有一个块变量 f (代表“form”)。
我们一般无需了解 Rails 辅助方法的内部实现, 但是对 form_for 来说, 我们要知道 f 对象的作用是什么: 在这个对象上调用表单字段(例如, 文本字段、单选按钮或密码字段)对应的方法, 生成的字段元素可以用来设定 @user 对象的属性。也就是说:

Ruby Rails学习中:注册表单,注册失败,注册成功

生成的 HTML 是一个有标注(label)的文本字段,用于设定 User 模型的 name 属性。

二. 注册失败

看标题就知道了, 现在咱们要做一个如果数据填写错误, 提交失败之后所返回的错误页面。

1.可正常使用的表单

为了让这个表单可用, 首先我们要添加代码清单 7.18 中的代码。这段代码再次用到了 render 方法, 上一次是用在了局部视图中, 不过如你所见, 在控制器的动作中也可以使用 render 方法。同时, 我们在这段代码中介绍了 if-else 分支结构的用法: 根据 @user.save 的返回值, 分别处理用户存储成功和失败两种情
况(前面也说过了, 存储成功时返回值为 true , 失败时返回值为 false )。

(1).能处理注册失败的 create 动作

打开文件:app/controllers/users_controller.rb

Ruby Rails学习中:注册表单,注册失败,注册成功

注:留意上述代码中的注释, 这不是最终的实现方式, 但现在完全够用, 所以现在先拿来顶替一下, 稍后再进行改写。

我们要实际操作一下, 提交一些无效的注册数据, 这样才能更好地理解上图中代码的作用, 结果下图所示:

Ruby Rails学习中:注册表单,注册失败,注册成功

底部完整的调试信息如下图所示:

Ruby Rails学习中:注册表单,注册失败,注册成功

下面我们来分析一下调试信息中请求参数散列的 user 部分(上图), 以便深入理解 Rails 处理表单的过程:

"user"=>{"name"=>"托儿索",
"email"=>"yasuo@qwertyuiop",
"password"=>"[FILTERED]",
"password_confirmation"=>"[FILTERED]"
}
这个散列是 params 的一部分,会传给 Users 控制器。前面曾说过, params 散列中包含每次请求的信息,例如向 /users/1 发送请求时, params[:id] 的值是用户的 ID,即 1。提交表单发送 POST 请求时, params 是一个嵌套散列。嵌套散列在前面使用控制台介绍 params 时用过。上面的调试信息说明,提交表单后, Rails 会构建一个名为 user 的散列,散列中的键是 input 标签 name 属性的值,键对应的值是用户在字段中填写的内容。例如:
<input id="user_email" name="user[email]" type="email" />
其中, name 属性的值是 user[email] ,对应于 user 散列中的 email 键。虽然调试信息中的键是字符串形式,不过却以符号形式传给 Users 控制器。 params[:user] 这个嵌套散列实际上就是 User.new 方法创建用户所需的参数。我们在前面曾介绍过 User.new 的用法,上面注册控制器中也用到了。也就是说,下述代码:
@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 之后的版本中,这行代码会抛出异常(如上面抛错的两图),增强了安全。

2.健壮参数

我们在前面曾提到过批量赋值,即使用一个散列初始化 Ruby 变量,如下所示:
@user = User.new(params[:user])  # 不是最终的实现方式
上述代码中的注释在上面的注册控制器代码中也有,说明这不是最终的实现方式。因为初始化整个 params 散列十分危险,会把用户提交的所有数据传给 User.new 方法。假设除了这几个属性, User 模型中还有一个 admin 属性,用于标识网站的管理员。如果想把这个属性设为 true ,要在 params[:user] 中包含 admin='' 。这个操作可以使用 curl 等命令行 HTTP 客户端轻易实现。如果把整个 params 散列传给 User.new ,那么网站中的任何用户都可以在请求中包含 admin='' 来获取管理员权限。
旧版 Rails 使用模型中的 attr_accessible 方法解决这个问题,在一些早期的 Rails 应用中可能还会看到这种用法。但是,从 Rails 4.0 起,推荐在控制器层使用一种叫做健壮参数(strong parameter)的技术。这个技术可以指定需要哪些请求参数,以及允许传入哪些请求参数。而且,如果按照上面的方式传入整个 params 散列,应用会抛出异常。所以,现在默认情况下,Rails 应用已经堵住了批量赋值漏洞。
下面我们需要 params 散列包含 :user 元素,而且只允许传入 name 、 email 、 password 和 password_confir-mation 属性。这个需求可以使用下面的代码实现:
params.require(:user).permit(:name, :email, :password, :password_confirmation)
这行代码会返回一个 params 散列,只包含允许使用的属性。而且,如果没有指定 :user 元素还会抛出异常。
为了使用方便,可以定义一个名为 user_params 的方法,换掉 params[:user] ,返回初始化所需的散列:
@user = User.new(user_params)
user_params 方法只会在 Users 控制器内部使用,不需要开放给外部用户,所以我们可以使用 Ruby 中的 private 关键字把这个方法的作用域设为“私有”, 如下图所示:

(1).在 create 动作中使用健壮参数

打开文件:app/controller/users_controller.rb

Ruby Rails学习中:注册表单,注册失败,注册成功

注:private 后面的 user_params 方法多了一层缩进, 目的是为了从视觉上易于辨认哪些是私有方法(你不加也没事, 但是以后代码多了之后, 看着混乱别怪我, 那是你自找的)。

然后看页面, 我们提交了无效的数据后, 页面的返回:

Ruby Rails学习中:注册表单,注册失败,注册成功

注:现在, 注册表单可以使用了, 至少提交后不会显示错误了。但是, 如上图所示, 提交无效数据后, (除了只在开发环境中显示的调试信息之外)表单没有显示任何反馈信息, 这容易让人误以为成功注册了, 所以我们继续:

3.注册失败错误消息

处理注册失败的最后一步是加入有用的错误消息, 说明注册失败的原因。默认情况下, Rails 基于 User 模型的验证, 提供了这种消息。假设我们使用无效的电子邮件地址和长度较短的密码创建用户:

Ruby Rails学习中:注册表单,注册失败,注册成功

注:errors.full_message 对象是一个由错误消息组成的数组。

与上面的控制台会话类似, 在前面写好的注册控制器中, 保存失败时也会生成一组和 @user 对象相关的错误消息。如果想在浏览器中显示这些错误消息, 我们要在 new 视图中渲染一个错误消息局部视图, 并把表单中每个输入框的 CSS 类设为 form-control (在 Bootstrap 中有特殊意义), 如下图所示:

(1).在注册表单中显示错误消息

打开文件:app/views/users/new.html.erb

Ruby Rails学习中:注册表单,注册失败,注册成功

注:这个错误消息局部视图只是临时的, 以后总有一天会完善的。

在上面的代码中, 渲染的局部视图名为 'shared/error_messages' , 这里用到了 Rails 的一个约定: 如果局部视图要在多个控制器中使用, 则把它存放在专门的 shared/ 目录中。所以我们要使用 mkdir 新建 app/views/shared 目录:

$ mkdir app/views/shared

然后像之前一样, 在文本编辑器中新建局部视图 _error_messages.html.erb 文件。这个局部视图的内容, 如下图所示:

(2).显示表单错误消息的局部视图

打开文件:app/views/shared/_error_messages.html.erb

Ruby Rails学习中:注册表单,注册失败,注册成功

这个局部视图的代码使用了几个之前没用过的 Rails 和 Ruby 结构,还有 Rails 错误对象上的两个新方法。第一个新方法是 count ,它的返回值是错误的数量:

Ruby Rails学习中:注册表单,注册失败,注册成功

第二个新方法是 any? ,它和 empty? 的作用相反。

从上面的代码可以看出, empty? 也可用在 Rails 错误对象上,如果错误对象为空,返回 true ,否则返回 false 。 any? 方法就是取反 empty? 的返回值,如果对象中有内容就返回 true ,没内容则返回 false 。(顺便说一下, count 、 empty? 和 any? 都可以用在 Ruby 数组上)

还有一个比较新的方法是 pluralize , 在控制台中可以通过 helper 对象调用:

Ruby Rails学习中:注册表单,注册失败,注册成功

如上所示, pluralize 方法的第一个参数是整数, 返回值是这个数字和第二个参数组合在一起后正确的单复数形式。 pluralize 方法由功能强大的转置器(inflector)实现, 转置器知道怎么处理大多数单词的单复数变换, 包括很多不规则的变换方式:

Ruby Rails学习中:注册表单,注册失败,注册成功

所以, 使用 pluralize 方法后,如下的代码:

<%= pluralize(@user.errors.count, "error") %>

返回值是 "0 errors" 、 "1 error" 、 "2 errors" , 等等, 单复数形式取决于错误的数量。这样可以避免出现类似 "1 errors" 这种低级的错误(这是网络中常见的错误之一)。

注意, 错误提示的代码中还添加了一个 CSS ID, error_explanation ,用于样式化错误消息。(前面曾说过, CSS 中以 # 开头的规则是用来给 ID 添加样式的。)

Ruby Rails学习中:注册表单,注册失败,注册成功

出错时, Rails 还会自动把有错误的字段包含在一个 CSS 类为 field_with_errors 的 div 元素中。我们可以利用这些 ID 和类为错误消息添加样式, 所需的 SCSS 如下图所示:

(3).错误消息的样式

打开文件:app/assets/stylesheets/custom.scss

Ruby Rails学习中:注册表单,注册失败,注册成功

注:这段代码使用 Sass 的 @extend 函数引入了 Bootstrap 中的 has-error 类。

然后, 信息填写无效的错误页面就有了:

Ruby Rails学习中:注册表单,注册失败,注册成功

4.注册失败的测试

首先, 我们要为用户注册功能生成一个集成测试文件, 这个文件名为 users_signup (沿用复数命名资源的约定):

Ruby Rails学习中:注册表单,注册失败,注册成功

(1).注册失败的测试 GREEN

打开文件:test/integration/users_signup_test.rb

Ruby Rails学习中:注册表单,注册失败,注册成功

首先,我们使用 get 方法访问注册页面:
get signup_path
为了测试表单提交后的状态,我们要向 users_path 发送 POST 请求。这个操作可以使用 post 方法完成:
assert_no_difference 'User.count' do
  post users_path, params: { user: { name: "",
                       email: "user@invalid",
                       password: "foo",
                       password_confirmation: "bar" } }
end
这里用到了 create 动作中传给 User.new 的 params[:user] 散列。(在 Rails 5 之前的版本中, params 隐式传入,而且只会传入 user 散列。Rails 5.0 废弃了这种做法,因此现在建议使用完整的 params散列。)
我们把 post 方法放在 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 的习惯用法。
注意,从技术层面来讲, get 和 post 之间没有关系,向 users_path 发送 POST 请求之前没必要先向 signup_path 发送 GET 请求。不过我喜欢这么做,因为这样能明确表述概念,而且可以再次确认渲染注册表单时没有错误。
综上,写出的测试如上图所示。在测试中,我们还调用了 assert_template 方法,检查提交失败后是否会重新渲染 new 动作。

注册表单提交之前的 URL 是 /signup, 提交之后的 URL 是 /users, 二者不一样的原因是我们为前者定制了具名路由, 而后者使用的是默认的 REST 式路由。添加下面两图中的代码, 去掉二者之间的差异。然后在表单中提交, 确认提交前后的 URL 都是 /signup。

打开文件:config/routes.rb(添加响应 POST 请求的 signup 路由)

Ruby Rails学习中:注册表单,注册失败,注册成功

打开文件:app/views/users/new.html.erb(把表单提交给 /signup)

Ruby Rails学习中:注册表单,注册失败,注册成功

三. 注册成功

如果提交的数据有效, 把用户存入数据库。我们先尝试保存用户, 如果保存成功, 用户的数据会自动存入数据库, 然后在浏览器中重定向, 转向新注册用户的资料页面, 页面中还会显示一个欢迎消息, 构思图如图所示:

1.完整的注册表单

创建用的create 动作是可以有视图, 但是通常是在成功创建资源后重定向到其他页面。这里, 我们按照习惯, 重定向到新注册用户的资料页面, 不过转到根地址也行。为此, 在应用代码中要使用 redirect_to 方法, 如下图所示:

(1).create 动作的代码,处理保存和重定向操作

打开文件:app/controllers/users_controller.rb

Ruby Rails学习中:注册表单,注册失败,注册成功

注:我们写的是: redirect_to @user 不过, 也可以写成: redirect_to user_url(@user) Rails 看到 redirect_to @user 后, 知道我们是想重定向到 user_url(@user) 。

2.闪现消息

有了上图中的代码后, 注册表单已经可以使用了。不过在提交有效数据注册之前, 我们要添加 Web应用中经常使用的一个增强功能: 访问随后的页面时显示一个消息(这里, 我们要显示一个欢迎新用户的消息), 如果再访问其他页面, 或者刷新页面, 这个消息则消失。在 Rails 中,短暂显示一个消息使用闪现消息(flash)实现。按照 Rails 的约定, 操作成功时使用 :success 键表示,如下图所示:

(1).用户注册成功后显示一个闪现消息

打开文件:app/controllers/users_controller.rb

Ruby Rails学习中:注册表单,注册失败,注册成功

把一个消息赋值给 flash 之后, 我们就可以在重定向后的第一个页面中将其显示出来了。我们要遍历 flash , 在网站布局中显示所有相关的消息。你可能还记得前面在控制台中遍历散列那个例子, 当时我故意把变量命名为 flash :

Ruby Rails学习中:注册表单,注册失败,注册成功

按照上述方式,我们可以使用如下的代码在网站的全部页面中显示闪现消息的内容:
<% flash.each do |message_type, message| %>
  <div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>
(这段代码很乱,混用了 HTML 和 ERb,不易阅读。后面咱再它改得好看一些。)
其中,下述 ERb 代码为各种类型的消息指定一个 CSS 类。
alert-<%= message_type %>
因此, :success 消息的类是 alert-success 。( :success 是个符号,ERb 会自动把它转换成字符串 "suc-cess" ,然后再插入模板。)
为不同类型的消息指定不同的 CSS 类,可以为不同类型的消息指定不同的样式。例如,以后还会使用flash[:danger] 显示登录失败消息。(其实,在前面为错误消息区域指定样式时,已经用过 alert-danger 。)Bootstrap 提供的 CSS 支持四种闪现消息样式,分别为 success 、info 、warning 和 danger ,在开发这个演示应用的过程中,我们会找机会全部使用一遍。
消息也会在模板中显示,如下的代码:
flash[:success] = "Welcome to the Sample App!"
得到的完整 HTML 是:
<div class="alert alert-success">Welcome to the Sample App!</div>
把前面的 ERb 代码放入网站的布局中,得到的布局如下所示:

(2).在网站的布局中添加 flash 变量的内容

打开文件:app/views/layouts/application.html.erb

Ruby Rails学习中:注册表单,注册失败,注册成功

3.首次注册成功

Ruby Rails学习中:注册表单,注册失败,注册成功

注:仔细观察上图,会有欢迎登陆的闪现消息,刷新一下页面就没有了。

4.注册成功的测试

在继续之前, 我们要编写测试, 确认提交有效数据后应用的表现正常, 并捕获可能出现的回归。与前面注册失败的测试一样, 我们主要检查数据库中的内容。这一次, 我们要提交有效的数据, 确认创建了一个用户。类似前面使用的

assert_no_difference 'User.count' do
post users_path, ...
end

这里我们要使用对应的 assert_difference 方法:

assert_difference 'User.count', 1 do
post users_path, ...
end

与 assert_no_difference 一样, assert_difference 的第一个参数是字符串 'User.count' , 目的是比较块中的代码执行前后 User.count 的变化。第二个参数可选, 指定变化的数量(这里是 1)。把 assert_difference 加入到对应的文件后, 得到的测试如下图所示:

(1).注册成功的测试 GREEN

Ruby Rails学习中:注册表单,注册失败,注册成功

注:向users_path 发送 POST 请求之后, 我们使用 follow_redirect! 方法跟踪重定向, 渲染 users/show 模板。所以, assert_template 'users/show' 这一行代码就能测试用户资料页面几乎所有的相关功能。这种对应用中重要功能的端到端覆盖展示了集成测试的重大作用

(2).测试检查实现的闪现消息

打开文件:test/integration/users_signup_test.rb

Ruby Rails学习中:注册表单,注册失败,注册成功

(3). 使用 content_tag 编写网站布局中的闪现消息

打开文件:app/views/layouts/application.html.erb

Ruby Rails学习中:注册表单,注册失败,注册成功

四. 做个总结:

• Rails 通过 debug 方法显示一些有用的调试信息;
• Sass 混入定义一组 CSS 规则,可以多次使用;
• Rails 默认提供了三个标准环境:开发环境、测试环境和生产环境;
• 可以通过一组标准的 REST URL 与 Users 资源交互;
• Gravatar 提供了一种简便的方法显示用户的头像;
• form_for 辅助方法用于创建与 Active Record 对象交互的表单;
• 注册失败后渲染注册页面,而且会显示由 Active Record 自动生成的错误消息;
• Rails 提供了 flash 作为显示临时消息的标准方式;
• 注册成功后会在数据库中创建一个用户记录,而且会重定向到用户资料页面,并显示一个欢迎消息;
• 可以使用集成测试检查表单提交的行为,并能捕获回归;

。。。

先到这吧,稍后继续。