首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

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  登录的用户名或密码不正确

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论