第2章 体验Ruby on Rails敏捷开发
前面已经介绍了Rails开发平台的各种相关知识,本章将向读者示范如何开发一个简单的Rails应用,这个Rails应用是一个简单的留言系统。
通过开发这个简单的留言系统,将向读者介绍Rails中如何使用一句简单的命令来生成整个Web应用的基本框架;如何使用代码生成器来生成应用程序的支架,实现C(Create)R(Read)U(Update)D(Delete)这些常用功能。
本章采用的开发方式是:随着客户需求的变更,应用的功能也可以逐渐改变、完善。通过这种开发方式,我们不难发现,使用Rails开发平台开发应用既可以快速提供应用原型让用户体验,也可以迅速满足用户新的需求。这就是Rails敏捷开发的魅力!
2.1 创建第一个Web应用
Rails包含了功能强大的代码生成器和一系列能够简化开发的约定规则,这就使得Ruby on Rails像是Web应用开发中的一场革命,将程序员从以往令人头疼的繁重开发工作中解脱出来,从而可以轻松地享受编程的迅捷与快乐。
2.1.1 生成Rails应用及启动应用
Rails提供了功能强大的代码生成器,当我们选择Rails作为开发平台后,就可以使用它们来生成应用中的基本代码。这意味着我们不需要使用任何IDE工具,就可以开发Rails项目。不像开发一个Java Web应用,如果不借助于任何IDE工具,就需要手动地一点一点地新建文件夹、配置文件……使用Rails只需要一个命令。
我们先介绍Rails的第一个代码生成器:rails appName [options],该命令用于生成一个Rails应用的基本框架。其中,appName是该应用的名称,而options则是创建该应用的各种选项。
下面,我们使用rails命令来生成一个Rails应用。在DOS窗口输入如下命令:
rails firstApp
即可看到如图2.1所示的界面。
图2.1 使用rails命令生成Rails应用
只要执行这条简单命令,Rails就自动为我们生成了一个基本应用所需要的文件夹和文件,也就是完成了一个Rails应用的建立工作。进入我们刚才执行rails命令的目录(由图2.1中可以看出笔者在G:盘根路径下),将可以看到一个firstApp的目录,这个目录就是我们所建立的Rails应用。
接下来,就可以启动刚刚建立的Rails应用了。在DOS窗口下切换到该应用所在的路径下,执行如下命令:
ruby script/server
上面的命令将会启动刚刚建立的Rails应用。实际上,该命令是通过应用script路径下的server文件来执行的。使用上面的命令启动firstApp应用,将在DOS窗口看到如图2.2所示的运行界面。
图2.2 启动Rails应用
从图2.2中可以看出,Rails应用所使用的Web服务器是WEBrick。WEBrick是Ruby领域的Web服务器,就像Dot NET领域的IIS服务器,或者Java EE领域所使用的Tomcat,JBoss等。Web服务器的基本作用就是对外提供HTTP服务。当然,Web服务器的实现细节就不可能在这里深入展开了。
提示 有的读者可能会感到奇怪:我们从来没有安装过什么WEBrick服务器,机器上怎么会包含WEBrick服务器呢?读者不要忘记,我们安装过Ruby和Rails,Ruby默认包含了丰富的软件,其中WEBrick HTTP服务器就是其中之一。WEBrick服务器允许我们自定义文档根路径,还可以像编写Java Servlet那样,编写Ruby领域的Servlet程序。但对于Rails开发者而言,我们无须太关心WEBrick的底层细节。
上面使用的启动Rails应用的方式,是最基本的启动Rails的方式,它启动的是开发环境中的应用。我们可以通过在ruby script\server命令中使用一些选项,来指定Rails的启动方式,常用的选项如下。
❑ -p port(或--port port):指定运行Rails的端口。例如:ruby script\server -p 3001。默认端口为3000。
❑ -b=ip(或--binding=ip):绑定Rails到指定的IP。默认IP为0.0.0.0。
❑ -e name(或--environment name):指定运行环境为test或development或production。例如:ruby script\server -e production。
❑ -c=charset(或--charet=charet):为输出设置默认的字符集。例如:ruby script\server-c=GB2312。如果没有使用这个选项,则默认的字符集为UTF-8。
❑ -h(或--help):显示该命令的帮助信息。
启动Rails应用之后,就可以在浏览器中浏览这个应用了。我们使用如图2.2中不带任何选项的方式启动Rails,在浏览器的地址栏输入“http://127.0.0.1:3000/”,即可看到如图2.3所示的界面。
图2.3 firstApp应用
经过上面步骤,已经搭建了一个Rails应用框架。搭建一个Rails应用只需要一个命令就可以完成,这确实非常方便;当建立Rails应用之后,无须进行任何部署,只需一个命令就可启动Rails应用,这也是非常方便的事情。
为了方便读者观看本书中例子的运行效果,在所有例子的根路径下,都放有一个start.cmd文件,双击该文件,即可启动Rails服务器或控制台(启动控制台我们后面会讲到)。
提示 当我们使用rails命令来建立Rails应用时,rails命令会在firstApp应用的script路径下生成一个名为server的脚本,这个脚本用于启动WEBrick服务器。
上面的Rails应用完全没有任何业务功能,这是肯定的!计算机永远也不知道我们想要什么,所以它无法帮我们实现太多具体功能。
但上面的Rails应用又包含了如下几个代码生成器。
❑ scaffold:生成某个模块的应用代码框架。
❑ controller:生成某个模块的控制器代码框架。
❑ model:根据数据表生成一个ActiveRecord模型类。
❑ mailer:生成一个邮件模块的代码框架。
❑ web_service:生成一个WebService模块的代码框架。
下面,我们来介绍如何使用scaffold代码生成器。因为在本章将要向读者示范的应用,就是在scaffold生成的支架的基础上改进而来的。
提示 对于一个企业应用而言,需求变更是不可避免的事情,我们的开发平台必须能正常应对这种需求变更。Rails平台提供了基本的代码生成器,通过这些代码生成器可以很快地开发出应用的雏形。而这个应用雏形具有很好的可扩展性,可以非常方便地添加新的功能,处理应用程序的需求变更,这也就是敏捷开发的精髓。
2.1.2 配置数据库
生成scaffold需要进行持久层访问,因此,我们必须先进行数据库配置。数据库配置写在firstApp应用的\config\ database.yml文件中,打开该文件,看到如下代码(不包含注释)。
development: adapter: mysql database: firstApp_development username: root password: host: localhost test: adapter: mysql database: firstApp_test username: root password: host: localhost production: adapter: mysql database: firstApp_production username: root password: host: localhost
上面的代码大致可分为三部分,每部分的结构相同,分别表示在开发环境(development)、测试环境(test)和产品环境(production)下的数据库配置。当我们使用rails命令生成一个应用框架后,这个database.yml文件中就已经默认有了绝大部分配置参数的值,我们只需在password:后增加连接数据库所用的密码,就可以告诉Rails系统如何连接数据库。而我们在开发一个Rails应用时,只需要关注development部分的代码即可。因此,我们只需为development部分添加password参数的值。
提示 Rails使用YML文件进行配置管理,YML文件和XML文件有大致相似的格式和功能,但更加简洁。例如上面的代码中可以认为development是一个父元素,该元素下有5个子元素,分别指定连接数据库所用的适配器、数据库名、数据库用户名、数据库密码和数据库服务所在的主机。
Rails应用的数据库配置方式,体现了Rails的约定优于配置的原则。Rails约定:当一个Rails应用程序启动时,它会自动寻找根路径下的\config\database.yml文件,查看当前运行环境(development或test或production)中的数据库连接部分,按照这部分的配置进行数据库连接。因此,当我们需要进行数据库连接时,只需要按照Rails约定的方式,在database.yml文件中找到相应的运行环境的设置部分,添加其password的值。
此外,Rails默认是根据database.yml文件中名为development的数据库配置建立连接的。
注意 更改数据库配置文件之后,必须重启服务器,才能使对数据库连接所做的更改生效。
现在假设我们需要开发一个简单的用户注册、用户登录应用,为此,我们需要建立一个用户数据表来保存系统的注册用户,下面是创建用户数据表所使用的数据库脚本。
drop database if exists firstApp_development; -- 创建数据库 create database firstApp_development; use firstApp_development; -- 创建用户表:users create table users ( id int not null auto_increment, -- 定义用户名字段:name name varchar(100) not null unique, -- 定义密码字段:password password varchar(100) not null, -- 定义主键 primary key (id) );
在MySQL的控制台导入上面数据库脚本,就可以创建本应用的开发数据库和用户表。
2.1.3 使用代码生成器生成支架
scaffold是Rails提供的一个功能十分强大的代码生成器,它可以生成一个简单应用雏形,这个应用雏形为某个实体生成基本的C(Create)R(Read)U(Update)D(Delete)操作,这个雏形应用就被称为支架。
使用scaffold代码生成器非常简单,即在DOS窗口进入Rails应用所在的路径下,然后执行如下格式的命令:
ruby script/generate scaffold模型名(控制器名)
上面格式的命令中,模型名为必选项,控制器名是可选项。
如果省略控制器名,则表示生成指定模型(Model)的基本的CRUD操作。所谓模型(Model),就是对应一个持久化实体的名称。Rail约定:数据库中的表对应模型中的一个类。例如数据库里有一个users数据表,而系统就可以对应一个User Model。这里,我们又看到了约定优于配置的好处,这些约定使得我们不必关心Rails的底层实现,只要按照Rails的约定,就能很容易地将数据层和持久层映射起来。必须指出的是,使用scaffold代码生成器时,Rails会检测系统所配置的数据库里是否包含Model所需的数据表。
如果指定控制器名,则可以为指定的控制器生成关于指定Model的基本的CRUD操作。
提示 正如前面介绍的,当我们使用rails命令来创建一个Rails应用时,rails命令为script路径下生成了很多脚本,其中generate就是一个代码生成器脚本,用它可以自动生成很多应用的框架代码。
现在,使用支架来为我们的firstApp应用生成支架。因为我们刚才为应用创建了一个users数据表,根据Rails的约定,它将会对应一个User Model。因此,我们可以为User Model使用scaffold代码生成器,即在DOS窗口进入Rails应用所在的路径下,执行如下命令。
ruby script\generate scaffold User User
在DOS窗口中看到的执行结果如图2.4所示。
图2.4 使用scaffold代码生成器
由图2.4可看到,使用scaffold代码生成器,生成了一系列的相关文件或文件夹。其中,自动在app/models路径下为我们生成了user.rb这个Model文件;在app/controllers路径下生成了Model对应的user_controller.rb控制器文件;在app/views路径下创建了一个user文件夹,该文件夹用于存放User控制器调用的所有视图文件。
在firstApp应用的根路径下双击start.cmd批处理文件,启动WEBrick服务器,然后在浏览器的地址栏输入http://localhost:3000/user或http://127.0.0.1:3000/user(假定读者在本机启动Rails应用),将会在浏览器中看到list页面。该页面用于列出所有用户对象,因为我们的数据库中目前没有任何的用户,所有页面中没有列出任何用户。单击“New user”链接,即可进入添加用户的页面,如图2.5所示。
由图2.5可知,由scaffold生成的添加页面是十分简单和不美观的。毕竟,这只是Rails为我们生成的最基本的视图模板。不过没关系,本章的后面会向读者介绍如何定制个性化视图。
在图2.5所示的页面中输入数据后提交,将会重定向到list页面,并在该页面上显示成功信息。浏览器中将会看到如图2.6所示的效果。
图2.5 使用scaffold生成的添加用户页面
图2.6 使用scaffold生成的用户清单页面
从图2.6可看出,该页面中除“New user”链接之外,还包含了“Show”、“Edit”和“Destroy”链接,这正是对应scaffold代码生成器的基本功能:生成对特定Model的CRUD操作。这对于快速迭代开发是非常有帮助的,还可以让客户在非常短的时间内看到系统的雏形。
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 登录的用户名或密码不正确
2.3 应对新需求
我们已经为firstApp应用完成了用户注册和登录功能,当然这个功能实在太过简单。假设用户提出了新需求:希望用户登录该系统后,能够添加留言,浏览所有留言,查看具体的一条留言和删除留言。也就是说,他希望将这个应用扩展成一个小型的留言系统。
这时候就是Rails敏捷开发大展身手的时候了:当应用的需求发生变更时,它可以非常容易地在原有应用的基础上进行扩充,增加新的功能,这也是Rails被称为敏捷开发框架的原因。
现在,我们已经确定了需要将firstApp应用改变为一个留言系统。那么,先修改该应用的标题:在/views/layouts路径下的user.rhtml视图文件中,将title中的文本由原来的“第一个Web应用”标题修改为“欢迎光临留言系统”,并将id="header-section"的div标签中的文本由原来的“第一个Web应用”修改为“留言系统”。此外,为了使得留言的视图文件和用户的视图文件能共用一个相同的装饰页面,把user.rhtml视图文件重命名为application.rhtml。
2.3.1 添加Model
将本Web应用扩充为一个留言系统,首先需要添加一个留言Model,这个Model对应数据库中的留言表。下面,在数据库中创建一个新的数据表,该表的MySQL脚本如下。
-- 创建留言表:messages create table messages ( id int not null auto_increment, -- 定义留言标题:title title varchar(50) not null, -- 定义留言内容:detail detail varchar(255) not null, -- 定义留言图片的数据:picture_data picture_data blob, -- 定义留言图片的类型:picture_content_type picture_content_type varchar(255), -- 定义留言时间:created_at created_at timestamp not null, -- 定义留言用户的ID:user_id user_id int not null, -- 建立外键约束:user_id为messages表参照users表的外键,它参照了users表中的id列 constraint foreign key (user_id) references users(id), -- 定义主键 primary key (id) );
我们将数据库中的留言表定义成messages,该表中定义了一个picture字段,用来存放用户贴图在系统中的路径;而定义的user_id字段则是messages表参照users表的外键。此外,根据Rails的表对应类的约定,该表将对应Model中的Message类。
接着,用scaffold代码生成器为Message Model生成基本的CRUD操作,即在DOS窗口进入Rails应用所在的路径下,执行如下命令:
ruby script\generate scaffold Message Message
执行完毕上面的命令之后,Rails会在该应用中为我们自动生成Message Model的支架。
在浏览器的地址栏输入http://localhost:3000/message或http://127.0.0.1:3000/message,将会看到可以列出所有留言信息的list页面。由于我们尚未添加任何留言,因此该页面中没有任何留言记录。单击页面下方的New message链接,将会进入添加留言的页面,如图2.9所示。
图2.9 添加新留言
读者会注意到,图2.9中的Picture data域是一片空白,而Picture content type域是一个单行文本框,不是一个文件域。这是因为,我们的messages表中picture字段是定义成varchar类型的。所以,Rails就会将它当成一个文本字段来处理,在模板中将它设计成一个单行文本框。这只是视图模板,不用担心,因为在后面完善这个firstApp应用时,我们还要修改这里。
另外,由于messages表中定义了不允许为空的user_id字段,而添加留言的页面中没有留言人的输入框(也没有必要有该输入框),同时,在scaffold为我们生成的message_controller.rb控制器文件中,用于添加一条留言记录的create Action并没有为user_id属性赋值,所以,这时候是不能成功地添加一条留言记录的。由此可看出,尽管scaffold代码生成器的功能十分强大,但仅仅使用它也并不是万能的,这是肯定的!它只是生成一个具有基本的CRUD操作的支架,不可能完成整个应用的所有功能。这就需要程序员在scaffold生成的代码的基础上进行修改和完善,这也就是我们下面几节将要介绍的内容。
2.3.2 改进“浏览留言”
我们希望当用户成功登录系统后,首先进入浏览留言页面,这需要修改user_controller.rb控制器文件中的login方法,将合法用户登录成功后重定向到message控制器中的list Action。代码片段如下:
redirect_to :controller=>"message", :action=>"list"
message控制器的list Action负责列出系统中所有留言,scaffold默认生成的list Action已经具有分页功能,因此我们不需要改变list Action。list Action的代码如下:
def list # 执行分页查询 @message_pages, @messages = paginate :messages, :per_page => 10 end
上面的list Action里使用了Rails提供的分页查询支持,通过这种分页查询支持,可以非常方便地以分页方式列出所有留言。
与该list Action对应的视图文件是/app/views/message路径下的list.rhtml文件。其中,用于列出所有留言内容的代码片段如下:
<table width=100% border=0 align="center" cellpadding="0" cellspacing="0" bgcolor="#BFCAE6"> <tr> <td colspan="6" id="title">所有留言</td> </tr> <tr> <td width="30%"><b>标题</b></td> <td width="30%"><b>时间</b></td> <td width="10%"><b>是否有图</b></td> <td width="15%"><b>留言人</b></td> <td colspan="2"> </td> </tr> <!-- 遍历@messages实例变量中的每一个message对象 --> <% for message in @messages %> <!-- 调用cycle帮助方法来设置循环的行的CSS样式 --> <tr class="<%= cycle("even", "odd") %>" > <!-- 调用truncate帮助方法,将title值设置为只显示前25个字符 --> <td width="30%" ><%= truncate(message.title, 25) %></td> <!-- 调用自定义的show_date帮助方法,来返回指定格式的日期形式 --> <td width="30%" ><%= show_date(message.created_at) %></td> <td width="10%"> <!-- Message对象调用自定义的has_picture?方法,查询留言是否有贴图 --> <% if message.has_picture? then %> <font style="color:red">有图</font> <% end %> </td> <!-- Message对象调用user实例方法,返回该对象对应的User对象 --> <td width="15%"><%= message.user.name %></td> <td width="7%"><%= link_to '查看', :action => 'show', :id => message %></td> <td> <% if session[:user_id] == message.user.id then %> <%= link_to '删除', { :action => 'destroy', :id => message }, :confirm => '您确定删除吗?', :post => true %> <% else %> <% end %> </td> </tr> <% end %> </table>
修改后的list页面比原来的代码丰富了很多,主要是增加了设置奇数行和偶数行的CSS样式,只显示标题的前25个字符,格式化日期的显示方式,以及为有贴图的留言显示“有图”字样。
值得注意的是,在上面的代码中,Message对象调用user实例方法,来返回该对象对应的User对象。读者可能觉得奇怪,哪里定义的user实例方法呢?因为Message Model和User Model是两个关联实体,且Message Model是User Model对应的从表实体,所以,在message.rb模型文件中加入如下代码:
# 调用Rails中的belongs_to方法,声明Message对象是User对象的从表对象 belongs_to :user
即可由belongs_to提供一系列方便地操作关联对象的实例方法,其中就有user实例方法。这样,当Message对象调用user实例方法时,就能十分方便地查找到它所对应的User对象,而不需要通过如下代码来返回User对象。
user_id=message.user_id user=User.find(user_id)
这种方式是先查询出留言对象的user_id,再通过这个user_id查找到对应的User对象。相比之下,我们在Rails中仅仅调用一个belongs_to方法,即可实现同样的功能,代码要简洁得多。
由于list.rhtml文件中调用了自定义的has_picture?方法,来查询留言是否有贴图,所以需要在message.rb模型文件中定义该方法。代码如下:
# 设置允许上传的图片文件最大为50 kb MAX_IMAGE_SIZE = 50*1024 # 该方法用于检查Message对象是否有图片 def has_picture? transaction do # 如果Message对象的图片内容的类型不为nil if self.picture_content_type!=nil then # 如果存在合法的上传图片 # 即:文件类型为图片,且文件大小不超过允许上传的最大图片的大小 if (self.picture_content_type).split("/")[0]=="image" && self.picture_data.size<= MAX_IMAGE_SIZE then return true end end return false end end
该方法的定义中,先判断Message对象的picture_content_type属性值是否为nil;如果不为nil,再判断是否存在合法的上传图片。也就是,仅当Message对象的picture_content_type属性值为图片类型的值,且文件大小不超过允许上传的最大图片的大小时,has_picture?方法才返回true,其他情况均返回false。
另外,由于list.rhtml文件中调用了自定义的show_date帮助方法,来返回格式化的日期形式,因而,需要在/app/helpers路径下的message_helper.rb文件中定义该方法。代码片段如下:
def show_date(date) # 日期将会被格式化成:年月日 时分秒 date.year.to_s+"年"+date.month.to_s+"月"+date.day.to_s+"日"+" "+date.to_s.split(" ")[3] end
该方法用于格式化需要显示的日期,使之符合我们的计时习惯。
控制器中不需要修改scaffold生成的代码。当用户使用这个应用成功登录后,就会进入浏览留言的页面。
2.3.3 改进“新增留言”
我们希望当新增留言的时候,允许用户上传图片,并能判断上传文件的类型是否为图片和文件是否过大。这在Rails中不难做到,不需要使用任何插件,只用十分简洁的代码即可实现图片的上传功能!这的确是一件让程序员愉快的事情。
在这一节,我们来改进“新增留言”,着重介绍图片上传的实现。
修改/app/views/message路径下的new.rhtml视图文件。修改后的代码如下:
<table valign="top" width="100%" border="0" cellspacing="0" cellpadding="0"> <tr> <td> <center> <%= error_messages_for 'message' %> </center> <!-- 将multipart选项设置为true,允许上传文件 --> <%= start_form_tag ({ :action => 'create', :id => @message }, :multipart => true) %> <table align="center" width="400" border="0" cellpadding="0" cellspacing="0" bgcolor="#BFCAE6"> <tr> <td colspan="2" id="title">新增留言</td> </tr> <tr> <td width="15%"><b>标题</b></td> <td ><%= text_field('message', 'title', :size => "30")%></td> </tr> <tr> <td width="15%"><b>内容</b></td> <td ><%= text_area 'message', 'detail', "cols" => 40, "rows" => 10 %></td> </tr> <tr> <td width="15%"><b>贴图</b></td> <!-- 调用file_field帮助方法,生成一个文件域 --> <td ><%= file_field 'message', 'picture' %></td> </tr> <tr> <td colspan="2" align="center"><%= submit_tag '提交 ' %></td> </tr> </table> <%= end_form_tag %> </td> </tr> </table>
上面的代码在start_form_tag帮助方法中指定multipart的选项值为true,使得该表单能够发送文件数据。并且,通过调用file_field帮助方法,生成了一个文件域。这里的picture属性只是个虚拟属性,因为在数据库的messages表中并不存在这个属性。这需要我们对这个虚拟属性做一些处理,使之对应messages表中的真实属性。具体实现方式下面会有详细介绍。
上面的表单是没有为留言时间设计表单域的,因为我们可以通过更简便的方式来为Message对象的这个属性赋值:messages表中表示留言时间的字段名为created_at,数据类型设计为timestamp,这样,当保存一条Message对象对应的记录时,Rails就会自动将当前时间赋值给created_at列,而不需要我们手动赋值。
在message_controller.rb控制器文件中,需要修改create方法,修改后该方法的代码片段如下。
def create # 查找出当前留言的User对象 user=User.find(session[:user_id]) # 将该User对象赋值给参数中Message对象的user属性 params[:message][:user]=user # 构造一个Message对象,并使用message参数来初始化该对象 @message = Message.new(params[:message]) # 如果Message对象能成功保存进数据库 if @message.save flash[:notice] = '新增留言成功!' # 重定向到list Action redirect_to :action => 'list' else # 提交new Action render :action => 'new' end end
在create方法的定义中,前面两句代码是scaffold生成的默认代码中所没有的。这是因为user_id是messages表参照users表的外键列,scaffold不会自动生成对外键列的操作。所以,我们需要根据session[:user_id]来查找出留言的User对象,并把该对象赋值给表单参数中的Message对象,作为它的一个user属性。这样,当Message对象调用save方法保存进数据库的时候,会让该Messge对象对应的数据行参照到该User实例对应的数据行。
在Message Model文件中,重定义一个picture=方法。因为我们的new.rhtml视图文件中有一个picture表单域,当提交表单后控制器将发送一个message[:picture]的请求参数,这个请求参数将要求Message类里包含一个picture=方法,该方法用于接受message[:picture]请求参数。
picture=方法负责把message[:picture]请求参数(这个请求参数值是一个文件对象,里面包含了非常丰富的信息)解析出来。下面是picture=方法的代码:
# 提供picture=方法,将一个picture的表单域设置成Message对象的多个属性 def picture=(picture_field) transaction do # 如果用户上传了图片 if picture_field.size>0 then # @picture_size为上传图片的文件大小 @picture_size=picture_field.size # @picture_type为上传图片的文件类型 @picture_type=picture_field.content_type.chomp # 设置Message对象的picture_content_type属性 self.picture_content_type =@picture_type # 设置Message对象的picture_data属性 self.picture_data = picture_field.read end end end
提供了picture=方法之后,我们就将message[:picture]请求参数与messages表中的真实属性picture_content_type和picture_data对应起来了。
在上传文件方面,Rails处理得很好,表单中的文件域参数值不再是一个简单的类型值,而是已经被包装成一个文件对象,它也有size,content_type和read方法,直接调用这些方法,即可返回这个文件对象的大小、文件类型和包含的二进制数据。
当用户添加一条留言时,我们需要对留言对象进行模型校验,这可以通过在message.rb模型文件中重写validate方法来实现。代码如下:
def validate # 验证title不能为空 errors.add("", "标题不能为空") if title.empty? # 验证detail不能为空 errors.add("", "内容不能为空") if detail.empty? # 下面校验上传的图片 if @picture_type != nil # 校验上传图片的文件类型 errors.add("", "贴图不是合法的图片文件") unless @picture_type =~ /^image/ end if @picture_size != nil # 校验上传图片的文件大小 errors.add("", "贴图文件太大,应不能超过50 KB") if @picture_size > MAX_IMAGE_SIZE end end
在上面的代码中,我们校验留言标题、留言内容的值不能为空,上传的文件类型必须是图片形式,并且文件大小不能超过允许上传的最大图片值(MAX_IMAGE_SIZE为允许上传的最大图片值,这个常量在前面的2.3.2节中已经有定义)。
登录留言系统后,在list页面中单击下方的“新增留言”链接,进入新增留言页面。然后在该页面中输入数据,选择一个图片文件上传。如果图片不合法,将会在浏览器中看到如图2.10所示的效果。
图2.10 新增留言时图片不合法
由图2.10可看到,页面中显示出了错误提示信息,系统仍停留在新增留言的页面。选择一个合法的图片文件,再单击“提交”按钮,才能成功地添加留言。
2.3.4 改进“查看留言”
scaffold代码生成器生成的代码是不能满足我们查看留言的需求的,它不能显示图片,界面也十分简陋,这需要我们来改进“查看留言”部分的代码。
修改/app/views/message路径下的show.rhtml视图文件,修改后用于显示留言的代码片段如下。
<table width="80%" border=0 align="center" class="pt9" cellpadding=10 cellspacing="1" bgcolor="#BFCAE6"> <tr> <td> <p> <label for="message_name">标题:</label> <%= @message.title %> </p> <p> <label for="message_description">内容:</label> <%= @message.detail %> </p> <p> <label for="message_picture">图片:</label> <!-- 如果留言有图片 --> <% if @message.has_picture? then %> <!-- 调用url_for帮助方法,生成一个URL --> <img height=120 width=120 align="center" src="<%= url_for(:action => "show_picture", :id => @message.id) %>"/> <% else %> <i>无图片</i> <% end %> </p> <p> <label for="message_time">留言时间:</label> <%= show_date(@message.created_at)%> </p> <p> <label for="message_category">留言人:</label> <!-- 调用user实例方法,返回Message对象对应的User对象 --> <%= @message.user.name %> </p> <br> <%= link_to '返回', :action => 'list' %> </td> </tr> </table>
上面的代码中,Message对象调用has_picture?方法来查询留言是否有图片(has_picture?方法定义的代码已经在前面2.3.2节中介绍过)。如果留言有图片,则调用Rails提供的url_for帮助方法,生成一个将会向show_picture Action发送请求的URL。然后通过这个Action来负责显示图片。
在message_controller.rb控制器文件中,添加show_picture Action的定义,该Action负责把数据库里保存的图片二进制数据转换成图片输出。该Action代码片段如下:
# 该方法用于在线显示图片 def show_picture @message = Message.find(params[:id]) # 如果留言有图片 if @message.has_picture? then # 通过调用send_data方法来发送图片中的二进制数据给用户 send_data(@message.picture_data, # 通过将disposition选项设置为inline,指定直接显示该图片,而不是附件 :disposition => "inline", # 指定一个HTTP内容类型 :type => @message.picture_content_type) end end
上面的方法定义通过调用Rails的send_data方法来把二进制数据转换成图片输出。其中,send_data方法的disposition选项应该指定为inline,否则,将会把文件以附件的形式下载。
修改完毕上述的代码之后,可以在浏览留言页面中选择一条标注为红色的“有图”字样的记录,单击它的“查看”链接,即可查看该留言的详细信息,包括图片。在浏览器中的显示效果如图2.11所示。
图2.11 查看有图的留言
如图2.11所示,页面中不仅显示出留言的文本信息,同时显示出了图片。如果用户查看的是一条没有图片的留言记录,则用于显示图片的位置将仅仅显示“无图片”的文本提示信息。
2.3.5 改进“删除留言”
对于删除留言,我们应当考虑到,只有留言的发表者才有权利看到“删除”链接,并能够删除留言。这需要我们在scaffold代码生成器生成的雏形上做一些改进。
在/app/views/message路径下的list.rhtml视图文件中,添加代码来控制“删除”链接的显示。代码片段如下:
<% if session[:user_id] == message.user.id then %> <%= link_to '删除', { :action => 'destroy', :id => message }, :confirm => '您 确定删除吗?', :post => true %> <% else %> <% end %>
经过上面的代码控制之后,只有当留言发布者的ID与session[:user_id]相等时,才显示“删除”链接,否则只是输出空格。因此,对于不是留言发布者的用户,这个“删除”链接将是不可见的。
修改message_controller.rb控制器文件中的destroy方法定义。修改后的代码片段如下:
def destroy # 查找指定ID的Message对象 message=Message.find(params[:id]) # 如果session[:user_id]与该Message对象对应的User对象的ID值相等 if session[:user_id] == message.user.id then # 删除该Message对象 message.destroy redirect_to :action => 'list' end end
上面的代码中,我们先调用find方法查找出当前需要删除的Message对象,如果该对象对应的User对象的ID值与session[:user_id]的值相等,即当前用户是该条留言的发布者,则删除Message对象对应的记录,并重定向到list Action。
修改完毕上述的代码之后,我们就完成了“删除留言”部分的改进工作。
到此为止,这个firstApp应用已经完善和扩充完毕了。现在可以把scaffold生成的我们不需要的代码或文件删掉。
2.4 本章小结
本章主要向读者示范了如何通过Ruby on Rails框架来开发一个Web应用,并示范了如果客户的需求发生变更,Rails开发平台如何应对这种改变。读者阅读本章时应该可以感受到Ruby on Rails平台的魅力:高效的开发效率,良好的可扩展性。本章主要是向读者示范Ruby on Rails平台的开发过程,因此大量使用了后面的知识点,读者可以参考后面章节的知识来阅读本章内容。后面章节将会细致介绍本章所使用的各知识点。