Rails中文教程


前言 Rails 框架从诞生的第一天开始就受到了 B/S 应用开发领域的广泛关注,经过大约 几年发展,Rails 框架已经逐渐成熟稳定起来,国外已有相当多的公司开始选择从 Java EE 平台过渡到 Rails 开发平台。毕竟,Rails 平台提出了一个非常具有诱惑力的口号: 开发效率是 Java EE 开发平台的 10 倍,这个口号正确与否不是笔者所关心的,但 Rails 确实具有很好的开发效率,而且很难出错:在 Java EE 开发领域,你需要掌握很多框架, 详细了解每个框架的各种特性,你才可以组合出一套适合自己的开发平台。但在 Rails 领域,你无需选择,你只要按 Rails 平台的约定开发,一切都会非常合理,非常优雅。 Rails 平台不仅提供了整套的 MVC 解决方案,并且提供了丰富的代码生成器。Rails 甚至不完全是一个框架,更像一个企业开发平台,它搭建了整个应用开发的骨架,并提 供了大量的代码生成器来辅助开发,开发者只需在系统生成的代码骨架上进行适当修 改、定制即可完成应用的开发。选择使用 Rails 作为开发平台,仿佛上了铁轨的火车一 样,火车只管向前运行,铁轨将负责把火车带到最终目的地(Rails 就是铁轨的意思)。 Rails 框架对迭代式的增量开发有很好地支持,即使系统的数据库需要重新设计、 需要增加系统实体,Rails 一样能够轻松面对(本书第 2 章示范了这种过程)。迭代式的 增量开发正是敏捷开发的核心之一,这也是 Rails 框架被称为敏捷开发框架的原因。不 仅如此,由于客观世界的复杂性,需求的变更常常是许多开发者最头疼的事情。使用了 Rails 开发平台后一切都改变了:Rails 替你做好了拥抱改变的准备。 本书内容结构 本书分成 4 篇: 第 1 篇(第 1~2 章)主要介绍了 Ruby on Rails 的基本知识,包括搭建 Ruby on Ra ils 开发平台,第 2 章则介绍了一个 Ruby on Rails 的入门项目,让读者体验 Rails 敏 捷开发,从而感受 Rails 平台的魅力。 第 2 篇(第 3~10 章)主要介绍 Ruby 语言基本知识,Rails 平台选择 Ruby 作为开发 语言,而且 Rails 框架本身也是使用 Ruby 语言开发的,因此 Ruby 语言基本知识也是本 书介绍的重要内容。本书详细介绍了 Ruby 语言的相关语法,包括 Ruby 面向对象特征, Ruby 的迭代器、正则表达式支持、异常处理等基础知识,也包括 Ruby 的多线程、IO 和 网络通信等高级内容。 第 3 篇(第 11~16 章)主要介绍 Rails 框架,详细介绍了 Rails 的模型(M)、视图 (V)和控制器(C)各部分的解决方案,也讲解了 Rails 的邮件支持和 Ajax 支持,以 期读者能充分掌握利用 Ruby on Rails 开发 B/S 结构应用的相关知识。 第 4 篇(第 17~18 章)是案例部分,介绍了两个案例:在线考试系统和电子拍卖系 统,两个系统分别属于电子学习领域和电子商务领域,极具代表性。案例的实体包括复 杂的 1-N 多重关联映射,对于实际企业开发有很好的示范意义。不仅如此,案例还充分 利用了 Rails 提供的 Ajax 支持来改善用户体验,具有很好的指导作用。 本书有什么特点 当《Struts 2 权威指南——基于 WebWork 核心的 MVC 开发》一书问世以来,笔者收 到很多读者来信,他们告诉笔者,非常喜欢书中介绍知识的方式:简短的文字介绍知识 点的原理、小巧的案例介绍知识点的用法,最后指出知识点的实际应用场景。本书也保 持了这一风格,依然坚持用案例来说话,希望带给读者 Ruby on Rails 如此简单的感觉。 虽然笔者现在不再是一个全职开发者,但笔者一直以程序员自居:程序员喜欢简单 的东西,程序员的职责就是把复杂的事情简单化,然后交给计算机去干。所以笔者坚持 认为:真正属于程序员的书应该是简单的,而不是难以阅读的。如果读者阅读本书时, 觉得书里介绍的知识很简单,那就可以按书里的介绍进行 Ruby on Rails 开发,那笔者 撰写本书的目的也达到了。 本书由两位作者合作完成,两位作者有极好的互补关系。其中郭秋霞女士是美国 Re actrix 公司 Rails 项目组成员,具有丰富的 Ruby on Rails 实际开发经验。本书之所以 得以出版,也得益于她的多次敦促。笔者也从她的工作中看到 Ruby on Rails 在国外的 广泛应用,从而感受到 Ruby on Rails 在国内“山雨欲来风满楼”的气氛。郭秋霞女士 以她丰富的经验为基础,完成本书的全部案例部分,希望带给读者真实企业开发的感受, 丰富的实际开发经验将让本书更加实用。 笔者在 Rails 流行之初就对其抱以极大的热情(笔者曾模仿 Rails 框架设计出 Java 领域的“Rails”框架,希望有机会与读者分享)。笔者目前是一个软件设计的布道者, 教过的学生有应届毕业生、有刚工作的程序员、也有具有多年开发经验的技术经理、技 术总监,从而能感受到各种层次读者的学习痛苦,因此写作过程中针对性很强,希望通 过本书能减少各种读者在学习过程中的痛苦。 本书写给谁看 读者阅读本书之前,应有一定的 B/S 应用开发基础。本书适用于正在使用 Ruby on Rails 进行应用开发的开发人员,渴望了解 Ruby on Rails 框架的开发人员。尤其适合 有初步的 Java EE 开发经验,想从 JavaEE 平台过渡到 Ruby on Rails 开发平台的开发 者。 本章要点 l 生成 Rails 应用 l 基本数据库连接配置 l 使用基本代码生成器 l 基本约定规则 l 设置中文响应 l 改进 Model l 改进控制器 l 改进视图页面 l 体验敏捷开发 前面已经介绍了 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 所示的运行界面。 Rails 所用的 Web 服 务器 图 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 应用的效果,我们先将 Rai ls 应用默认的英文响应改为中文响应。这很简单,只需在该应用的/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.r html 视图文件。 提示 RHTML 文件就是 Rails 应用里的视图页面,RHTML 页面的作用有点类似于 J ava 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 视图文件,新的代码如下。 ====第一个 Web 应用的标题====
第一个 Web 应用
<%= yield %>
上面的代码通过调用yield方法,将被装饰页面中的内容包含到这个装饰页面中来, 这样做的含义是:用户浏览任何页面时,实际看到的都是这个母版和被装饰页面组合输 出的结果。 接着,定义/app/views/user/register.rhtml 视图文件,代码如下。
<%= error_messages_for 'user' %>
<%= start_form_tag :action => 'register'%> <%= observe_field(:user_name, :frequency => 0.5, # 指定更新 update 元素 :update => :result, :url => { :action => :check_name }) %>
注册
用户名 <%= text_field("user", "name", :size => "20")%>
密码 <%= password_field("user", "password", :size => "20")%>
<%= submit_tag '提交' %>
<%= end_form_tag %>
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_nam e Action,这个 Action 将会返回用户输入的用户名是否已经被占用,并将返回的结果 通过 id 为 results 元素输出。 由于上面的 register.rhtml 视图文件中进行了 Ajax 调用,因而需要在/app/views/lay outs/ user.rhtml 文件中包含 JavaScript 库,使得系统支持 Ajax 调用。包含系统中 J avaScript 库的代码片段如下: <%= javascript_include_tag :defaults %> 将上面的代码添加到 user.rhtml 文件中的 head 标签内即可。 接下来,在 user_controller.rb 控制器文件中,将原来 scaffold 代码生成器生成的 cr eate 方法改为 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? %> <%=h @name %>用户名已经被注册,请重新选择一个! <% else %> <%=h @name %>用户名可用! <% 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 视图文件。其代码片段如下:
<% if @flash[:notice] -%>
<%= @flash[:notice] %>
<% end -%>
<% if @errors and not @errors.empty? then -%>
    <% for error in @errors %>
  • <%=h error %>
  • <% end %>
<% end -%>
<%= start_form_tag :action => 'login'%>
登录
用户名 <%= text_field("user", "name", :size => "20")%>
密码 <%= password_field("user", "password", :size => "20")%>
<%= submit_tag '提交' %>
<%= end_form_tag %>
如果您还有没有注册,请点<%= link_to("这里", :action => "regist er") %>注册新用户
上面的代码中,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,并重定向到 i ndex 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-s ection"的 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:30 00/message,将会看到可以列出所有留言信息的 list 页面。由于我们尚未添加任何留 言,因此该页面中没有任何留言记录。单击页面下方的 New message 链接,将会进入添 加留言的页面,如图 2.9 所示。 图 2.9 添加新留言 读者会注意到,图 2.9 中的 Picture data 域是一片空白,而 Picture content typ e 域是一个单行文本框,不是一个文件域。这是因为,我们的 messages 表中 picture 字段是定义成 varchar 类型的。所以,Rails 就会将它当成一个文本字段来处理,在模 板中将它设计成一个单行文本框。这只是视图模板,不用担心,因为在后面完善这个 fi rstApp 应用时,我们还要修改这里。 另外,由于 messages 表中定义了不允许为空的 user_id 字段,而添加留言的页面 中没有留言人的输入框(也没有必要有该输入框),同时,在 scaffold 为我们生成的 me ssage_controller.rb 控制器文件中,用于添加一条留言记录的 create Action 并没有 为 user_id 属性赋值,所以,这时候是不能成功地添加一条留言记录的。由此可看出, 尽管 scaffold 代码生成器的功能十分强大,但仅仅使用它也并不是万能的,这是肯定 的!它只是生成一个具有基本的 CRUD 操作的支架,不可能完成整个应用的所有功能。 这就需要程序员在 scaffold 生成的代码的基础上进行修改和完善,这也就是我们下面 几节将要介绍的内容。 2.3.2 改进“浏览留言” 我们希望当用户成功登录系统后,首先进入浏览留言页面,这需要修改 user_contr oller.rb 控制器文件中的 login 方法,将合法用户登录成功后重定向到 message 控制器 中的 list Action。代码片段如下: redirect_to :controller=>"message", :action=>"list" message 控制器的 list Action 负责列出系统中所有留言,scaffold 默认生成的 list Acti on 已经具有分页功能,因此我们不需要改变 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 文件。 其中,用于列出所有留言内容的代码片段如下: <% for message in @messages %> " > <% end %>
所有留言
标题 时间 是否有图 留言人  
<%= truncate(message.title, 25) %> <%= show_date(message.created_at) %> <% if message.has_picture? then %> 有图 <% end %> <%= message.user.name %> <%= link_to '查看', :action => 'show', :id => message %> <% if session[:user_id] == message.user.id then %> <%= link_to '删除', { :action => 'destroy', :id => message }, :conf irm => '您确定删除吗?', :post => true %> <% else %>   <% end %>
修改后的 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 对象的 pictur e_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 视图文件。修改后的代码如下:
<%= error_messages_for 'message' %>
<%= start_form_tag ({ :action => 'create', :id => @message }, :multip art => true) %>
新增留言
标题 <%= text_field('message', 'title', :size => "30")%>
内容 <%= text_area 'message', 'detail', "cols" => 40, "rows" => 10 %>
贴图 <%= file_field 'message', 'picture' %>
<%= submit_tag '提交' %>
<%= end_form_tag %>
上面的代码在 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 对象对应的数据行参照到该 Us er 实例对应的数据行。 在 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 =~ /^imag e/ 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 视图文件,修改后用于显示留言的代码 片段如下。

<%= @message.title %>

<%= @message.detail %>

<% if @message.has_picture? then %> @message.id) %>"/> <% else %> 无图片 <% end %>

<%= show_date(@message.created_at)%>

<%= @message.user.name %>


<%= link_to '返回', :action => 'list' %>
上面的代码中,Message 对象调用 has_picture?方法来查询留言是否有图片(has_ picture?方法定义的代码已经在前面 2.3.2 节中介绍过)。如果留言有图片,则调用 Rai ls 提供的 url_for 帮助方法,生成一个将会向 show_picture Action 发送请求的 URL。 然后通过这个 Action 来负责显示图片。 在 message_controller.rb 控制器文件中,添加 show_picture Action 的定义,该 Acti on 负责把数据库里保存的图片二进制数据转换成图片输出。该 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 平台的开发过程,因此大量使用了后面的知识点,读者可 以参考后面章节的知识来阅读本章内容。后面章节将会细致介绍本章所使用的各知识 点。 本章要点 l 电子邮件的基础知识 l SMTP 协议初步 l POP3 协议初步 l 在 Ruby 程序中使用 SMTP 支持发送邮件 l 在 Ruby 程序中使用 POP3 支持接收邮件 l ActionMailer::Base 的邮件支持 l 在 Rails 应用中发送邮件 l 发送 HTML 格式邮件 l 发送带附件的邮件 l 电子邮件在实际应用中的用途 l 账户需要激活的优势 l 实现注册激活系统 15.4 注册激活系统 本章将介绍一个注册激活系统,当用户输入注册信息时,必须输入有效的邮箱,当 用户注册成功后,并不能立即登录系统,而要登录注册时输入的邮箱,然后通过该邮箱 内的激活邮件来激活自己账户。通过这种方式可以防止用户恶意注册,保证用户必须输 入有效的邮箱地址。 15.4.1 基本注册功能 当用户注册一个账户时,实际上就等同于向底层数据表增加一条记录,因此本应用必须 有持久层支持。本示例应用的注册功能需要一个数据表支持,该数据表用于保存系统的 所有注册账户。下面是本应用的数据库脚本: drop database if exists regist_development; create database regist_development; use regist_development; --创建用户表 create table users ( id int not null auto_increment, name varchar(100) not null unique, pass varchar(100) not null, email varchar(150) not null, active_code varchar(255), is_activated boolean, primary key (id) ); 上面创建了一个 users 数据表,该数据表里保存了用户名、密码、E-mail、激活码 和是否激活等 5 个数据列。前三个数据列的作用非常清晰,此处不再赘述。激活码列用 于保存用户刚刚注册时系统生成的注册码,这个注册码将通过邮件发送给用户,系统正 是通过比较用户输入的注册码和这个注册码是否相等,从而决定是否可以激活用户。 上面的 users 数据表对应的持久化类是 User,这个持久化类的代码非常简单,只是增加 了一些 Model 校验功能。下面是 User 类的代码: class User < ActiveRecord::Base def validate #验证 name 不能为空 errors.add("", "用户名只能是字母、数字或下划线,且长度必须为 4 到 20 位") unless name=~ /^\w{4,20}$/ #验证 name 不能是一个数据库中已经存在的名字 errors.add("", "用户名不能重复,您选择的用户名已经存在") unless User.find_by_name(name).nil? #验证 password 不能为空 errors.add("", "密码只能是字母或数字,且长度必须为 4 到 20 位") unless pass =~ /^[a-zA-Z0-9]{4,20}$/ #验证 e-mail 规则 errors.add("", "电子邮件必须匹配电子邮件规则") unless email =~ /^\w+@\w+.[a-zA-Z]{2,6}$/ end end 提供了上面的 User Model 之后,处理用户注册就非常简单了,只需要提供一个注册表单, 这个表单里包含用户注册的基本信息即可。下面是用户注册的表单页代码: 请输入您的注册信息
注意:请务必输入有效的邮箱用于接收激活邮件
<%= error_messages_for :user %> <% form_for :user, @user, :url => { :action => "pro_regist" } do |f| %> 用户名:<%= f.text_field :name %>
密 码:<%= f.password_field :pass %>
Email:<%= f.text_field :email %>
<%= submit_tag("注册")%> <% end %> 在浏览器中浏览该页面,看到如图 15.8 所示的表单页效果。 图 15.8 注册表单 当用户单击如图 15.8 所示的“注册”按钮时,将会把图 15.8 所示的表单提交到 pro_re gist Action。这个 Action 也非常简单,它只需调用该 User 类的 create 方法向 users 数据表中增加一条记录即可。下面是处理用户注册的 Action 代码: # 处理用户登录 def pro_regist #创建一个新的 user 对象 @user = User.new(params[:user]) # 以当前时间来随机生成激活码 @user.active_code = rand(Time.now.to_i).to_s # 设置用户开始处于未激活状态 @user.is_activated = false #如果 user 对象能成功地保存进数据库 if @user.save then flash[:notice] = '您已经注册成功' flash[:name] = @user.name # 发送邮件 ActivateMail.deliver_sent(@user) redirect_to :action => 'success' # 保存失败 else render :action => 'index' end end 上面的 Action 方法的实现与普通注册 Action 代码大致相似,不同的是上面的 Acti on 代码需要调用 ActivateMail.deliver_sent(@user)代码来发送邮件。这行代码调用 A ctivateMail Model 里的 sent 方法来发送激活邮件,这也就是本应用实现用户激活的重 点。 15.4.2 发送激活邮件 本应用需要向注册用户发送激活邮件,对于 Rails 应用而言,激活邮件也就是一封最普 通的邮件,因此我们像开发普通邮件模块一样来实现发送激活邮件。同样,我们先使用 Rails 提供的邮件代码生成器来生成一个邮件模块。进入 Rails 应用的根路径下,输入 如下命令: # 生成 ActivateMail 邮件 Model,并提供一个 sent 方法 ruby script/generate mail ActivateMail sent 上面的代码生成器命令会在 app/models 路径下生成一个 activate_mail.rb 文件,这个文件 就是邮件发送 Model,该 Model 里定义了一个 sent 方法,该方法就是发送邮件的业务逻辑 方法。修改上面的 sent 方法,让该 sent 方法实现发送激活邮件的功能。修改后的 sent 方 法代码如下: # 定义发送邮件的业务方法 def sent(user) # 指定邮件标题 @subject = '激活邮件' # 将新注册的 User 实例传给邮件内容模板 @body = {'user'=>user} # 使用用户的注册邮件作为收件人地址 @recipients = user.email # 使用 spring_test@sina.com 作为发件人地址 @from = 'spring_test@sina.com' @sent_on = Time.now @headers = {} # 指定使用 HTML 格式的邮件 @content_type = 'text/html' end 与前面完全类似的是,我们一样采用 spring_test@sina.com 作为发件人地址,这就要求我 们必须在 config 路径下的 environment.rb 文件中配置 sina 的邮件服务器。在 environmen t.rb 文件中增加如下代码: ActionMailer::Base.delivery_method = :smtp ActionMailer::Base.server_settings = { :address => 'smtp.sina.com', :port => 25, :domain => 'sina.com', :user_name => 'spring_test@sina.com', :password => '123456', :authentication => :login } ActionMailer::Base.default_charset = 'GBK' 经过上面步骤,我们已经实现了发送激活邮件的大部分功能。还有一个必须完成的 地方:我们定义 sent(user)方法时,该方法为@body 赋值的是一个 Hash 对象,这意味 着我们还需要为该方法指定一个邮件模板。 当我们执行 ruby script/generate mail ActivateMail sent 命令时,该命令还在 app\ views\activate_mail 路径下生成了一个 sent.rhtml 文件,这个模板文件的响应将作为 ActivateMail.sent(user)方法发送的邮件内容。sent.rhtml 模板文件的代码如下:

<%= @user.name%>,您好!

请在浏览器的地址栏中输入如下地址来激活您的账户:
http://localhost:3000/regist/pro_activate?name=<%= @user.name%>&active_code = <%= @user.active_code%> 上面的邮件模板是一个非常简单的页面,这个页面包含了一个简单的超级链接,这 个超级链接将向服务器发送两个请求参数:用户名和验证码。 如果用户注册结束,登录注册时所输入的邮箱,将可以看到如图 15.9 所示的界面。 图 15.9 激活邮件 用户只要单击如图 15.9 中的账户激活链接,就可以激活刚刚注册的账户。在还未 激活用户账户之前,我们打开 users 数据表,看到用户刚刚注册的账户信息处于如图 15. 10 所示的状态。 图 15.10 未激活的用户账户 当用户账户被激活后,图 15.10 中 is_activated 列的值将变成 1,这表明用户账户 已被激活。 15.4.3 处理激活 用户单击激活邮件中的链接,相当于向系统的 regist 控制器的 pro_activate Action 发送一个请求,并将用户名和激活码作为请求参数,Rails 应用将根据这两个请求参数 来决定是否激活用户账户。下面是 pro_activate Action 的代码: # 处理用户激活 def pro_activate user = User.find_by_name(params[:name]) # 如果用户不为空,且用户处于未激活状态,且用户输入的激活码正确 if user != nil && user.is_activated == false && user.active_code == param s[:active_code] then # 修改激活状态 user.update_attribute(:is_activated , true) flash[:notice] = "恭喜您,您已经成功激活了您的账户!" # 如果用户已经处于激活状态 elsif user != nil && user.is_activated == true then flash[:notice] = "您的账户已经处于激活状态,请勿重复激活!" else flash[:notice] = "激活失败!" end redirect_to :action=>'activate_result' end 上面 Action 先根据发送的 name 请求参数取出 users 表中对应的用户账户,然后判 断取出的账户里包含的激活码与用户请求中包含的激活码请求参数是否相同。如果两个 激活码相同,而且用户还未激活,系统将把用户账户的 is_activated 属性修改为 true, 即完成用户激活。如果用户的 is_activated 状态已经是 true,则提醒用户不用重复激 活账户。 当用户注册成功,并且通过如图 15.9 所示的激活链接来激活账户后,就可以看到 如图 15.11 所示的界面。 图 15.11 激活成功 一旦成功激活了用户账户,也就是将 users 数据表中对应记录行的 is_activated 数据列的值修改为 1。 15.4.4 处理登录 增加了激活功能后的用户登录,也增加了额外的处理,除了需要判断用户名和密码是否 匹配之外,还必须判断用户账户是否处于激活状态。下面是处理用户登录的 Action 代 码: # 处理用户登录 def pro_login # 根据用户名和密码查询用户 user = User.find_by_name_and_pass(params[:user][:name] , params[:user][:p ass]) # 如果找不到对应用户 if user == nil then render :text =>'

您输入的用户名和密码不匹配

' , :layout=>true # 如果用户处于未激活状态 elsif user != nil && user.is_activated == false render :text =>'

您的账户还未激活,请先激活您的账户

' , :layout=> true # 成功登录 else render :text =>'

您已经成功登录

' , :layout=>true end end 从上面的 Action 代码中可以看出,如果用户输入的用户名和密码匹配,但用户还 处于未激活状态下时,系统将提示用户账户还未激活,将看到如图 15.12 所示界面。 图 15.12 账户还未激活 通过上面示例,我们示范了电子邮件在实际应用中的用途:通过电子邮件来激活账 户,可以避免用户注册账户时随意输入电子邮件地址,可以防止恶意注册。 15.5 本章小结 本章主要介绍了 Rails 应用的邮件支持。本章从电子邮件的基础知识开始讲起,简 单地介绍了电子邮件的发展和用途,以及 SMTP 协议和 POP3 协议的初步知识,并通过示 例示范了如何在 Ruby 语言中发送和接收邮件。本章重点介绍了 Rails 的邮件支持,包 含如何定义 Rails 邮件系统配置,如何发送 HTML 格式邮件,如何发送带附件的邮件。 本章的最后还通过一个注册激活系统来示范了电子邮件在实际应用中的用途。 本章要点 l Ajax 简介 l Ajax 技术对传统 Web 技术的改进 l Ajax 技术的关键技术 l Ajax 技术的原理和流程 l 在 Rails 应用中手动实现 Ajax 应用 l 发送异步请求 l 异步方式提交表单 l 周期性地发送异步请求 l 使用 Observer l 直接执行服务器响应脚本 l 使用自动完成 l 自定义发送异步请求的时机 l 实现拖动效果 l 实现排序效果 l 各种视觉效果 Ajax 技术极好地改进了传统 Web 开发技术,通过使用 Ajax 技术,允许客户 端异步地向服务器发送请求,当服务器响应到达客户端时,客户端页面通过 JavaScript 自动把服务器响应更新在页面中。通过使用 Ajax 技术,会极好地改 善用户的感觉,用户可以持续发送异步请求。当用户发送请求后,无须等待服务 器返回,因为原来的页面内容还存在,用户可以继续前面的操作。当服务器响应 到达客户端时,JavaScript 负责将服务器响应加载在页面中。 Ajax 技术是如此有用,因此 Rails 框架也提供了极好的 Ajax 支持,Rails 框架的 Ajax 支持以 Prototype 和 Script.aculo.us 为基础。由于 Prototype 和 Script.aculo.us 是如此优秀,以至于它们后来独立成为了优秀的 Ajax 函数库。 尤其是 Prototype,只要是进行 Ajax 开发的开发者,几乎都为它的小巧易用而 折服。而 Rails 则对 Prototype 和 Script.aculo.us 做了进一步封装,更加简化 了 Ajax 开发。 通过 Rails 的 Ajax 支持,开发者大部分时候无须编写任何 JavaScript 代码, 即可完成优秀的 Ajax 效果。Rails 为大部分 Ajax 应用场景都提供了极好的封装, 开发者只需调用 Rails 的 Ajax 方法,即足以应付大部分 Ajax 场景。 16.5 Ajax 的效果模块:ScriptaculousHelper 前面已经介绍了 Rails 的 Ajax 主要由 Prototype 和 Scriptaculous 组成, 前面介绍的 Ajax 核心模块就是对 Prototype 的封装。接下来介绍的 ScriptaculousHelper 则是对 Scriptaculous 的封装,通过它来实现 Rails 的富 客户端效果。 16.5.1 拖动效果 通过 ScriptaculousHelper 模块的帮助,允许我们在页面中通过拖动来实现 Ajax 交互。Rails 用以实现拖动效果主要有如下两个方法。 ● draggable_element:该方法将某个 HTML 元素定义成一个可以拖动的元 素。 ● drop_receiving_element:该方法将某个 HTML 元素定义成一个拖动目的 地元素。将一个可拖动元素(使用 draggable_element 定义)拖动到拖动目的地 元素(使用 drop_receiving_element 定义)时,Rails 将会向服务器发送 Ajax 请求。 draggable_element 方法的用法很简单,它将一个 HTML 元素定义成一个自由拖 动的元素。该方法的语法格式如下: draggable_element(element_id, options = {}) 该方法将 element_id 元素定义成一个可自由拖动的元素,其中 options 用 于定义各种拖动选项,其中最常用的选项就是:revert,该选项控制拖动一个元 素后该元素是否会回到原来的地方。 下面程序示范了使用 draggable_element 方法来定义可拖动 HTML 元素。

拖动示例

<%= image_tag("vista.jpg" , :id =>"image1")%> <%= image_tag("vista.jpg" , :id =>"image2")%> <%= draggable_element("image1", :revert => true)%> <%= draggable_element("image2", :revert => false)%> 上面的代码两次使用 draggable_element 定义了 2 张可拖动图片,其中一张 图片被拖动后不会回到原来的地方,而另一张图片则会回到原来的地方,效果如 图 16.16 所示。 图 16.16 可拖动效果 这种拖动效果仅仅只是一种视觉上的改变,还未实现任何实质(系统后台) 的改变,因为我们的拖动未发送任何 Ajax 请求。如果我们结合 drop_receiving_element 方法,将可以实现更强大的功能。 使用 drop_receiving_element 方法的语法格式如下: drop_receiving_element (element_id, options = {}) 该方法将 element_id 所指定的 HTML 元素定义成一个可拖动的目的地。因为 该方法会发送 Ajax 选项,因此可以接受如下几个选项。 ● :url:该选项指定一个符合 url_for 格式的 URL 地址。 ● :update:该选项指定使用服务器响应来更新哪个 HTML 元素。 除此之外,当然也可指定:complete、:loaded 等 Ajax 交互阶段的特定选项。 下面将使用 draggable_element 和 drop_receiving_element 实现一个购物 车效果。 进入购物车页面时,先经过如下 Action 处理,该 Action 创建了一个字符串数组 作为实例变量。 # 定义进入购物车的 Action def cart # 如果 session[:cart]为 nil,则将其赋为{},否则不改变 session[:cart] ||= {} # 创建一个字符串数组 @books = %w{spring ajax struts2} end 在上面的 Action 中定义了用户的:cart Session,该 Session 用以跟踪用户 的购物状态。还手动构建了一个字符串数组,这个字符串数组将作为系统的商品。 提示 实际应用中我们可以通过 Model 从数据库中取出所有商品,但 本应用为了简化应用的复杂度,只突出 Ajax 应用,故没有使用 Model 进行持久化访问。 视图页面将使用 draggable_element 方法和 drop_receiving_element 方法来构 建通过拖动来购买商品的应用。cart.rhtml 页面的代码如下:

拖动效果

下面是所有商品

<%@books.each do |book|%> <%= image_tag book + ".jpg", :id =>book %> <%= draggable_element(book, :revert => true)%> <%end%>

将上面商品拖到下面方框中就是购买了该商品

<%= drop_receiving_element("my_cart", :url => {:action => "add" }, :update=>"my_cart") %> 上面页面会迭代输出@books 实例变量中每个元素,并将每个元素都转换成一 个可拖动的元素;除此之外,还使用 drop_receiving_element 方法定义了一个 拖动目的地元素,每当元素被拖动到该元素时,Rails 将向 add Action 发送请 求,并发送被拖动元素的 id 作为请求参数。 下面是 add Action 的代码: # 定义添加物品的 Action def add # 取得请求参数 product_id = params[:id]; # 如果请求参数不为空 if product_id != nil then # 如果购物车中的物品已经存在,则数量加 1,否则设置该物品数量为 1 session[:cart][product_id] = session[:cart].include?(product_id) ? session[:cart][product_id] + 1 : 1 end # 使用_cart.rhtml 页面来生成响应 render :partial => 'cart' end 从上面的 Action 代码来看,Rails 拖动时会将被拖动元素的 id 作为请求参数发 送到服务器,上面的 Action 就可以获取用户希望购买的物品,从而可以修改用 户的 session 状态。该 Action 最后提交到_cart.rhtml 页面来显示处理结果。 该页面的代码如下: <% session[:cart].each do |product,quantity| %>
<% quantity.times do |i| %> <%= image_tag product + ".jpg", :id => "item_#{product}_#{i}", :size => "40x60", :style => "position:relative;" %> <% end %> <%= product %>:<%= quantity %>
<% end %> <%= "您暂时还未购买任何图书!" if session[:cart].empty? %> 上面的_cart.rhtml 页面生成的响应将会用来更新原来页面的 my_cart 元 素,从而可以通过拖动来购买商品,图 16.17 是上面页面的购物车效果。 图 16.17 基于拖动的购物车效果 上面应用的界面更加友好:浏览者可以直接通过拖动鼠标来购买商品,这是 很有乐趣的事情。 值得指出的,如果希望加载该页面时即显示用户的购物车中有多少商品,应 该在用户进入该页面时立即向 add Action 发送请求(add Action 已经处理了没 有请求参数的情形)。上面应用已经实现了该功能,我们在 body 元素 load 时通 过 remote_function 来发送请求,从而实现了页面加载时立即发送请求的要求。 16.5.2 使用 sortable_element 排列列表项 sortable_element 是另一个可支持 Ajax 交互的方法,这个方法把页面中多 个列表项定义成可上下拖动的元素,通过上下拖动这些 HTML 元素,从而允许手 动对这些列表项进行排序。当用户拖动完成后,Rails 会将排序后的元素 id 以 异步方式发送到服务器。 使用 sortable_element 方法的语法格式如下: sortable_element(element_id, options = {}) 该方法将会把 element_id 属性对应的 HTML 元素里所有的列表项变成可排序 的列表项。因为该方法会发送 Ajax 选项,因此可以接受如下几个选项。 ● :url:该选项指定一个符合 url_for 格式的 URL 地址。 ● :update:该选项指定使用服务器响应来更新哪个 HTML 元素。 除此之外,当然也可指定:complete、:loaded 等 Ajax 交互阶段的特定选项。 下面的页面代码将会使用 sortable_element 方法来把系列列表项转成可排序的 列表项。页面代码如下:

可重新排序的列表项

<% @books.each do |book| %>
  • <%= book %>
  • <% end %>

    <%= sortable_element 'list', # 使用服务器响应来更新 list-info 元素 :update => 'list-info', # Ajax 交互完成后高亮显示 :complete => visual_effect(:highlight, 'list'), :url => { :action => "order" } %> 上面页面代码迭代输出@books 实例变量,该实例变量是一个数组,每个数组元 素对应生成一个列表项。除此之外,该页面还使用 sortable_element 将 list 所有的列表项定义成可重新排序的列表项。当用户每次拖动这些列表项重新排序 时,都会向 order Action 发送异步请求。order Action 的代码如下: def order # 被调整顺序后的列表项会作为 list 参数发送到控制器 @order = params[:list] # 提交到 list 页面显示处理结果 render :partial => 'list' end 上面方法中定义了一个实例变量,这个实例变量里封装的是请求参数 list 的值, 然后将处理结果提交到_list.rhtml 页面显示。_list.rhtml 页面的代码如下: 更新后的顺序是: <%= @order.join(', ') %>. _list.rhtml 页面的代码非常简单,仅仅只是输出了控制器中的@order 实例 变量而已。 在浏览器中拖动页面元素,看到如图 16.18 所示的效果。 图 16.18 可排序列表项的效果 正如上面的代码中看到的,因为 sort Action 实际上并未对请求进行任何处 理,仅仅只是将获得的请求参数提交到_list.rhtml 页面输出,因此我们可以在 图 16.18 中看到请求参数。从图中可以看出:sort Action 取得的请求参数正是 列表项排序后的 id 值。 值得指出的是,上面页面在生成列表项时,使用了这样的代码:
  • <%= book %>
  • 。不难发现,每个列表项的 id 都是 item_XXX的形式,这是Rails的要求,它要求可排序列表项的id值满足item_XXX 的格式。当用户拖动列表项重新排序时,系统将把排序后所有列表项的 id 作为 数组一起发送到服务器,但会去掉前面的 item_前缀。 注意 使用 sortable_element 来生成可排序的列表项时,别忘了让所 有列表项的 id 为 item_XXX 的格式。 16.5.3 视觉效果 ScriptaculousHelper 模块提供了一个 visual_effect 方法来实现各种视觉 效果,这些视觉效果并没有任何 Ajax 功能,但它们可以使人机界面更加友好。 实际上,Rails 的视觉效果是依赖于 Scriptaculous 的视觉效果,因此我们 也可以直接使用 Scriptaculous 来实现这些视觉效果。 使用 visual_effect 方法的语法格式如下: visual_effect(name, element_id = false, js_options = {}) 该方法中的 name 为视觉效果的名称,这个名称见表 16.1;element_id 指定 该方法对哪个元素应用视觉效果,这个元素可以省略,如果该元素省略,需把该 方法直接指定到某个元素,指定对该元素应用该视觉效果;js_options 指定该 视觉效果的选项。 如果我们不想通过 Rails 方法,而是直接通过 Scriptaculous 来指定某个元 素的视觉效果,也是可行的。因为直接使用 Scriptaculous 指定视觉效果属于 JavaScript 代码,因此应该将其放在