2.2 完善第一个Web应用
在这一节里,我们将在上一节使用scaffold生成了支架的基础上,进一步改进和完善这个firstApp应用。下面先为该应用增加两个功能:用户注册和用户登录。下面将依次按照应用中的视图部分、控制器部分和模型部分的顺序,向读者详细讲解如何改进它们。
2.2.1 使用中文响应
在讲解改进这两个模块之前,为了方便读者看这个firstApp应用的效果,我们先将Rails应用默认的英文响应改为中文响应。这很简单,只需在该应用的/app/controllers路径下的application.rb控制器文件中添加如下代码。
# 将set_charset方法定义成一个before过滤器
before_filter :set_charset
# 该方法用于设置字符集
def set_charset
@headers["Content-Type"] = "text/html; charset=gb2312"
@response.headers["Content-Type"] = "text/html; charset=gb2312"
suppress(ActiveRecord::StatementInvalid) do
ActiveRecord::Base.connection.execute 'SET NAMES gb2312'
end
end
上面的代码调用before_filter方法,将set_charset方法定义为一个before过滤器。在application.rb控制器文件中定义的before过滤器,可以使Rails在调用应用中的所有方法前先调用before过滤器定义的方法(这里是set_charset方法)。set_charset方法将字符集设置为gb2312,因而,就能使本应用中的视图以中文显示。
提示 关于界面中文化的更详细介绍,请参考本书13.1节。
2.2.2 改进用户注册
在刚才scaffold为我们生成的支架中,增加用户的代码部分可以改进为用户注册模块。我们可以将/app/views/user路径下的new.rhtml视图文件重命名为register.rhtml视图文件。
提示 RHTML文件就是Rails应用里的视图页面,RHTML页面的作用有点类似于Java EE应用里的JSP页面。
当我们定义了一个RHTML视图文件后,就可以通过浏览器来直接浏览该页面了。因此,如果我们直接在浏览器地址栏中输入http://localhost:3000/user/register,就可以在浏览器中看到如前面2.1.3节中图2.5所示的效果。但这个页面十分简陋和不美观,下面我们来美化它。
为了对应用界面进行简单美化,我们在firstApp应用的/public/stylesheets路径下添加一个message.css文件,该文件中包含了我们自定义的CSS样式。
然后进入/app/views/layouts路径下,我们可以看到默认的user.rhtml视图文件。该文件是/app/views/user文件夹中所有视图文件的装饰页面(即母版)。
提示 装饰器页面的作用类似于Java EE应用里SiteMesh框架的作用,它提供了简单的方法来为整个应用生成统一的页面风格。
修改user.rhtml视图文件,新的代码如下。
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312"/>
<!-- 连接CSS样式文件 -->
<link rel="stylesheet" href="/stylesheets/message.css" type="text/css" media="all"/>
<title>====第一个Web应用的标题====</title>
</head>
<body>
<div id="user-page">
<div id="header-section">
第一个Web应用
</div>
<div id="content-section">
<!-- 调用yield来输出被装饰页面中的内容 -->
<%= yield %>
</div>
</div>
</body>
</html>
上面的代码通过调用yield方法,将被装饰页面中的内容包含到这个装饰页面中来,这样做的含义是:用户浏览任何页面时,实际看到的都是这个母版和被装饰页面组合输出的结果。
接着,定义/app/views/user/register.rhtml视图文件,代码如下。
<table valign="top" width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td>
<center>
<!-- 调用error_messages_for帮助方法,返回User对象在校验时的错误信息 -->
<%= error_messages_for 'user' %>
</center>
<!-- 调用start_form_tag帮助方法,创建一个表单的开始标签:<form> -->
<%= start_form_tag :action => 'register'%>
<table align="center" width="500" border="0" cellpadding="0" cellspacing="0"
bgcolor="#F9CC76">
<tr>
<td colspan="2" id="title"> 注册</td>
</tr>
<tr>
<td width="12%"><b>用户名</b></td>
<!-- 调用text_field帮助方法,为User对象生成name属性的文本框 -->
<td >
<%= text_field("user", "name", :size => "20")%>
<span id="result"></span>
</td>
<!--使用observe_field检测User对象的name表单域 -->
<!--将name单行文本框设置为observe_field,每0.5秒执行一次check_name Action
-->
<!--并将返回的结果用于更新results这个部分-->
<%= observe_field(:user_name,
:frequency => 0.5,
# 指定更新update元素
:update => :result,
:url => { :action => :check_name }) %>
</tr>
<tr>
<td width="12%"><b>密码</b></td>
<!-- 调用password_field帮助方法,为User对象生成password属性的文本框 -->
<td ><%= password_field("user", "password", :size => "20")%></td>
</tr>
<tr>
<!-- 调用submit_tag帮助方法,生成一个提交(submit)按钮 -->
<td colspan="2" align="center"><%= submit_tag '提交' %></td>
</tr>
</table>
<!-- 调用end_form_tag帮助方法,创建一个表单的关闭标签:</form> -->
<%= end_form_tag %>
</td>
</tr>
</table>
Rails提供了丰富的帮助方法,可以在视图文件中直接调用,其中的一些帮助方法可用来生成Html标签,如:start_form_tag,text_field,password_field和submit_tag等。而error_messages_for帮助方法则返回ModelUser对象在校验时的错误信息。
如果用户请求URL对应的Action不存在,则Rails自动使用该控制器对应的视图模块里的同名RHTML页面来生成响应,所以,我们可以先不在user_controller.rb控制器文件中定义register方法,如果用户向http://localhost:3000/user/register发送请求,则该register.rhtml页面将对该请求生成响应。
注意 虽然我们可以不为一个RHTML页面提供空Action方法(也就是在控制器类里定义的一个方法),但这不太符合MVC的模式。MVC模式认为,所有用户请求都应该向Action发送,而不是直接发送给视图页面,视图页面只负责向用户输出响应。为此我们建议为每个RHTML视图页面都提供一个Action方法,即使该方法是空方法。
值得注意的是:上面的代码中,我们通过调用Rails提供的observe_field方法来执行Ajax调用,它负责检测名为user_name的表单域,每隔0.5秒执行一次check_name Action,这个Action将会返回用户输入的用户名是否已经被占用,并将返回的结果通过id为results元素输出。
由于上面的register.rhtml视图文件中进行了Ajax调用,因而需要在/app/views/layouts/ user.rhtml文件中包含JavaScript库,使得系统支持Ajax调用。包含系统中JavaScript库的代码片段如下:
<!-- 包含系统中默认的JavaScript库 -->
<%= javascript_include_tag :defaults %>
将上面的代码添加到user.rhtml文件中的head标签内即可。
接下来,在user_controller.rb控制器文件中,将原来scaffold代码生成器生成的create方法改为register方法,并修改该方法的定义。修改后的代码片段如下:
def register
# 如果用户请求不是一个POST请求
if request.method != :post
# 构造一个User对象(没有初始化)
@user = User.new
# 否则(用户请求是一个POST请求)
else
# 构造一个User对象,并使用参数进行初始化
@user = User.new(params[:user])
# 如果User对象能够成功保存进数据库(通过了有效性验证)
if @user.save
# 将提示信息写进flash[:notice]
flash[:notice] = '您已注册成功!请登录!'
# 重定向到login控制器的login Action
redirect_to :action => 'login'
#否则
else
# 提交给register Action
render :action => 'register'
end
end
end
register方法的定义中,首先判断用户请求是否为一个POST请求,如果不是,则只是创建一个没有初始化的User对象;如果请求为POST请求,则创建一个使用user参数进行了初始化的User对象,该对象再调用save方法。在执行save方法的过程中,Rails会调用一个validate方法来对Model对象进行校验。如果User对象通过模型校验,系统将重定向到login Action;否则,仍然提交给register Action。
由于我们的注册页面设计有检测用户名的功能,需要向check_name Action发送Ajax请求,因此,在这个控制器文件中需要定义check_name方法。代码片段如下:
def check_name
# observer_field使用text_field的当前值作为传递给Action的POST数据
# 因此,我们在“控制器”中使用 request.raw_post来访问这个数据
@name = request.raw_post || request.query_string
# 查找name属性的值与@name参数匹配的User对象
@user = User.find_by_name(@name)
# 提交时不使用layout模板
render(:layout => false)
end
从上面的代码注释可知,由于observer_field方法将text_field的当前值直接发送给Action,因此我们使用request.raw_post || request.query_string来取得请求参数。一旦取得用户参数后,我们从数据库中查找出一条name列的值与用户参数匹配的记录,将该记录对应的User对象发送给客户端作为响应。
该User对象数组提交给check_name视图,该视图对应check_name.rhtml页面。该页面代码如下:
<% if !@user.nil? %>
<font color="red"><%=h @name %>用户名已经被注册,请重新选择一个!</font>
<% else %>
<font color="green"><%=h @name %>用户名可用!</font>
<% end %>
然后,我们在user.rb模型文件中重写validate方法,对User对象的模型校验进行定义。代码片段如下:
def validate
# 验证name不能为空
errors.add("", "用户名不能为空") if name.empty?
# 验证password不能为空
errors.add("", "密码不能为空") if password.empty?
end
Rails在构造一个Model对象时,会自动为该对象创建一个Errors对象,用于存储模型校验过程中的错误信息。因此,先通过添加错误信息进入该对象中,然后在视图文件中调用error_messages_for帮助方法,即可返回这些错误信息。
在浏览器的地址栏中输入http://localhost:3000/user/register,打开用户注册页面。我们在第一个文本框内输入一个用户名,然后等待0.5秒。如果该用户名已经被占用,将看到如图2.7所示的页面。

图2.7 用户注册时检测用户名
由图2.7可看到,我们输入的用户名是不可用的。页面中出现红色的提示信息“用户名已经被注册,请重新选择一个!”。如果输入的用户名在数据库中不存在,则会提示“用户名可用!”
在注册页面中输入合法的数据,然后单击“提交”按钮,系统将会重定向到login Action,并在登录页面中显示“您已注册成功!请登录!”的提示。下一节,我们就开始实现登录功能。
2.2.3 实现用户登录
我们希望firstApp应用中具有用户登录功能,并且在用户登录页面中提供用户注册的链接。
由于scaffold生成器生成的文件中,没有类似用户登录的页面,因而我们自己在/app/views/user路径下新建一个login.rhtml视图文件。其代码片段如下:
<table valign="top" width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td>
<% if @flash[:notice] -%>
<div id="notice"><%= @flash[:notice] %></div>
<% end -%>
<center>
<!-- 手动输出错误提示信息 -->
<% if @errors and not @errors.empty? then -%>
<div id="errorExplanation">
<ul>
<!-- 遍历@errors变量中的每一个error元素 -->
<% for error in @errors %>
<li><%=h error %></li>
<% end %>
</ul>
</div>
<% end -%>
</center>
<%= start_form_tag :action => 'login'%>
<table align="center" width="350" border="0" cellpadding="0" cellspacing="0"
bgcolor="#F9CC76">
<tr>
<td colspan="2" id="title"> 登录</td>
</tr>
<tr>
<td width="20%"><b>用户名</b></td>
<td ><%= text_field("user", "name", :size => "20")%></td>
</tr>
<tr>
<td width="15%"><b>密码</b></td>
<td ><%= password_field("user", "password", :size => "20")%></td>
</tr>
<tr>
<td colspan="2" align="center"><%= submit_tag '提交' %></td>
</tr>
</table>
<%= end_form_tag %>
</td>
</tr>
<tr>
<td colspan="2" align="center">如果您还有没有注册,请点<%= link_to("<font style= 'font-size:10pt; color:blue'><b>这里</b></font>", :action => "register") %>注册新用户</td>
</tr>
</table>
上面的代码中,flash[:notice]是一个临时的值存取器,用于Action之间的通信。在一个Action中往flash[:notice]中存放数据,然后可在下一个Action中将这些数据取出。这样,当用户注册成功后,可重定向到login Action,取回register Action写进flash[:notice]的提示信息。
另外,上面的代码遍历了Errors对象中的每一个元素,手动输出模型校验过程中的错误信息。
接着,需要在user_controller.rb控制器文件中添加login方法的定义。代码片段如下:
def login
# 如果用户请求是个GET请求
if request.get?
# 将session[:user_id]清空
session[:user_id]=nil
@user = User.new
# 否则(用户请求不是个GET请求)
else
# 构造一个User对象,并用接收到的user参数来初始化该对象
@user=User.new(params[:user])
# 初始化@errors实例变量
@errors = Array.new
# 如果User对象的name属性为空
if params[:user][:name].to_s.empty?
@errors << '必须输入用户名!'
end
# 如果User对象的password属性为空
if params[:user][:password].to_s.empty?
@errors << '必须输入密码!'
end
# 如果@errors中没有错误信息,即数据校验过程没有错误产生
if @errors.size == 0 then
# 调用User类中自定义的try_to_login方法,来验证用户名和密码
# 如果用户名和密码和数据库中的某条用户记录匹配,try_to_login方法将返回该User对象
logged_in_user=@user.try_to_login
# 如果logged_in_user对象不为空,即该用户对象合法
if !logged_in_user.to_s.empty?
# 将合法的用户ID写入Session
session[:user_id]=logged_in_user.id
redirect_to :action=>"index"
else
@errors <<"用户名或密码错误!"
end
end
end
end
上面的代码首先判断用户请求是否为GET请求,如果是,将session[:user_id]清空;如果不是,则构造一个新的User对象,并用接收到的user参数来初始化该对象。然后,对用户输入的数据进行手动校验:校验User对象的name属性和password属性的值不能为空。并通过调用User模型中自定义的try_to_login方法,检查输入的用户对象是否合法。如果通过了所有的校验,将合法的用户ID写入Session,并重定向到index Action。
由于上面的login方法定义中调用了User模型中的try_to_login方法,因而,我们需要在user.rb模型文件中定义该方法。代码片段如下:
def try_to_login
# 开始事务处理
transaction do
User.find(:first,
:conditions=>["name=? and password=?", name, password]
)
# 事务处理完毕
end
end
try_to_login方法的定义中,在事务处理内部,User类调用Rails提供的find方法,查找出users表中name列和password列中的值分别与name参数和password参数匹配的记录,并返回第一条符合条件的记录所对应的User对象。
接着,我们来实现firstApp应用的登录控制。登录控制是通过Filter实现的,下面是/app/controllers/application.rb文件中的Filter方法代码。
# 定义为私有方法
private
# 该方法检查访问权限
def authorize
unless session[:user_id]
flash[:notice]="请先登录!"
redirect_to(:controller =>"user", :action=>"login")
end
end
这个Filter负责拦截用户请求,并检查用户请求的Session。如果Session中不包含登录后的用户ID,则说明用户尚未登录,系统跳转到登录界面。
定义了该Filter方法之后,将该方法定义成控制器中的Before Filter,Filter将会默认拦截控制器中所有的Action,包括拦截注册、登录和处理登录的Action,这会导致用户无法正常地注册和登录系统。因此,需要使用一个except选项来指定不被拦截的Action。代码片段如下:
# 将authorize方法定义成Before Filter
before_filter :authorize, :except=> [:login, :register, :check_name]
上面的代码将authorize方法定义成一个Before Filter,并通过except选项来指定该Filter不会拦截login,register和check_name这三个Action。
另外,我们希望将该Web应用的首页设置为登录页面,这在Rails中很容易做到。只需先将firstApp应用的public路径下的index.html文件删除,然后在该应用的config路径下的routes.rb文件中添加下面的代码:
map.connect '', :controller => "user", :action=>"login"
上面的代码是自定义用户请求的路由方式。
注意 在routes.rb文件中,有两条Rails默认的路由规则。因为Rails将用户请求映射到应用程序中时,是按照routes.rb文件中的路由规则从上往下依次匹配的,因此,上面的代码必须放在默认的路由规则上面。
在浏览器的地址栏中输入http://localhost:3000,将会打开登录页面。如果输入的用户名和密码不正确,提交后系统仍返回登录页面,并在页面中显示错误提示信息。在浏览器中看到的效果如图2.8所示。

图2.8 登录的用户名或密码不正确






