Rails-速成课-全-
Rails 速成课(全)
原文:
zh.annas-archive.org/md5/64fa2cf2639b7305013c0f9f4cbfa2aa译者:飞龙
前言
Ruby on Rails 框架强调开发者的生产力,使得曾经需要数月才能完成的网站,如今可以在几周甚至几天内实现!感谢 Ruby 编程语言以及诸如约定优于配置和避免重复自己等原则,Rails 开发者可以花更少的时间来配置应用程序,更多的时间用于编写代码。
Ruby on Rails 还是一个全栈网页框架,这意味着它处理从访问数据库中的数据到在浏览器中渲染网页的所有内容。作为一个全栈框架,Rails 由看似无尽的不同组件组成,例如 Active Record、资产管道、CoffeeScript、Sass、jQuery、turbolinks 以及各种测试框架。
本书旨在简化内容,准确解释你需要了解的所有知识,帮助你开发自己的 Ruby on Rails 应用程序。在你掌握 Rails 基础知识后,我将根据需要介绍和解释框架的新组件。
到最后,你将学会如何从零开始构建自己的 Rails 应用程序。你将添加测试来确保功能按预期工作,保护你的应用程序和用户免受安全漏洞的威胁,优化应用程序的性能,并最终将应用程序部署到自己的服务器上。
本书适用人群
我假设你在开始本书之前已经有一定的网页开发经验。你应该熟悉 HTML 和 CSS。你应该知道什么是 H1 元素,以及如何将图像和链接添加到网页中。了解面向对象编程的一些知识是有帮助的,但不是必需的。
你将使用计算机的终端(或命令提示符)输入命令,但你无需有太多终端命令的经验也能跟随示例进行操作。除了终端,你还需要一个文本编辑器来编写 Ruby 代码。许多 Rails 开发者使用复古编辑器,如 Vim 或 Emacs。
如果你还没有自己偏好的文本编辑器,我推荐 Sublime Text。你可以在 www.sublimetext.com/ 上找到 Sublime Text 的免费试用版。免费试用版没有到期限制,但偶尔会提示你购买许可证。
概述
本书分为两部分。第一部分介绍 Ruby 语言和 Ruby on Rails 框架的基础知识。第二部分则介绍 Ruby 和 Ruby on Rails 中的高级主题。每章末尾都有练习题,答案会在本书末尾提供。
第一章介绍了 Ruby 的基础知识,包括数据类型、控制流、方法和类。
第二章介绍了 Ruby on Rails 的基础知识。内容包括 Rails 原则、Rails 应用程序使用的目录结构和常见的 Rails 命令。在本章结束时,你将创建你的第一个 Rails 应用程序!
第三章、第四章 和 第五章 讲解了 Rails 使用的模型-视图-控制器架构的三个部分。
第六章 讲解了如何创建 Git 仓库来存储你的应用程序,并使用 Heroku 将应用程序部署到网络上。
一旦你掌握了 Ruby 和 Ruby on Rails 的基础知识,你就可以进入更高级的话题。
第七章 讲解了 Ruby 模块、Ruby 对象模型,甚至还有一些元编程内容。
第八章 介绍了更高级的 Active Record 关联。在本章结束时,你还将构建一个新应用程序的数据模型。
第九章 讲解了你新应用程序使用的认证系统。该系统允许用户注册账户、登录应用程序并登出。
第十章 讲解了使用 Ruby 随附的 MiniTest 框架对应用程序的各个部分进行自动化测试。本章还讨论了测试驱动开发。
第十一章 介绍了常见的 web 应用程序安全漏洞,并解释了如何确保你的应用程序是安全的。
第十二章 介绍了 Rails 应用程序的性能优化。内容包括 Rails 内置的优化功能、SQL 查询优化和缓存。
第十三章 介绍了几种追踪 bug 的方法。学习如何添加应用程序生成的日志文件,并如何使用交互式调试器来解决真正棘手的 bug。
第十四章 解释了如何使用 GitHub API,并讲解了为你的应用程序创建自己的 API 的过程。
最后,第十五章 解释了如何在 Amazon 云上设置自己的服务器,并使用 Capistrano 部署你的应用程序。
安装
要跟随本书中的示例并完成练习,你需要 Ruby 编程语言、Ruby on Rails 框架、Git 版本控制系统以及 Heroku Toolbelt。
Ruby 语言官网提供了安装说明,链接在 www.ruby-lang.org/en/installation/。Rails 被分发为一组 Ruby gems,你可以通过单个命令下载和安装,具体取决于你的操作系统。(Ruby on Rails 官网也提供了安装说明,链接在 rubyonrails.org/download/。)你可以在 git-scm.com/downloads/ 下载 Git。
安装完 Ruby、Rails 和 Git 后,安装最新版本的 Heroku Toolbelt,用于将你的应用程序部署到 Heroku。下载 Heroku Toolbelt 安装程序,链接在* toolbelt.heroku.com/*,然后按照那里提供的指示完成安装。
Ruby、Rails 和 Git
以下部分包含了在 Mac OS X、Linux 和 Windows 上安装 Ruby、Rails 和 Git 的详细说明。如果你使用的是 Mac OS X 或 Linux,还可以参考多个 Ruby 版本,这是一种替代的安装 Ruby 的方式。Windows 上有一个名为 pik 的工具可以管理多个 Ruby 版本,但自 2012 年以来该工具未再更新,因此我在此不作介绍。
Mac OS X
通过ruby --version检查你当前的 Ruby 版本。如果你使用的是 Mac OS X Mavericks,你应该已经安装了 Ruby 2.0.0 版本。否则,你需要安装一个更新的版本。
即使你已经有了 Ruby 2.0.0,我仍然推荐在 Mac OS X 上使用 Homebrew 包管理器。Homebrew 是一种在 Mac OS X 上安装和更新常用开发工具的简便方式。有关下载和安装 Homebrew 的说明可以在* brew.sh/*上找到。安装 Homebrew 后,打开终端并输入命令brew install ruby来安装最新版本的 Ruby。
接下来,使用命令gem install rails安装 Ruby on Rails。然后再次使用 Homebrew 安装 Git,输入命令brew install git。
Linux
Linux 的安装说明根据你使用的 Linux 发行版有所不同。首先,检查你的包管理器,它可能已经有最新版本的 Ruby。如果有,只需像安装其他软件包一样安装它。
如果没有,你需要从源代码安装 Ruby。从* www.ruby-lang.org/en/downloads/*下载当前稳定版本。解压文件后,在终端中输入以下命令:
$ **./configure**
$ **make**
$ **sudo make install**
安装完成后,通过输入命令sudo gem install rails来安装 Ruby on Rails。
每个 Linux 发行版都包含 Git。如果你的系统中没有安装 Git,可以使用包管理器安装它。
Windows
你将使用 RubyInstaller 来安装 Ruby。下载 RubyInstaller 和相应的开发工具包,链接在* rubyinstaller.org/downloads/*。
首先,点击 RubyInstaller 下载页面上的最新 Ruby 版本进行下载;在写这篇文章时,是 2.1.5。然后向下滚动到“Development Kit”部分,点击你所选 Ruby 版本下的链接下载开发工具包。以 Ruby 2.1 为例,你需要选择 DevKit-mingw64-32-4.7.2-20130224-1151-sfx.exe。如果你使用的是 64 位版本的 Windows,则需要下载 64 位版本的安装程序和相应的 64 位开发工具包,当前版本是 DevKit-mingw64-64-4.7.2-20130224-1151-sfx.exe。
下载完成后,双击 RubyInstaller 文件,然后按照屏幕上的提示完成 Ruby 的安装。确保勾选 Add Ruby executables to your PATH 选项。完成后,双击 DevKit 文件并输入路径 C:\DevKit 来解压文件。现在打开命令提示符并输入以下命令以安装开发工具包:
$ cd C:\DevKit
$ ruby dk.rb init
$ ruby dk.rb install
有些用户在尝试安装 gems 时会遇到 SSL 错误。更新到最新版本的 gem 命令可以修复这些错误。输入以下命令来更新你的 gem 版本:
$ gem update --system –-clear-sources –-source http://rubygems.org
安装 Ruby 和开发工具包后,通过输入 gem install rails 来安装 Rails。这将连接到 RubyGems 服务器,下载并安装构成 Ruby on Rails 框架的各种包。
最后,下载最新版本的 Git 并双击文件完成安装。
注意
注意:在本书中,我要求你输入一些命令,如 bin/rake 和 bin/rails。这些命令在 Windows 上不起作用。Windows 用户请在这些命令前添加 ruby。例如,你需要输入 ruby bin/rake 和 ruby bin/rails。
多个 Ruby 版本
存在一些第三方工具,可以方便地在单台计算机上安装和管理多个版本的 Ruby。如果你维护多个不同的应用程序,或者想在不同版本的 Ruby 上测试一个应用程序,这将非常有用。
Ruby on Rails 网站推荐使用 rbenv 和 ruby-build 插件来管理 Ruby 的安装。rbenv 命令用于在 Ruby 版本之间切换,而 ruby-build 提供了 rbenv install 命令,用于安装不同版本的 Ruby。
安装 rbenv
如果你使用的是 Mac OS X,rbenv 和 ruby-build 都可以通过 Homebrew 安装。有关安装 Homebrew 的说明,请访问 brew.sh/。
打开终端,输入 brew install rbenv ruby-build,然后跳转到 安装 Ruby。
在 Linux 上,通过从 GitHub 克隆代码来安装 rbenv 和 ruby-build,如下所示。完整的安装说明可以在线查看,地址为 github.com/sstephenson/rbenv/。
首先,确保你已安装适当的开发工具。ruby-build的 wiki 页面* github.com/sstephenson/ruby-build/wiki/*包含了大多数流行 Linux 发行版的建议构建环境。例如,在 Ubuntu 上,输入以下命令来安装编译 Ruby 所需的一切。
$ **sudo apt-get install autoconf bison build-essential git \**
**libssl-dev libyaml-dev libreadline6 \**
**libreadline6-dev zlib1g zlib1g-dev**
Reading package lists... Done
Building dependency tree
--*snip*--
Do you want to continue? [Y/n]
输入字母y来安装这些包,然后按回车。其他 Linux 发行版所需的包可以在上面的 wiki 页面中找到。
接下来,输入以下命令将rbenv的 git 仓库克隆到你的主目录。
$ **git clone https://github.com/sstephenson/rbenv.git ~/.rbenv**
Cloning into '/home/ubuntu/.rbenv'...
--*snip*--
Checking connectivity... done.
然后,将~/.rbenv/bin目录添加到你的$PATH中,并在.bashrc文件中添加一行,以便每次登录时初始化rbenv。
$ **echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc**
$ **echo 'eval "$(rbenv init -)"' >> ~/.bashrc**
$ **source ~/.bashrc**
最后,通过将rbenv插件目录克隆到ruby-build的 git 仓库来安装ruby-build,可以使用以下命令。
$ **git clone https://github.com/sstephenson/ruby-build.git \**
**~/.rbenv/plugins/ruby-build**
Cloning into '/home/ubuntu/.rbenv/plugins/ruby-build'...
--*snip*--
Checking connectivity... done.
一旦安装了rbenv和ruby-build,你就可以安装 Ruby 了。
安装 Ruby
输入命令rbenv install -l来列出当前可用的 Ruby 版本。
$ **rbenv install -l**
Available versions:
1.8.6-p383
1.8.6-p420
1.8.7-p249
1.8.7-p302
--*snip*--
忽略以jruby、rbx和ree等词开头的版本。现在只需关注版本号。截止本文写作时,最新版本是 2.1.1。如果在你安装 rbenv 时有更新的版本,请将下面命令中的 2.1.1 替换为正确的版本号。
$ **rbenv install 2.1.1**
Downloading yaml-0.1.6.tar.gz...
--*snip*--
Installed ruby-2.1.1 to /home/ubuntu/.rbenv/versions/2.1.1
一旦完成,输入rbenv global 2.1.1来设置系统的全局默认 Ruby 版本。现在通过输入gem install rails安装 Ruby on Rails。最后,输入rbenv rehash来更新rbenv。你可以在rbenv官网* github.com/sstephenson/rbenv/*了解更多关于rbenv如何切换 Ruby 版本的内容。
第一部分:Ruby on Rails 基础知识
第一章 Ruby 基础
1993 年,松本行弘(Yukihiro “Matz” Matsumoto)将他喜欢的几种语言(Perl、Smalltalk、Eiffel、Ada 和 Lisp)的部分特点结合起来,创造了他理想中的语言——Ruby。
Ruby 是一种动态的面向对象编程语言,同时也支持命令式和函数式编程风格。它注重简洁、生产力和开发者的愉悦感。Ruby 网站将其称为“程序员的最佳朋友”,而有其他语言经验的开发者通常会发现 Ruby 容易编写且自然易读。
扎实的 Ruby 基础对于理解 Ruby on Rails 至关重要,因此本章将介绍 Ruby 的基础知识。在我们逐步讲解语言特性时,我将展示一些经验丰富的 Ruby 开发人员常用的习惯用法,以便您以后能够在自己的程序中使用它们。
交互式 Ruby
我探索 Ruby 语言的最喜欢方式是通过交互式 Ruby 解释器(IRB)。大多数时候,我在文本编辑器中开发应用程序,但我仍然保持一个 IRB 会话以快速测试想法。
要启动 IRB,打开一个终端(或 Windows 上的命令提示符),输入irb,然后按 ENTER。您应该会看到类似于以下的提示符:
irb(main):001:0>
如果在输入irb后看到错误消息,可能是因为您没有安装 IRB。请查看引言并按照 Ruby 的安装说明来设置 IRB。
IRB 是一种名为读取-评估-打印循环(REPL)的程序。IRB 读取您的输入,评估它并显示结果。它会重复这个过程,直到您按 CTRL-D 或输入quit或exit。
通过输入几个被引号包围的单词来试试 IRB:
irb(main):001:0> **"Hello, Ruby"**
=> "Hello, Ruby"
Ruby 会评估您输入的表达式并显示结果。一个简单的字符串会评估为它本身,但这与打印字符串不同。要在屏幕上输出内容,可以使用 Ruby 方法puts,如下面所示:
irb(main):002:0> **puts "Hello, Ruby"**
Hello, Ruby
=> nil
现在,Ruby 将字符串输出到屏幕,并显示nil,这是评估puts方法的结果。在 Ruby 中,每个方法都会返回一个值。puts方法没有任何有用的返回值,因此返回nil。
在本章的其余部分中,您将看到更多的示例,您可以将其输入到 IRB 中。我鼓励您尝试这些示例,探索您可以在 IRB 和 Ruby 中做什么。
注意
如果 IRB 停止评估您输入的内容,可能是因为您忘记了闭合的引号或其他预期的语法。遇到这种情况时,按 CTRL-C 取消当前操作并返回到可用的提示符。
现在,让我们来看看 Ruby 中可用的数据类型。
数据类型
Ruby 有六种主要的数据类型:数字、字符串、符号、数组、哈希和布尔值。在本节中,我将简要介绍这些数据类型以及如何使用它们。
数字
Ruby 支持您在学校学到的数学运算,还有一些您可能没见过的运算。输入一个表达式到 IRB 并按 ENTER 键查看结果:
irb(main):003:0> **1 + 1**
=> 2
我们让 Ruby 计算表达式 1 + 1,它返回了结果 2。试试更多的数学操作。一切应该按预期工作,至少直到你尝试除法,如下所示:
irb(main):004:0> **7 / 3**
=> 2
Ruby 默认执行整数除法。换句话说,它会丢弃余数。你可以使用取模运算符(%)来找到余数。如果你想得到一个分数结果,则需要明确告诉 Ruby 使用浮点数运算,方法是在至少一个数字后面加上小数点和零。在 IRB 中,你可以看到取模运算符和浮点除法的例子:
irb(main):005:0> **7 % 3**
=> 1
irb(main):006:0> **7.0 / 3**
=> 2.3333333333333335
这个概念很重要:虽然这些看起来像简单的数学运算符,但它们实际上在 Ruby 中是方法。你甚至可以在其他语言认为是基本数据类型的数据上调用方法。
irb(main):007:0> **1.odd?**
=> true
在这里,我们询问数字 1 是否是奇数,IRB 的回答是true。
字符串
你可以通过用单引号或双引号将字符括起来来创建字符串,例如下面这个例子:
irb(main):008:0> **'A String!'**
=> "A String!"
你还可以在 Ruby 中将字符串组合成更大的字符串。Ruby 能理解两种操作:字符串相加和将字符串乘以一个数字。让我们看每种操作的一个例子:
irb(main):009:0> **"Hello" + "World"**
=> "HelloWorld"
irb(main):010:0> **"Hi" * 3**
=> "HiHiHi"
请注意,Ruby 在加法或乘法操作时不会自动在单词之间添加空格,这个细节由你负责处理。
到目前为止,我还没有区分单引号字符串和双引号字符串,但双引号字符串实际上允许你以更复杂的方式组合字符串。例如,它们支持一种叫做字符串插值的特性,在这种情况下,Ruby 会计算由#{和}括起来的表达式,将结果转换为字符串,并自动插入到字符串中,如下所示:
irb(main):011:0> **x = 10**
=> 10
irb(main):012:0> **"x is #{x}"**
=> "x is 10"
在这种情况下,#{x} 的值为 10,因此 Ruby 将数字 10 转换为字符串,并返回"x is 10"。
双引号字符串还支持特殊字符,如换行符和制表符。这些特殊字符由反斜杠后跟字母组成。输入\n可以创建换行符(见下文),或输入\t可以创建制表符。要在双引号字符串中添加字面意义上的反斜杠,请输入两个反斜杠。
irb(main):013:0> **puts "Line one\nLine two"**
Line one
Line two
=> nil
你已经看到了一些字符串方法,但还有许多其他方法也很有用,包括length和empty?。(是的,Ruby 中的方法可以以问号甚至感叹号结尾。)让我们来看一下这两个方法的实际应用:
irb(main):014:0> **"Hello".length**
=> 5
irb(main):015:0> **"Hello".empty?**
=> false
length方法返回字符串中的字符数,而empty?方法则告诉你字符串是否包含任何字符。
注意
方法名末尾的问号,如empty?,表示它是一个谓词方法,将返回一个布尔值。感叹号(!)通常表示该方法执行某些危险操作,例如直接修改对象。
符号
Ruby 有一种在其他编程语言中不常见的数据类型,那就是符号(Symbol)。符号与字符串类似,都是由字符组成的,但符号不是被引号括起来,而是以冒号为前缀,如下所示:
irb(main):016:0> **:name**
=> :name
符号通常用作标识符。它们只创建一次,并且是唯一的。这意味着它们既易于程序员读取为字符串,又节省内存。你可以通过创建一些字符串和符号,然后调用 object_id 方法来亲自验证这一点。
irb(main):017:0> **"name".object_id**
=> 70156617860420
irb(main):018:0> **"name".object_id**
=> 70156617844900
irb(main):019:0> **:name.object_id**
=> 67368
irb(main):020:0> **:name.object_id**
=> 67368
注意,这里的两个字符串内容相同,但对象 ID 不同。这是两个不同的对象。两个符号具有相同的内容和相同的对象 ID。
当 Ruby 比较两个字符串是否相等时,它会检查每个字符的相等性。而比较两个符号是否相等时,只需要进行数值比较,这样更加高效。
数组
数组表示 Ruby 中的对象列表。你可以通过用方括号将一组对象括起来来创建一个数组。例如,来创建一个数字数组:
irb(main):021:0> **list = [1, 2, 3]**
=> [1, 2, 3]
Ruby 数组可以包含任何类型的对象,甚至是其他数组。你可以通过将一个数字索引传递给数组的 [] 方法来访问数组中的单个元素。第一个元素的索引是零。尝试查看刚创建的数组中的第一个元素:
irb(main):022:0> **list[0]**
=> 1
输入 list[0] 告诉 Ruby 获取数组中的第一个数字,方法将返回 1。
注意
如果你尝试访问一个数组中不存在的元素,[] 方法将返回 nil。
你还可以向 [] 方法传递两个数字来创建一个数组切片,如下所示。你提供的第一个数字指定了起始索引,而第二个数字则告诉它你想要在数组切片中包含多少元素:
irb(main):023:0> **list[0, 2]**
=> [1, 2]
这里,[] 方法从索引零开始,并返回 list 中的前两个数字。
与字符串一样,你还可以使用 + 运算符将数组相加,创建一个新数组。如果你只是想将元素添加到现有数组的末尾,可以使用 << 运算符。你可以在这里看到每种操作的示例:
irb(main):024:0> **list + [4, 5, 6]**
=> [1, 2, 3, 4, 5, 6]
irb(main):025:0> **list << 4**
=> [1, 2, 3, 4]
尽管 + 运算符返回一个新数组,但它不会修改现有数组。而 << 运算符会修改现有数组。你还可以使用索引重新赋值给现有元素或向数组添加新元素。
哈希
哈希是一个键值对的集合。在 Ruby 中,哈希是用大括号括起来的。与数组索引不同,哈希的键可以是任何数据类型。例如,符号通常作为哈希键使用。当你需要访问哈希中的一个值时,只需将对应的键传递给 [] 方法,如下所示。尝试访问一个不存在的键会返回 nil。
irb(main):026:0> **some_guy = { :name => "Tony", :age => 21 }**
=> {:name=>"Tony", :age=>21}
irb(main):027:0> **some_guy[:name]**
=> "Tony"
键和值之间的等号和大于符号(=>)的组合通常被称为哈希火箭。由于符号经常用作哈希的键,Ruby 1.9 添加了一个专门为它们设计的简写语法。你可以将符号前的冒号移到后面,然后省略哈希火箭。以下是一个示例:
irb(main):028:0> **another_guy = { name: "Ben", age: 20 }**
=> {:name=>"Ben", :age=>20}
尽管你可以使用这种简写语法创建哈希,但 Ruby 似乎有些怀旧,在显示哈希时仍然使用旧语法。
你也可以使用keys方法获取哈希中所有键的数组。如果你需要哈希中所有值的数组,改用values方法即可。这里的代码展示了每个方法的示例,使用的是刚刚创建的相同哈希:
irb(main):029:0> **another_guy.keys**
=> [:name, :age]
irb(main):030:0> **another_guy.values**
=> ["Ben", 20]
哈希经常用来表示数据结构,如这些示例所示。它们有时也用来将命名参数传递给方法。如果哈希是方法调用的最后一个(或唯一)参数,你甚至可以省略大括号。
例如,merge方法将两个哈希合并。这里的代码将名为another_guy的哈希与一个包含{ job: "none" }的新哈希合并。
irb(main):031:0> **another_guy.merge job: "none"**
=> {:name=>"Ben", :age=>20, :job=>"none"}
由于这个方法调用的唯一参数是新的哈希,你可以省略大括号。Rails 有许多其他类似的这种方法调用示例。
布尔值
布尔表达式是任何可以计算为真或假的表达式。这些表达式通常涉及布尔运算符,Ruby 支持常见的运算符,包括小于(<)、大于(>)、等于(==)和不等于(!=)。尝试在 IRB 提示符下使用这些布尔表达式:
irb(main):032:0> **1 < 2**
=> true
irb(main):033:0> **5 == 6**
=> false
Ruby 还提供了and(&&)和or(||)运算符,用于组合多个布尔表达式,如下所示:
irb(main):034:0> **1 < 2 || 1 > 2**
=> true
irb(main):035:0> **5 != 6 && 5 == 5**
=> true
这两个运算符短路。也就是说,&&只有在两侧的表达式都为真时才为真。如果第一个表达式为假,则第二个表达式不会被计算。类似地,||如果任一表达式为真,则为真。如果第一个表达式为真,则第二个表达式不会被计算。
||运算符有时也用于赋值。当你希望只在变量当前为nil时初始化它,否则保持当前值时,你可以这样做。Ruby 为此提供了||=运算符。这被称为条件赋值,你可以在这里看到一个示例:
irb(main):036:0> **x = nil**
=> nil
irb(main):037:0> **x ||= 6**
=> 6
如果变量x不是假值,那么条件赋值将返回x的值,而不是将其设置为 6。
注意
Ruby 中的任何表达式都可以被评估为布尔表达式。在 Ruby 中,只有nil和false被认为是假的。其他所有值都被认为是真的。这与一些其他语言不同,在这些语言中,像空字符串、空集合和数字零这样的值被认为是假的。
常量
常量为一个不变的值赋予一个名称。在 Ruby 中,常量的名称必须以大写字母开头。常量通常使用大写字母书写,像这样:
irb(main):038:0> **PI = 3.14**
=> 3.14
irb(main):039:0> **2 * PI**
=> 6.28
Ruby 实际上不会阻止你给常量重新赋值,但如果这么做,它会显示一个警告。
变量
在 Ruby 中,你无需提前声明变量或指定类型,只需像下面这样给一个名称赋值:
irb(main):040:0> **x = 10**
=> 10
变量 x 现在指向数字 10。变量名通常采用 蛇形命名法,即所有字母小写,单词之间用下划线连接。
irb(main):041:0> **first_name = "Matthew"**
=> "Matthew"
变量名可以包含字母、数字和下划线,但必须以字母或下划线开头。
控制流
到目前为止,我们看的例子都是线性的。真正的程序通常包含只有在满足某个条件时才会执行的语句,以及多次重复执行的语句。在这一部分,我将介绍 Ruby 的条件语句和迭代。
条件语句
条件语句 让你的程序根据你提供的表达式选择执行一个或多个代码分支。因此,在代码中做决策也叫做 分支。例如,下面的条件语句只有在表达式 age < 13 计算为 true 时才会打印 Child 这个词。
irb(main):042:0> **age = 21**
=> 21
irb(main):043:0> **if age < 13**
irb(main):044:1> **puts "Child"**
irb(main):045:1> **end**
=> nil
变量 age 被设置为 21,因此 age < 13 会计算为 false,什么也不会被打印出来。
你还可以使用 elsif 和 else 来创建更复杂的条件语句。我们来看一个需要检查多个条件的代码示例:
irb(main):046:0> **if age < 13**
irb(main):047:1> **puts "Child"**
irb(main):048:1> **elsif age < 18**
irb(main):049:1> **puts "Teen"**
irb(main):050:1> **else**
irb(main):051:1> **puts "Adult"**
irb(main):052:1> **end**
Adult
=> nil
这段代码可以根据 age 的值采取三种不同的分支。在我们的例子中,它应该跳过 if 和 elsif 语句中的代码,只打印 Adult。
之前所有的条件示例都检查了为 true 的表达式,但如果你想在表达式为 false 时执行一段代码该怎么办呢?像其他语言一样,Ruby 有一个逻辑 非 操作符(可以是 not 或 !),在这里非常有用。以下示例将在 name 不是空字符串时打印其值。
irb(main):053:0> **name = "Tony"**
=> "Tony"
irb(main):054:0> **if !name.empty?**
irb(main):055:1> **puts name**
irb(main):056:1> **end**
=> nil
当 name.empty? 为 false 时,! 操作符应该将结果反转为 true,这样 if 语句中的代码就会执行。用更自然的方式来说,这个条件可能是“除非 name 为空,否则打印它的值。”与 if 语句不同,Ruby 的 unless 语句在表达式计算为 false 时执行代码。
irb(main):057:0> **name = "Tony"**
=> ""
irb(main):058:0> **unless name.empty?**
irb(main):059:1> **puts name**
irb(main):060:1> **end**
=> nil
我觉得这还是有点啰嗦。对于像这样的单行表达式,Ruby 允许你把条件放在行尾:
irb(main):061:0> **name = "Tony"**
=> ""
irb(main):062:0> **puts name unless name.empty?**
=> nil
这个例子简洁且易读。对我来说,这段代码的意思是“除非 name 为空,否则打印它。”这段代码也是 Ruby 灵活性的一个很好的例子。你可以使用最符合你理解的风格来编写条件表达式。
迭代
当你处理一个对象集合时,比如数组或哈希,你通常会想对每个项目执行操作。除了其他语言中看到的 for 循环外,Ruby 集合提供了 each 方法。
each 方法接受一块代码,并对集合中的每个元素执行它。Ruby 中的块通常以 do 开头,以 end 结尾。块也可以接受一个或多个参数,这些参数列在一对管道符号中。each 方法返回整个集合的值。
下一个示例遍历数组 list 中的每个元素,list 是我们在本章前面创建的数组 [1, 2, 3, 4]。它将每个元素赋值给变量 number,然后打印 number 的值。
irb(main):063:0> **list.each do |number|**
irb(main):064:1> **puts number**
irb(main):065:1> **end**
1
2
3
4
=> [1, 2, 3, 4]
像这样的简单块通常在 Ruby 中写成一行。你可以使用大括号来表示一个块,而不是写 do 和 end,这在单行块中非常常见。像前面的例子一样,这个示例遍历列表并打印每个元素,但它在一行代码中完成所有操作。
irb(main):066:0> **list.each { |n| puts n }**
1
2
3
4
=> [1, 2, 3, 4]
你还可以使用 each 方法遍历哈希。因为哈希是键值对的集合,所以块将接受两个参数。我们来尝试用 each 遍历我们之前的一个哈希:
irb(main):067:0> **some_guy.each do |key, value|**
irb(main):068:1> **puts "The #{key} is #{value}."**
irb(main):069:1> **end**
The name is Tony.
The age is 21.
=> {:name=>"Tony", :age=>21}
块不仅对迭代有用。任何方法都可以接受一个块并使用它包含的代码。例如,你可以将一个块传递给 File.open 方法。Ruby 应该将文件句柄作为变量传递给块,执行块中的代码,然后自动关闭文件。
方法
一个 方法 是一个可重用代码的命名块。定义自己的方法在 Ruby 中很简单。方法定义以 def 开头,后面跟着一个名称,直到 end 结束。这个方法每次被调用时会打印 “Hello, World!”:
irb(main):070:0> **def hello**
irb(main):071:1> **puts "Hello, World!"**
irb(main):072:1> **end**
=> nil
如你所见,示例中的方法定义应该返回 nil。
注意
如果你使用的是 Ruby 2.1,方法定义会将方法名作为符号返回。
一旦你定义了一个方法,你可以通过在 IRB 提示符中输入方法名来调用它:
irb(main):073:0> **hello**
Hello, World!
=> nil
Ruby 方法总是返回其最后一条语句的值;在这个例子中,最后一条语句是 puts,它返回 nil。你可以使用 return 显式返回一个值,或者直接将你希望返回的值作为方法的最后一行。
例如,如果你希望 hello 方法返回 true,你可以像这样修改它:
irb(main):074:0> **def hello**
irb(main):075:1> **puts "Hello, World!"**
irb(main):076:1> **true**
irb(main):077:1> **end**
=> nil
现在像之前一样调用该方法:
irb(main):078:0> **hello**
Hello, World!
=> true
因为方法的最后一行是 true,所以当调用该方法时,它会返回 true。
在 Ruby 中,你通过在方法名后面添加参数来指定方法参数,参数可以选择性地用括号括起来,如下一个例子所示。参数也可以有默认值。
irb(main):079:0> **def hello(name = "World")**
irb(main):080:1> **puts "Hello, #{name}!"**
irb(main):081:1> **end**
=> nil
这个示例重新定义了 hello 方法,使其接受一个名为 name 的参数。这个参数的默认值为 "World"。这个方法可以像之前一样调用来显示 “Hello, World!”,或者你可以传递一个 name 参数的值来问候其他人。
irb(main):082:0> **hello**
Hello, World!
=> nil
irb(main):083:0> **hello "Tony"**
Hello, Tony!
=> nil
方法参数周围的括号也是可选的。如果意图不明确,可以加上括号;否则,可以省略它们。
类
在像 Ruby 这样的面向对象编程语言中,一个类表示一个独特类型对象的状态和行为。在 Ruby 中,对象的状态存储在实例变量中,方法定义了它的行为。Ruby 类的定义以class开头,后接大写字母的类名,并以匹配的end结束。
类定义可以包括一个名为initialize的特殊方法。创建类的新实例时,会调用该方法。通常用来为类所需的实例变量赋值。在 Ruby 中,实例变量以@开头,如以下类定义所示:
irb(main):084:0> **class Person**
irb(main):085:1> **def initialize(name)**
irb(main):086:2> **@name = name**
irb(main):087:2> **end**
irb(main):088:1> **def greet**
irb(main):089:2> **puts "Hi, I'm #{@name}."**
irb(main):090:2> **end**
irb(main):091:1> **end**
=> nil
这段代码定义了一个名为Person的新类。initialize方法接受一个参数,并将该参数的值赋给实例变量@name。greet方法打印一个友好的问候。让我们编写一些代码,来使用这个新类。
irb(main):092:0> **person = Person.new("Tony")**
=> #<Person:0x007fc98418d710 @name="Tony">
irb(main):093:0> **person.greet**
Hi, I'm Tony.
=> nil
你可以通过调用Person.new并传递所需的参数来创建Person类的实例。前面的示例创建了一个名为 Tony 的Person实例。
Person.new的返回值是该对象的字符串表示。它由类名、内存中的对象引用以及实例变量的列表组成。调用greet方法应该会显示我们预期的友好问候。
实例变量,如@name,在类外部是不可访问的。尝试在 IRB 提示符下访问person.name,你应该会看到一个错误。
irb(main):094:0> **person.name**
NoMethodError: undefined method 'name'
如果你需要在类外部访问或修改@name,你需要编写一个getter和一个setter。这两个方法分别用于获取或设置实例变量的值。幸运的是,Ruby 类提供了attr_accessor方法,可以为你自动生成 getter 和 setter。
通常你会在Person类的定义中包含attr_accessor :name。为了避免重新输入整个类定义,你可以重新打开该类并添加这一行:
irb(main):095:0> **class Person**
irb(main):096:1> **attr_accessor :name**
irb(main):097:1> **end**
=> nil
这段代码将attr_accessor调用添加到Person类,并自动更新所有该类的对象。这也是 Ruby 灵活性的另一个例子。你可以重新打开一个类,甚至在运行时,根据需要添加新方法。
现在,如果我们想要更改这个人的名字,我们只需要将其设置为其他值,如下所示:
irb(main):098:0> **person.name**
=> "Tony"
irb(main):099:0> **person.name = "Wyatt"**
=> "Wyatt"
irb(main):100:0> **person.greet**
Hi, I'm Wyatt.
=> nil
attr_accessor方法使用符号:name来定义 getter name和 setter name=。现在你可以根据需要获取和设置实例变量的值。如果你只需要一个 getter,可以使用attr_reader替代attr_accessor。这样做允许你读取@name的值,但不能修改它。
类方法
attr_accessor方法与我之前讨论过的方法有所不同。请注意,attr_accessor是在类定义体内调用的。而你之前见过的方法,比如greet方法,是在类的实例上调用的。
在 Ruby 中,调用类的实例方法称为 实例方法。调用类本身的方法称为 类方法。new 方法就是一个类方法的例子。当你输入 Person.new("Tony") 时,你调用的是 Person 类的 new 类方法。
继承
在 Ruby 中,你可以定义一个新的类,基于现有类的状态和行为构建,新类将从现有类继承变量和方法。继承定义了这两个类之间的是一个关系。例如,学生是一个人。我们可以这样定义 Student 类:
irb(main):101:0> **class Student < Person**
irb(main):102:1> **def study**
irb(main):103:2> **puts "ZzzzZzzz"**
irb(main):104:2> **end**
irb(main):105:1> **end**
=> nil
第一行中的 < Person 表示 Student 类继承自 Person 类。由 Person 定义的变量和方法现在对 Student 类可用:
irb(main):106:0> **student = Student.new("Matt")**
#<Student:0x007fd7c3ac4d90 @name="Matt">
➊ irb(main):107:0> **student.greet**
Hi, I'm Matt.
=> nil
irb(main):108:0> **student.study**
ZzzzzZzzzz
=> nil
因为我们在本章早些时候已经在 Person 类中创建了 greet 方法,所以我们可以让任何 Student 调用这个方法 ➊,而无需在新类中重新定义它。
Ruby 只支持 单继承,这意味着一个类不能同时从多个类继承。不过,你可以通过使用 模块 来绕过这一限制。模块是一个方法和常量的集合,它不能被实例化,但可以包含到其他类中以提供额外的行为。我们在 第七章中讨论了模块和其他 Ruby 的高级特性。
摘要
你现在已经在成为一名优秀的 Ruby on Rails 程序员的道路上迈出了重要的一步。你在本章中学到的 Ruby 知识将使你更容易理解 Rails 框架。
我建议在你感到熟悉 Ruby 之前,尽量多使用 IRB。当你准备好开始探索 Rails 时,输入 exit 退出 IRB,然后继续阅读 第二章。
练习
| Q: | 1. 你可以使用 Ruby 的 File.read 方法读取纯文本文件。创建一个包含一段或两段博客文章或书籍内容的文件,并将其命名为当前目录下的 test.txt。以下代码示例将名为 test.txt 的文件读取到变量 file 中,并显示文件的内容:
**file = File.read("test.txt")**
**puts file**
如你所见,file 包含一个字符串。使用 file.split 可以将字符串转换为一个单词数组。现在你可以使用 Ruby 内置的数组方法来操作文件的内容。例如,使用 file.split.length 来计算文件中的单词数。file.split.uniq.length 告诉你文件中有多少个 独特 的单词。
| Q: | 2. 使用练习 1 中的单词数组,统计每个单词在文件中出现的次数。一种方法是遍历数组,并将每个单词的计数存储在一个哈希中,其中键是单词,值是计数。 |
|---|
| 问: | 3. 创建一个WordCounter类,以执行练习 1 和练习 2 中的操作。该类在初始化时应接受一个文件名以读取,并包含名为count、uniq_count和frequency的方法,用于执行前两个练习中的操作。以下类定义可以帮助你开始:
class WordCounter
def initialize(file_name)
@file = File.read(file_name)
end
# your code here...
end
|
第二章. Rails 基础
Ruby on Rails 是一个开源的 web 框架。像 Ruby 语言一样,它强调程序员的幸福感和生产力。正如你将看到的,它包括了合理的默认设置,帮助你减少配置的时间,让你有更多时间写代码。它还为你的应用程序创建了一个目录结构,为你所需要的每个文件提供了合适的位置。
Rails 由 David Heinemeier Hansson 创建。他将其从他为 37signals 构建的项目管理应用 Basecamp 中提取出来,并于 2004 年 7 月首次作为开源发布。
Rails 还是一个 全栈 web 框架。这意味着它包含了构建 web 应用程序所需的一切,能够接受用户请求、查询数据库并使用模板呈现数据来响应。
在终端中输入以下命令,确保已经安装了 Rails:
$ **rails --version**
这应该会显示 Rails 4.0.0 或更高版本。如果没有,请查看 Ruby、Rails 和 Git 中的 Rails 安装说明。
您的第一个 Rails 应用程序
Rails 让入门变得简单,所以让我们马上开始吧。你只需要输入五个命令,就可以运行一个 Rails web 应用程序了。
就像绝地武士自己制造光剑一样,我认为 web 开发者应该自己建造个人网站,因此我们将从这里开始。我把我的网站当作一个测试新想法的游乐场。在快速发展的 Ruby on Rails 世界中,保持自己的网站最新,也有助于你学习如何使用新发布的特性。
打开一个终端窗口,并为你的 Rails 项目创建一个目录。我称之为 code,但你可以使用任何你喜欢的名称。
$ **mkdir code**
$ **cd code**
现在使用 rails new 命令创建一个新应用程序。我们的第一个应用程序将是一个简单的博客,因此我们就称它为 blog。
$ **rails new blog**
该命令会创建你新应用所需的所有文件,并运行 bundle install 命令来下载并安装 Rails 所需的其他 gems。(Gems 是打包的 Ruby 应用程序或库。)根据你的网络速度,这可能需要几分钟。当安装完成后,使用 cd 命令进入新创建的 blog 目录:
$ **cd blog**
最后,使用 rails server 命令启动服务器,这样你就能看到你新的应用程序。当你创建这个应用程序时,blog 目录中也会创建一个名为 bin 的目录。bin 目录是你将来找到 rails 和其他命令的地方。
$ **bin/rails server**
该命令启动了内置于 Ruby 中的 WEBrick 服务器。服务器启动后,打开您的网页浏览器并访问这个地址:http://localhost:3000。如果一切正常,您应该能看到像 图 2-1 中那样的网页。

图 2-1. 您的第一个 Rails 应用程序
恭喜你!你正式成为了一名 Ruby on Rails 开发者。你刚刚创建的页面包含了一些关于如何开始使用你的应用程序的提示,但在我们深入了解之前,让我们先了解一下 Rails。
Rails 原则
Rails 框架基于两个广为人知的软件工程原则。理解这些原则将帮助你理解构建应用程序的 “Rails 方式”。
约定优于配置
你无需配置任何内容就能让基本的 Rails 应用程序启动并运行。在 Rails 中,这个概念被称为 约定优于配置。只要遵循 Rails 的约定,一切都会按预期工作。
当你创建一个应用时,Rails 会为你做出许多选择。这些选择包括开发模式下使用哪个 Web 服务器和数据库服务器,应用将使用哪个 JavaScript 库和测试库。甚至应用数据库表和模型的名称也是根据约定自动选择的。当然,你可以打破约定,改变这些设置,但那样你就需要自己配置一些内容。
不重复自己
Rails 中的另一个关键原则是 不重复自己,通常缩写为 DRY。在 Rails 中,你避免在应用程序中重复相同的知识。在多个地方指定相同的信息,可能会导致在修改一个实例时忘记更新另一个,从而引发错误。
在我们深入了解 Rails 架构和目录结构的过程中,你会看到 DRY 原则的几个示例。Rails 应用的每个部分都有一个单一的、特定的位置。那些可以从其他来源推断出来的内容,例如数据库表中的列名,便不需要在任何地方重复指定。
Rails 架构
Rails 应用程序围绕 模型-视图-控制器(MVC) 软件工程模式构建。MVC 模式旨在将应用程序的数据与用户的交互分离。这种关注点分离通常会导致一个更易于理解和维护的应用程序。
模型
模型 表示应用程序的数据以及操作这些数据的规则。应用程序的数据有时被称为应用程序的 状态。操作这些数据的规则也被称为 业务逻辑。所有对应用程序状态的更改必须通过模型层。
Rails 模型包含数据验证和模型间关联的代码。你写的大部分代码都会位于 Rails 模型内部,除非它直接与用户看到的数据视图相关。
视图
视图 是你应用程序的用户界面。因为我们正在构建 Web 应用程序,所以视图主要由 HTML 组成。Rails 默认使用一种名为 嵌入式 Ruby(ERB) 的模板系统。
嵌入式 Ruby 允许你在 HTML 模板中包含 Ruby 代码以访问数据。当用户请求页面时,模板中的 Ruby 代码会被服务器评估,结果生成的 HTML 页面会被发送给用户。
在视图中嵌入 Ruby 代码有时会导致程序员在视图中包含过多的代码。这样做是有问题的,因为如果你添加了另一个视图,代码就需要被重复使用。只在视图中使用的代码可以移动到helper中,helper是专门用于视图中的方法。一般来说,视图中不应包含比简单条件语句更复杂的代码。
注意
除了 HTML 页面,Rails 还可以生成 JSON 和 XML。Ruby 内置支持生成 CSV 文件,并且有很多 gem 可以用来生成其他类型的输出,例如 PDF 文档和 Excel 表格。
控制器
controller 就像是将模型和视图连接在一起的“粘合剂”。控制器负责接受来自用户的请求,从模型中收集必要的数据,并渲染正确的视图。听起来像是很多工作,但得益于 Rails 应用程序中使用的约定,这个过程几乎是自动完成的。
在 Rails 中,控制器只是一个 Ruby 类,其中的方法与应用程序中的各种操作相对应。例如,在你的博客应用程序中,你可能有一个名为 show 的方法用来显示博客文章,还有一个名为 new 的方法用来添加新文章。
Rails 应用程序结构
现在你已经了解了 Rails 使用的原则和架构,让我们看看这些部分在 rails new 命令创建的目录结构中位于哪里。在博客目录中,你应该会找到 10 个子目录。
app 目录
app 目录是你在构建应用程序时会花费大部分时间的地方。它包含了之前讨论过的 MVC 架构的各个部分的子目录,以及assets、helpers 和 mailers。
assets 目录保存着应用程序使用的图片、JavaScript 文件和 CSS 样式表。helpers 目录包含着视图使用的 Ruby 文件和方法。mailers 目录用于存放发送电子邮件的 Ruby 类。
bin 目录
bin 目录包含了用于访问 bundle、rails 和 rake 命令行程序的简单 Ruby 脚本,这些程序用于构建你的应用程序。这些脚本确保这三个程序在当前 Rails 应用程序的上下文中运行。你可以同时安装多个版本的这些程序,如果不使用 bin 中的脚本来访问它们,可能会导致错误。
config 目录
Rails 强烈依赖“约定优于配置”,但有时配置是不可避免的。在这些情况下,可以查找 config 目录。
environments 子目录包含 Rails 自动创建的三个不同环境的配置文件。环境是为特定用途(如开发或测试)使用的一组设置。这些设置存储在 development.rb(用于开发应用程序时)、test.rb(用于运行自动化测试时)和 production.rb(应用程序部署并在生产环境中运行时使用)中。
文件 application.rb 包含所有环境的设置。然而,刚才提到的某个特定环境文件中的信息将优先于此处的设置。
文件 database.yml 存储每个环境的数据库配置。默认情况下,Rails 在你运行 rails new 时会创建一个 SQLite 数据库,因此 database.yml 中的默认设置适用于该 SQLite 数据库。这个数据库是一个单一的文件,你将在开发期间使用它。通常,在生产环境中,你会指定一个不同的数据库服务器。
文件 routes.rb 将用户输入的网页地址映射到应用程序中的特定控制器和操作。随着你向应用程序添加资源和操作,你需要更新此文件以反映这些变化。在讨论控制器时,我会在第四章中讲解基于资源的路由。
db 目录
db 目录初始时只包含一个名为 seeds.rb 的文件。你可以使用这个文件创建应用程序的默认数据。例如,在一个包含用户帐户的应用程序中,你可能希望在这里添加一个特殊的“管理员”用户。
在构建应用程序时,你会创建 数据库迁移,即用于创建和修改数据库中表的 Ruby 脚本。会创建一个名为 migrate 的目录,用来存放这些数据库迁移文件。文件 schema.rb 会显示应用程序数据库的当前状态。如果你在应用程序中使用默认的 SQLite 数据库,数据库本身也会被放在这个文件夹中。
lib 目录
lib 目录是你放置任何可重用库代码的地方。除了两个子目录:assets 和 tasks 之外,初始时该目录为空。Assets 是图像、CSS 样式表和 JavaScript 文件。Tasks 是 Ruby 脚本,用于自动化操作,如管理应用程序的数据库、清除日志和临时文件、以及运行测试。这些任务使用 rake 命令执行。
log 目录
当你的应用程序运行时,数据会写入 log 目录中的一个文件。当你在开发环境中运行代码时,这个文件名为 development.log。针对测试和生产环境,将创建单独的文件。
public 目录
public 目录中的文件会像在应用程序根目录中的文件一样发送给用户。例如,目录中的三个文件用于错误信息——404.html、422.html 和 500.html。你可以通过将这些文件的名称添加到地址栏中来查看其中一个文件。例如,如果访问 http://localhost:3000/404.html,你应该会看到默认的“页面不存在”错误页面。
这个目录还包含一个默认的favicon.ico,这是大多数浏览器地址栏中显示的图标,还有一个robots.txt文件,控制搜索引擎如何索引你的应用。你可以修改这些文件以适应你的应用。你可能想要自定义错误页面,加入自己的品牌标识,并为你的站点添加自定义的 favicon。
测试目录
test 目录包含自动化测试的子目录,每个子目录用于你的应用的不同部分。它还包含脚本 test_helper.rb,该脚本加载 config/environments/test.rb 中的测试环境设置,并添加在测试中使用的辅助方法。
一些 Rails 开发者实践测试驱动开发(TDD)。在 TDD 中,你首先编写一个自动化测试,描述一个新特性;然后你编写足够的代码让测试通过;最后,你重构或重组代码,以提高可读性并减少复杂性。
tmp 目录
tmp 目录包含临时文件。在这里,你可以找到应用程序资源的缓存副本、运行程序(如 Web 服务器)的进程 ID 文件(pid)、用户会话以及表示应用正在使用的套接字的文件。
注意
因为这些文件通常不需要保存,所以版本控制系统会忽略它们。
vendor 目录
最后,vendor 目录保存第三方 gems 所需的资源。它的作用类似于 lib 目录,只不过它用于你没有自己编写的库。
Rails 命令
在构建 Rails 应用时,你会使用四个不同的命令行程序。对于新的 Rails 开发者来说,这些程序有时会让人感到困惑。
gem 命令
gem 命令用于安装 Ruby gems。Rails 框架实际上是作为一组 gem 文件发布的。
你新创建的 Rails 应用由超过 40 个 gems 组成。维护这些 gems 的正确版本及其依赖关系可能变得复杂。因此,你很少直接使用 gem 命令;相反,你通常依赖一个名为 Bundler 的工具来管理 gems,并保持依赖关系的更新。你通过 bundle 命令与 Bundler 进行交互。
bundle 命令
bundle 命令用于安装和更新应用所需的 gems。它通过读取在 rails new 命令自动创建的位于 Rails 应用根目录的 Gemfile 来安装 gems。它将你所使用的 gems 及其依赖的版本号存储在 Gemfile.lock 文件中。
bundle list命令会显示当前应用程序使用的所有 gems 的名称和版本:
$ **bin/bundle list**
如前所述,我们使用的是bin目录中的bundle副本。
rake命令
rake命令是一个自动化构建工具,用于执行与应用程序相关的任务。(如果您熟悉make命令,rake是其 Ruby 版本。)输入此命令以获取应用程序的可用任务列表:
$ **bin/rake --tasks**
此命令会将您应用程序可以使用的任务列表以及每个任务的简短描述打印到终端。有些 gems 会为您的应用程序添加任务,您也可以通过编写 Ruby 脚本并将其保存在lib/tasks目录中来添加您自己的任务。
rails命令
您之前使用rails命令来创建应用程序并启动服务器。您还可以使用此命令来生成新代码并启动控制台。单独输入rails命令以列出可用选项:
$ **bin/rails**
除了您用来构建应用程序的new命令和启动 web 服务器的server命令外,Rails 还提供了其他一些有用的命令。这些包括generate命令(生成新代码)、console命令(启动一个交互式 Ruby 控制台,并预加载您的 Rails 应用程序)以及dbconsole命令(启动当前配置数据库的命令行界面)。
现在,您已经看到了几个 Rails 命令行工具,让我们使用其中一些来为我们创建的博客应用程序添加一些功能。
Rails 脚手架
我们将使用 Rails 中的一个功能,称为脚手架。Rails 脚手架是一个有时会引发争议的功能,它会为您生成应用程序代码。这个单一命令会自动创建模型、一组视图和控制器。
许多开发者认为您应该编写所有自己的代码。我同意他们的观点,但 Rails 脚手架对于启动一个应用程序非常有用,尤其是对于新接触 Ruby on Rails 的开发者。接下来的几章我们将探讨生成的代码。到最后,您将理解每个文件,并能够手动编写这些文件。
打开一个新的终端窗口(或在当前窗口中新建一个标签页)。如果需要,切换到code/blog目录。然后,使用rails generate命令为博客添加帖子:
$ **bin/rails generate scaffold Post** ➊**title:string** ➋ **body:text**
在这里,我们要求 Rails 为博客文章生成一个脚手架。我们指定了文章应该包含标题 ➊ 和正文 ➋。
标题将是一个字符串,正文将是数据库中的一个文本字段。当此命令运行时,您应该能在终端看到一阵活动,文件会被生成并放入正确的文件夹中。
上一个命令应该已经生成了数据库迁移。使用rake命令来运行该迁移:
$ **bin/rake db:migrate**
该命令应该会创建一个名为 posts 的表,字段包括 id、title、body、created_at 和 updated_at。title 和 body 字段将存储用户输入的数据。Rails 会自动添加 id、created_at 和 updated_at 字段。id 字段是一个唯一的、自增的整数,代表数据库中的每一行。created_at 和 updated_at 字段是时间戳,表示该行数据创建的时间和最后一次更新的时间。Rails 会自动跟踪这些值。
要查看结果,请在浏览器中访问 http://localhost:3000/posts。你应该能看到类似于图 2-2 的页面。

图 2-2. Rails Post 脚手架
这个页面的设计可能不会赢得任何奖项,但它是实用的。点击新建帖子链接,查看添加新博客帖子的表单。添加帖子后,点击返回链接返回主页。
默认情况下,Rails 会以表格的形式显示数据,并提供查看、编辑和删除链接。随意尝试这些链接,并验证应用程序是否正常工作。
在你玩应用程序时,记得查看运行服务器的终端窗口中的输出。这是应用程序生成的开发日志副本。你将在这里找到大量信息,例如请求的 URL、正在运行的 Ruby 方法和正在执行的 SQL 命令。
总结
本章介绍了构建 Rails 应用程序的基本原理、架构、目录结构和命令。在下一章中,我们将深入探讨刚刚生成的 Rails 代码,从模型开始,你将学习如何编写自己的代码。
练习
| 问题: | 1. 探索你新博客的功能。创建、编辑和删除帖子。查看帖子列表和单个帖子。注意当你在应用程序中浏览时,地址栏中的 URL 是如何变化的。 |
|---|---|
| 问题: | 2. 习惯在你选择的编辑器中浏览博客应用程序中的各种文件。如果你使用的是 Sublime Text 2,你可以直接打开博客目录,然后使用侧边栏打开单个文件。 |
第三章 模型
在 Rails 中,模型表示应用程序中的数据以及操作这些数据的规则。模型管理应用程序与相应数据库表之间的交互。应用程序的大部分业务逻辑也应该放在模型中。
本章介绍了活跃记录,这是 Rails 组件之一,提供模型持久化(即,将数据存储在数据库中),以及数据验证、数据库迁移和模型关联。验证是确保仅有效数据存储在数据库中的规则。你创建数据库迁移来更改数据库的架构,关联是你应用程序中多个模型之间的关系。
帖子模型
在上一章中,我们使用 Rails 脚手架生成器构建了一个简单的博客,包含用于博客帖子的模型、视图和控制器。通过在你喜欢的文本编辑器中打开文件app/models/post.rb,查看脚手架生成器创建的帖子模型。
class Post < ActiveRecord::Base
end
这里没有太多内容。目前,这个文件仅告诉我们Post类继承自ActiveRecord::Base。在我讲解你可以实际做什么之前,让我们从活跃记录开始讨论。
活跃记录
活跃记录是对象关系映射(ORM)模式的一种实现,Martin Fowler 在其《企业应用架构模式》(Addison-Wesley Professional, 2002)中使用相同的名称描述了这一模式。它是类与表之间、属性与列之间的自动映射。
数据库中的每个表都由应用程序中的一个类表示。该表的每一行由关联类的实例(或对象)表示,行中的每一列由该对象的一个属性表示。表 3-1 中的示例演示了这种结构。如果你能查看你的数据库,看到的就是这样的内容。
表 3-1. 帖子表
| id | 标题 | 内容 | 创建时间 | 更新时间 |
|---|---|---|---|---|
| 1 | 你好,世界 | 欢迎来到我的博客... | ... | ... |
| 2 | 我的猫 | 最可爱的猫咪... | ... | ... |
| 3 | 太忙了 | 抱歉,我没有更新... | ... | ... |
表 3-1 包含了三个示例博客帖子。这个表由Post类表示。id为 1 的帖子可以由一个Post对象表示。我们把这个对象叫做post。
你可以通过调用对象的属性方法来访问与单个列相关的数据。例如,要查看帖子的标题,调用post.title。通过在对象上调用属性方法访问和更改数据库值的能力被称为直接操作。
创建、读取、更新和删除
让我们通过在 Rails 控制台中输入一些命令进一步探索 Active Record。Rails 控制台就是你在第一章中使用的 IRB,只不过加载了 Rails 应用程序的环境。
要启动 Rails 控制台,进入你的blog目录并输入bin/rails console。你可能会注意到控制台启动时比 IRB 稍微慢一些。在这短暂的暂停期间,应用程序的环境正在加载。
与 IRB 一样,当你完成操作时,你可以输入exit退出控制台。
数据库应用程序的四个主要功能是创建、读取、更新和删除,通常缩写为CRUD。一旦你掌握了这四个操作,你就能构建任何类型的应用程序。
Rails 使这些操作变得非常简单。在大多数情况下,你可以通过一行代码完成每个操作。现在让我们使用它们来处理我们博客上的帖子。
创建
我们将从向数据库中添加一些记录开始。在完成本节内容时,请在 Rails 控制台中输入这些命令。本章接下来的示例将使用这些记录。
在 Rails 中创建记录最简单的方法是使用命名恰当的create方法,如下所示:
2.1.0 :001 > **Post.create title: "First Post"**
➊ (0.1ms) begin transaction
SQL (0.4ms) INSERT INTO "posts" ("created_at"...
(1.9ms) commit transaction
=> #<Post id: 1, title: "First Post", ...>
Rails 控制台会在执行命令时显示发送到数据库的 SQL 语句 ➊。为了简洁起见,接下来的示例中我将省略这些 SQL 语句。
create方法接受一组属性-值对,并将记录插入到数据库中,使用适当的值。在这种情况下,它将title属性设置为"First Post"的值。当你运行这个示例时,id、created_at和updated_at的值会自动为你设置。id列是数据库中的自增值,而created_at和updated_at是 Rails 为你设置的时间戳。由于没有为body列传入值,因此它被设置为 NULL。
create方法是一个快捷方式,用于实例化一个新的Post对象、分配值并将其保存到数据库。如果你不想使用快捷方式,你也可以为每个操作写单独的代码行:
2.1.0 :002 > **post = Post.new**
=> #<Post id: nil, title: nil, ...>
2.1.0 :003 > **post.title = "Second Post"**
=> "Second Post"
2.1.0 :004 > **post.save**
=> true
这次我们使用了多个命令,但就像之前一样,我们创建了一个全新的Post对象。现在数据库中存储了两个帖子。在这两个示例中,我们仅为帖子的title属性分配了值,但你可以通过相同的方式为帖子的body属性分配值。Rails 会自动为你分配id、created_at和updated_at的值。你不应该修改这些值。
阅读
一旦你的数据库中有了一些帖子,你可能会希望将它们读出并显示。首先,让我们使用all方法查看数据库中的所有帖子:
2.1.0 :005 > **posts = Post.all**
=> #<ActiveRecord::Relation [#<Post id: 1, ...>, #<Post id: 2, ...>]>
这会返回一个 Active Record 关系,它包含一个数据库中所有帖子的数组,并将其存储在posts中。你可以将其他方法链式调用到这个关系上,Active Record 会将它们合并成一个查询。
Active Record 还实现了 first 和 last 方法,它们返回数组中的第一个和最后一个条目。Active Record 版本的这些方法只会返回数据库表中的第一个或最后一个记录。这比先获取表中所有记录,再在数组上调用 first 或 last 要高效得多。让我们试着从数据库中获取几篇帖子:
2.1.0 :006 > **Post.first**
=> #<Post id: 1, title: "First Post", ...>
2.1.0 :007 > **Post.last**
=> #<Post id: 2, title: "Second Post", ...>
这个例子返回的是按照 id 排序的第一篇和最后一篇帖子。你将在下一个章节学习如何按其他字段对记录进行排序。然而,有时你可能确切知道想要哪个记录,而它可能不是第一个或最后一个。在这种情况下,你可以使用 find 方法通过 id 获取记录。
2.1.0 :008 > **post = Post.find 2**
=> #<Post id: 2, title: "Second Post", ...>
只是不要请求 find 去获取一个不存在的记录。如果数据库中没有指定 id 的记录,Active Record 将抛出 ActiveRecord::RecordNotFound 异常。当你知道某个特定记录存在但不知道它的 id 时,可以使用 where 方法指定你已知的某个属性:
2.1.0 :009 > **post = Post.where(title: "First Post").first**
=> #<Post id: 1, title: "First Post", ...>
where 方法也返回一个关系。如果有多个记录匹配,你可以在 where 后链式调用 all 方法,告诉 Rails 按需获取所有匹配的记录。
如果你知道数据库中只有一个匹配的记录,你可以在 where 后链式调用 first 方法来获取这个特定记录,就像在之前的例子中一样。这个模式非常常见,因此 Active Record 还提供了 find_by 方法作为快捷方式:
2.1.0 :010 > **post = Post.find_by title: "First Post"**
=> #<Post id: 1, title: "First Post", ...>
这个方法接受一个属性-值对的哈希,并返回第一个匹配的记录。
更新
更新记录就像读取它到一个变量、通过直接操作改变值,然后将其保存回数据库一样简单:
2.1.0 :011 > **post = Post.find 2**
=> #<Post id: 2, title: "Second Post", ...>
2.1.0 :012 > **post.title = "2nd Post"**
=> "2nd Post"
2.1.0 :013 > **post.save**
=> true
Rails 还提供了 update 方法,它接受一个属性-值对的哈希,更新记录,并在一行中保存到数据库:
2.1.0 :014 > **post = Post.find 2**
=> #<Post id: 2, title: "2nd Post", ...>
2.1.0 :015 > **post.update title: "Second Post"**
=> true
update 方法与 save 方法类似,成功时返回 true,如果保存记录时出现问题,则返回 false。
删除
一旦你从数据库中读取了一个记录,你可以通过 destroy 方法将其删除。但这次不要输入这些命令,你可不想删除你之前创建的帖子!
2.1.0 :016 > **post = Post.find 2**
=> #<Post id: 2, title: "Second Post", ...>
2.1.0 :017 > **post.destroy**
=> #<Post id: 2, title: "Second Post", ...>
destroy 方法也可以在类上调用,通过 id 删除记录,这与先将记录读取到变量中的效果相同:
2.1.0 :018 > **Post.destroy 2**
=> #<Post id: 2, title: "Second Post", ...>
你还可以根据关系删除记录:
2.1.0 :019 > **Post.where(title: "First Post").destroy_all**
=> [#<Post id: 1, title: "First Post", ...>]
这个例子删除了所有标题为 "First Post" 的记录。然而,使用 destroy_all 方法时要小心。如果在没有 where 条件的情况下调用它,你会删除指定类的所有记录!
更多 Active Record 方法
如果你熟悉 SQL 或其他访问数据库记录的方法,你会知道操作数据库不仅仅是简单的 CRUD。Active Record 提供了更多数据库操作的方法,如排序、限制、计数和其他计算。
查询条件
除了你到目前为止看到的简单 where 条件,Active Record 还提供了几个方法来帮助你优化查询。order 方法指定返回记录的顺序;limit 指定返回多少条记录;offset 指定从列表中返回的第一条记录。
limit 和 offset 方法通常一起用于分页。例如,如果你想每页显示 10 篇博客文章,你可以这样读取第一页的文章:
2.1.0 :020 > **posts = Post.limit(10)**
=> #<ActiveRecord::Relation [#<Post id: 1, ...>, #<Post id: 2, ...>]>
要读取网站第二页的文章,你需要跳过前 10 篇文章:
2.1.0 :021 > **posts = Post.limit(10).offset(10)**
=> #<ActiveRecord::Relation []>
输入这个会返回一个空集,因为我们的数据库中只有两篇文章。当你以这种方式将 offset 和 limit 结合使用时,你可以将 offset 设置为 limit 的倍数,查看博客的不同页面。
你还可以更改关联中条目的排序方式。使用 limit 时,返回的记录顺序是未定义的,所以你需要指定排序方式。使用 order 方法,你可以为返回的记录集指定不同的排序方式:
2.1.0 :022 > **posts = Post.limit(10).order "created_at DESC"**
=> #<ActiveRecord::Relation [#<Post id: 2, ...>, #<Post id: 1, ...>]>
使用 DESC 告诉 order 返回从最新到最旧的文章。你也可以使用 ASC 按相反的顺序排列。如果你更愿意按标题的字母顺序查看文章,可以将 "created_at DESC" 替换为 "title ASC"。如果不指定 ASC 或 DESC,order 方法默认按升序排列,但我总是指定一个排序,以便明确我的意图。
计算
数据库还提供了对记录执行计算的方法。我们可以在 Ruby 中读取记录并执行这些操作,但内置的数据库方法通常已优化为更快并且使用更少的内存。
count 方法返回匹配给定条件的记录数:
2.1.0 :023 > **count = Post.count**
=> 2
如果你没有指定条件,count 默认会计算所有记录,如此示例所示。
sum、average、minimum 和 maximum 方法在某个字段上执行请求的功能。例如,这行代码会查找并返回最新博客文章的日期:
2.1.0 :024 > **date = Post.maximum :created_at**
=> 2014-03-12 04:10:08 UTC
你看到的最大 created_at 日期应该与最新博客文章的日期匹配,而不一定是示例中显示的日期。
迁移
数据库迁移 用于每次需要更改数据库结构时。当我们使用脚手架生成器创建博客文章时,它为我们生成了迁移文件,但你也可以自己创建迁移文件。随着你构建应用程序,数据库迁移将包含对数据库所做更改的完整记录。
迁移文件存储在 db/migrate 目录中,并以时间戳开头,表示它们的创建时间。例如,你可以通过编辑文件 db/migrate/_create_posts.rb* 来查看脚手架生成器创建的迁移文件。(由于你文件上的时间戳肯定与我的不同,从现在开始我将使用星号来表示文件名中的日期部分。)现在我们来看看这个文件:
class CreatePosts < ActiveRecord::Migration
➊ def change
create_table :posts do |t|
t.string :title
t.text :body
t.timestamps
end
end
end
数据库迁移实际上是 Ruby 类。当迁移运行时,调用change方法 ➊。在这个例子中,该方法创建一个名为posts的表,并包含title、body和timestamps字段。timestamps字段指的是created_at和updated_at字段。Rails 还会自动添加id列。
你可以使用rake命令将迁移作为任务运行。例如,输入bin/rake db:migrate来运行所有待处理的迁移,并使你的数据库保持最新。
Rails 通过在名为schema_migrations的数据库表中存储时间戳,跟踪哪些迁移已经运行。
如果在数据库迁移中犯了错误,可以使用db:rollback任务来撤销它。纠正迁移后,使用db:migrate重新运行它。
模式
除了单独的迁移文件外,Rails 还存储了你数据库的当前状态。你可以通过打开文件db/schema.rb来查看。忽略文件顶部的注释块,应该像这样:
--*snip*--
ActiveRecord::Schema.define(version: 20130523013959) do
create_table "posts", force: true do |t|
t.string "title"
t.text "body"
t.datetime "created_at"
t.datetime "updated_at"
end
end
每次运行数据库迁移时,此文件都会更新。你不应手动编辑它。如果你将应用程序迁移到新电脑,并且想要一次性创建一个新的空数据库,而不是通过运行单独的迁移,你可以使用db:schema:load rake任务来实现:
$ **bin/rake db:schema:load**
运行此命令会重置数据库结构,并在此过程中移除所有数据。
添加列
现在你对迁移有了更多了解,接下来让我们创建一个并运行它。当我们创建博客文章模型时,忘记了文章需要作者。通过生成一个新的迁移,向文章表添加一个字符串列:
$ **bin/rails g migration add_author_to_posts author:string**
Rails 生成器(g是generate的缩写)查看迁移的名称,在这个例子中是add_author_to_posts,并尝试推断你想做什么。这是约定优于配置的另一个例子:按照add_ColumnName_to_TableName的格式命名迁移,Rails 会解析这些信息并自动添加所需内容。根据名称,我们显然想要将名为author的列添加到文章表中。我们还指定了author是一个字符串,因此 Rails 拥有创建迁移所需的所有信息。
注意
你可以为迁移命名任何你想要的名称,但你应该遵循约定,这样就不需要手动编辑迁移文件。
输入bin/rake db:migrate来运行迁移并向数据库中添加author列。如果你仍然打开了 Rails 控制台,你需要exit并重新启动,使用bin/rails console才能使更改生效。你也可以查看db/schema.rb文件,以查看文章表中新添加的列。
在作者迁移中
你刚生成的添加列的代码很简单。编辑文件db/migrate/_add_author_to_posts.rb*来查看它是如何工作的。
class AddAuthorToPosts < ActiveRecord::Migration
def change
add_column :posts, :author, :string
end
end
像 _create_posts.rb,这个迁移是一个包含 change 方法的类。调用 add_column 方法并传入表名、列名和列类型。如果你想添加多个列,你可以为每个列创建单独的迁移,或者可以多次调用这个方法。
Active Record 迁移还提供了 rename_column 方法用于更改列名,remove_column 方法用于从表中删除列,以及 change_column 方法用于更改列的类型或其他选项,如默认值。
验证
记住,模型有用于操作应用数据的规则。Active Record 验证 是一组规则,旨在保护你的数据。添加验证规则以确保只有有效数据被写入你的数据库。
添加验证
让我们看一个例子。因为我们在做一个博客,所以我们应该确保所有帖子都有标题,以免读者感到困惑,我们可以通过验证规则来做到这一点。
验证在 Rails 中作为类方法实现。打开你的帖子模型(app/models/post.rb)并添加这一行:
class Post < ActiveRecord::Base
**validates :title, :presence => true**
end
这会验证 title 字段中是否有文本。如果尝试创建没有标题的博客文章,现在应该会出现错误。
其他常见验证
除了 :presence 验证,Rails 还提供了各种其他验证。例如,你可以使用 :uniqueness 验证,确保没有两篇帖子有相同的标题。
:length 验证接受一个选项哈希,以确认值的长度是否正确。将这一行添加到你的帖子模型中,可以确保所有标题至少有五个字符:
**validates :title, :length => { :minimum => 5 }**
你还可以指定 :maximum 值来代替 :minimum,或者使用 :is 设置一个精确值。
:exclusion 验证确保值不属于给定的值集合。例如,添加此验证会禁止标题为 Title 的博客帖子:
**validates :title, :exclusion => { :in => [ "Title" ] }**
你可以将 :exclusion 看作是不允许的值的黑名单。Rails 还提供了 :inclusion 验证,用于指定一个接受的值的白名单。
测试数据
验证会在数据保存到数据库之前自动运行。如果尝试保存无效数据,save 会返回 false。你也可以手动测试模型,使用 valid? 方法:
2.1.0 :025 > **post = Post.new**
=> #<Post id: nil, title: nil, ...>
2.1.0 :026 > **post.valid?**
=> false
2.1.0 :027 > **post.errors.full_messages**
=> ["Title can't be blank"]
在这个例子中,valid? 方法应该返回 false,因为你没有为标题设置值。验证失败会将消息添加到一个名为 errors 的数组中,调用 errors 数组上的 full_messages 方法应该返回一个由 Active Record 根据你的验证生成的错误消息列表。
自由使用验证规则以防止无效数据进入你的数据库,但在创建这些验证时,也要考虑到用户。清楚地指出哪些值是有效的,并在提供无效数据时显示错误消息,方便用户纠正错误。
关联
只有最简单的应用程序才包含一个单一模型。随着你的应用程序的增长,你会需要更多的模型,随着你添加更多模型,你需要描述它们之间的关系。Active Record 关联描述了模型之间的关系。例如,让我们为博客帖子添加评论。
帖子和评论是相关联的。每个帖子有许多评论,每个评论属于一个帖子。这种一对多关系是最常用的关联之一,我们将在这里探讨它。
生成模型
一个博客评论应该有一个作者、一个内容和一个指向帖子的引用。你可以轻松地使用这些信息生成一个模型:
$ **bin/rails g model Comment author:string body:text post:references**
注意
记得在生成新模型后运行数据库迁移!
post:references 选项告诉 Rails 生成器在评论数据库表中添加一个外键。在这种情况下,外键名为 post_id,因为它指向一个帖子。post_id 字段包含该评论对应帖子的 id。迁移创建了我们在数据库中需要的列,现在我们需要编辑模型来完成关联设置。
添加关联
首先,再次打开 app/model/post.rb 来添加评论关联。之前我提到过每个帖子有许多评论,这正是我们在这里需要的关联:
class Post < ActiveRecord::Base
validates :title, :presence => true
**has_many :comments**
end
Rails 使用一个叫做 has_many 的类方法以可读的方式创建这个关联。现在,编辑 app/model/comment.rb,你会看到 Rails 生成器已经自动为你添加了匹配的 belongs_to 语句:
class Comment < ActiveRecord::Base
belongs_to :post
end
现在,帖子到评论的关联应该能按预期工作。如果你的 Rails 控制台在你做这些更改时仍在运行,你需要重启它才能看到效果。
使用关联
当你在模型中创建一个关联时,Rails 会自动为该模型定义几个方法。使用这些方法,你就不需要担心保持 post_id 更新了。它们会自动维护这个关系。
has_many 方法
你在 Post 中看到的 has_many :comments 语句定义了几个方法:
-
comments。返回一个 Active Record 关系,表示该帖子的评论数组。 -
comments<。将现有的评论添加到该帖子中。 -
comments=。用给定的评论数组替换该帖子的现有评论数组。 -
comment_ids。返回与该帖子相关联的评论 ID 数组。 -
comment_ids=。用给定的 ID 数组中的评论替换该帖子的现有评论数组。
因为 comments 方法返回的是一个关系,它通常与其他方法一起使用。例如,你可以使用 post.comments.build 创建与某个帖子相关的新评论,它会为该帖子构建一个新评论,或者使用 post.comments.create 创建并保存一个新评论到数据库中。每个方法都会自动为新创建的评论分配 post_id。此示例为你的第一个帖子创建了一个新评论。你应该能在 post.comments 的输出中看到新评论:
2.1.0 :028 > **post = Post.first**
=> #<Post id: 1, title: "First Post", ...>
2.1.0 :029 > **post.comments.create :author => "Tony", :body => "Test comment"**
=> #<Comment id: 1, author: "Tony", ...>
2.1.0 :030 > **post.comments**
=> #<ActiveRecord::Relation [#<Comment id: 1, author: "Tony", ...>]>
如果你想检查是否有评论与某个帖子相关联,可以使用 comments.empty?,如果没有评论,则返回 true。你也许还会发现,知道某个帖子有多少个评论是很有用的;在这种情况下,你可以使用 comments.size:
2.1.0 :031 > **post.comments.empty?**
=> false
2.1.0 :032 > **post.comments.size**
=> 1
当你知道某个帖子有评论与之相关时,你可以通过传递评论 ID 给 post.comments.find 来查找特定评论。如果找不到与该帖子关联的匹配评论,该方法将抛出 ActiveRecord::RecordNotFound 异常。如果你不想抛出异常,可以使用 post.comments.where,如果没有找到匹配的评论,该方法会返回一个空的关系。
属于 belongs_to 方法
Comment 模型中的 belongs_to :post 语句定义了五个方法。由于 belongs_to 是单一关联(一个评论只能属于一个帖子),因此所有这些方法的名称都是单数形式:
-
post。返回此评论所属帖子的实例 -
post=。将此评论分配给另一个帖子 -
build_post。为此评论构建一个新帖子 -
create_post。为此评论创建一个新帖子并保存到数据库 -
create_post!为此评论创建一个新帖子,但如果帖子无效,则会抛出ActiveRecord::RecordInvalid异常
这些方法是 Post 模型中定义的方法的逆操作。当你有一个评论并希望操作其关联的帖子时,可以使用它们。例如,下面我们来获取与我们第一个评论相关的帖子:
2.1.0 :033 > **comment = Comment.first**
=> #<Comment id: 1, author: "Tony", ...>
2.1.0 :034 > **comment.post**
=> #<Post id: 1, title: "First Post", ...>
在第一个评论上调用 post,它也是我们目前唯一的评论,应该会返回我们的第一个帖子。这证明了关联是双向有效的。如果你数据库中仍然有多个帖子,你也可以将此评论分配给另一个帖子:
2.1.0 :035 > **comment.post = Post.last**
=> #<Post id: 2, title: "Second Post", ...>
2.1.0 :036 > **comment.save**
=> true
将评论分配给另一个帖子会更新评论的 post_id,但不会写入数据库。更新 post_id 后别忘了调用 save!如果你犯了这个常见错误,评论的 post_id 实际上是不会改变的。
总结
本章对 Active Record 进行了快速概览,因此在控制台中练习,直到你对这些概念感到熟悉为止。增加更多的帖子,更新现有帖子的正文,并为这些帖子创建评论。特别关注 CRUD 操作和关联方法。这些方法在所有 Rails 应用程序中都很常用。
下一章将介绍 Rails 控制器。在那里,你将看到所有这些方法在实际操作中是如何使用的,随着你一步步完成各种控制器动作。
练习
| 问题: | 1. 或许我们想联系一下那些在我们博客上留言的人。生成一个新的迁移,为评论表添加一个字符串列,用来存储电子邮件地址。运行这个迁移,并使用 Rails 控制台验证你现在是否可以为评论添加电子邮件地址。 |
|---|---|
| 问题: | 2. 我们需要确保用户在创建评论时实际上输入了一些文字。为评论模型中的author和body字段添加验证。 |
| 问题: | 3. 写一个查询来确定每个帖子拥有的评论数量。你不能通过一个查询来完成,但你应该能够通过遍历帖子集合(就像它是一个数组一样)来找到答案。 |
第四章. 控制器
Rails 的控制器连接应用程序的模型和视图。应用程序接收到的任何网页请求都会被路由到相应的控制器。控制器从模型获取数据,然后呈现适当的视图或重定向到其他位置。
在本章中,我们将继续开发我们的博客。在这个过程中,你将详细了解控制器。我将介绍使用 REST 进行资源表示、资源路由以及控制器可以执行的操作类型。
表现层状态转移
表现层状态转移,或称REST,是一种由 HTTP 规范的作者之一罗伊·菲尔丁博士于 2000 年提出的客户端-服务器软件架构。REST 处理资源的表示,在 Rails 中,资源对应于模型。在 RESTful 架构中,客户端发起请求到服务器,服务器处理请求并将响应返回给客户端。在 Rails 应用中,处理请求并返回响应的服务器是控制器。控制器通过一组常见的 URL 和 HTTP 动词与客户端交互。
你可能已经至少熟悉这两种 HTTP 动词。请求一个网页有时被称为GET请求。GET 请求不会改变应用程序的状态;它仅仅返回数据。当你在网页上提交表单数据时,结果通常是一个POST请求。在使用 REST 的应用程序中,POST 请求用于在服务器上创建记录。
在我们上一章讨论模型时,你了解了 CRUD(创建、读取、更新和删除)。REST 使用表 4-1 中列出的四个 HTTP 动词,分别对应这些操作。
表 4-1. 数据库操作与 HTTP 动词的映射
| 数据库操作 | HTTP 动词 |
|---|---|
创建 |
POST |
读取 |
GET |
更新 |
PATCH |
删除 |
DELETE |
你的应用程序会根据所使用的 HTTP 动词来决定如何处理请求。对于一个资源的 GET 请求,它会返回对应模型的数据;PATCH 请求会用新信息更新模型;DELETE 请求则销毁模型。所有这三种操作都使用相同的 URL,只有 HTTP 动词不同。
Rails 应用程序在表 4-1 中列出的四个 CRUD 操作之外,还增加了三种操作。index操作显示所有资源的列表;new操作显示用于创建新资源的表单;edit操作显示用于编辑现有资源的表单。
每个操作在 Rails 控制器中都有一个对应的方法。这七个方法在表 4-2 中进行了总结。
表 4-2. 默认的 RESTful 操作
| 操作 | 描述 | HTTP 动词 |
|---|---|---|
index |
列出所有记录 | GET |
show |
显示一条记录 | GET |
new |
显示创建记录的表单 | GET |
edit |
显示编辑记录的表单 | GET |
create |
创建一条新记录 | POST |
update |
更新现有记录 | PATCH |
destroy |
删除记录 | DELETE |
我们将在本章中涵盖这些操作,但首先让我们看看 URL 是如何生成的。
路由
设置所有这些 URL 并将动作映射到动词听起来可能有点复杂,但幸运的是,Rails 路由会为你处理这一切。路由将 URL 连接到组成应用程序的代码。首先,让我们看看最常见的路由类型——资源路由。
资源
应用程序的路由存储在config/routes.rb文件中。请在文本编辑器中打开该文件。
忽略所有注释。现在,你的文件应该只有三行:
Rails.application.routes.draw do
resources :posts
end
Rails 应用程序默认使用 REST。博客应用目前只有一个资源(博客文章),单行的resources :posts为应用程序构建了一组路由。使用rake命令来显示应用程序的路由:
$ **bin/rake routes**
Prefix Verb URI Pattern Controller#Action
posts GET /posts(.:format) posts#index
POST /posts(.:format) posts#create
--*snip*--
此命令输出每个七个默认 RESTful 操作的路由帮助器前缀、HTTP 动词、URL 模式和控制器操作。
例如,对/posts的 GET 请求会调用PostsController#index方法。当你修改路由文件时,再次运行此命令以查看应用程序的路由如何变化。
嵌套资源
当一个资源属于另一个资源时,可以将其作为嵌套资源添加。在博客中,评论属于文章。以下是在config/routes.rb中表示这种关系的方式:
resources :posts **do**
**resources :comments**
**end**
在resources :posts后添加一个do、end块。然后在该块中添加resources :comments。这告诉 Rails,评论仅在文章内可用。
限制资源
像你刚才看到的那样,添加resources :comments会为评论创建七个默认 RESTful 操作的路由。现在,我们只关心创建新的评论。你可以通过在config/routes.rb中为该资源添加only子句来限制生成的路由集:
resources :posts do
resources :comments**, only: :create**
end
现在,只有评论的create操作被映射到一个 URL。你应该只为计划实现的操作提供路由。
自定义路由
你的应用程序中的某些操作可能与七个默认操作不对应。例如,应用程序可能包含一个search操作,返回包含特定术语的博客文章列表。在这种情况下,Rails 允许你手动配置自定义路由。
自定义路由对于将旧的 URL 映射到新的 Rails 应用程序,或简化复杂操作的 URL 也非常有用。例如,假设你的应用程序允许用户通过创建一个新的会话来登录,通过销毁会话来注销。添加 resources :user_session 会创建类似 user_session/new 的路径。如果你更愿意使用不同的路径,可以为 login 和 logout 创建自定义路由。
Rails.application.routes.draw do
resources :posts do
resources :comments, :only => :create
end
**get 'login' => 'user_sessions#new'**
**post 'login' => 'user_session#create'**
**delete 'logout' => 'user_sessions#destroy'**
end
现在你的应用程序的登录页面应该位于路径 /login。当用户访问登录页面时,浏览器会发送一个 GET 请求到这个路径。控制器会响应该 GET 请求并显示登录表单。当用户提交表单时,浏览器会向相同的路径发送一个 POST 请求,并包含表单内容。然后,控制器会响应该 POST 请求并为用户创建一个新的会话。当用户点击注销按钮时,浏览器会向路径 /logout 发送一个 DELETE 请求,销毁用户的会话。
我们没有为博客应用程序添加身份验证,但如果你想查看创建的路由,仍然可以将这些路由添加到 config/routes.rb 中。如果你不想继续,删除它们,因为访问与控制器动作不对应的路径会导致错误。
根路由
最后,让我们创建一个 根路由,这样我们就不需要每次都在浏览器地址栏中输入 /posts 了。根路由为你的应用程序设置了首页。在 config/routes.rb 的末尾添加 root 'posts#index':
Rails.application.routes.draw do
resources :posts do
resources :comments, :only => :create
end
**root 'posts#index'**
end
现在,在没有路径的情况下访问服务器应该显示文章索引页面。你应该始终为你的应用程序包含一个根路由。
路径和 URL
添加路由还会自动为你的控制器和视图创建助手。你可以使用这些助手,参见 表 4-3,而无需在应用程序中手动输入 URL。这样,如果你将来决定更改应用程序的 URL,就不必在代码中查找并更新所有旧的 URL。
表 4-3. Rails 路径和 URL 助手
| 路径助手 | URL 助手 |
|---|---|
posts_path |
posts_url |
new_post_path |
new_post_url |
edit_post_path(id) |
edit_post_url(id) |
post_path(id) |
post_url(id) |
路径助手 仅包含路径,而 URL 助手 还包括协议、服务器和端口(如果不是标准的)。Rails 应用程序通常使用路径助手。URL 助手在需要完整 URL 的情况下非常有用,例如生成用于电子邮件中的 URL。
每个方法名称的第一部分与 bin/rake routes 命令显示的前缀匹配。
你可以在 Rails 控制台中测试这些助手,方法如下:
2.1.0 :001 > **app.posts_path**
=> "/posts"
2.1.0 :002 > **app.post_path(1)**
=> "/posts/1"
2.1.0 :003 > **app.new_post_path**
=> "/posts/new"
2.1.0 :004 > **app.root_path**
=> "/"
在使用 Rails 路由时,测试这些助手是一个有用的检查方法。如果你忘记使用哪个助手来创建路径,可以在控制台中输入它以查看结果。
控制器动作
在 Rails 中的约定是每个资源对应一个控制器。该控制器包含每个动作的方法。(记住第二章中的原则:约定优于配置。)Rails 脚手架生成器为帖子创建了一个控制器。打开文件 app/controllers/posts_controller.rb,查看这些方法背后的 Ruby 代码。建议在继续阅读本章的过程中运行 Rails 服务器:
$ **bin/rails server**
现在,让我们逐个查看每个控制器方法,从 index 开始,依次查看 destroy。
index 动作从数据库中检索所有帖子:
def index
@posts = Post.all
end
你会在该方法中看到熟悉的 @post = Post.all。你可能会惊讶地发现,这就是 index 方法中的唯一一行代码。默认情况下,Rails 会渲染与动作名称匹配的视图文件,在本例中是 app/views/posts/index.html.erb。(我们将在下一章讨论视图。)
在浏览器中访问 http://localhost:3000/posts,查看 index 动作的结果。
show 动作从数据库中检索单个帖子,但 show 方法本身没有任何代码:
def show
end
此方法依赖于一个 Rails before_action,你应该能在控制器的第二行看到它:
before_action :set_post, only: [:show, :edit, :update, :destroy]
before_action 是一个类方法,它会在 show、edit、update 和 destroy 方法之前自动调用 set_post 方法,避免在这些方法中重复代码。(记住 DRY:不要重复自己。)
def set_post
@post = Post.find(params[:id])
end
set_post 方法在控制器的底部附近定义,并使用 private 关键字。它调用 Post.find 方法,检索与传递给控制器的参数 id 相对应的帖子。参数将在下一节中更详细地讨论,所以现在我们继续查看这些控制器方法。
new 动作显示一个表单,用于添加新帖子:
def new
@post = Post.new
end
表单使用新创建的帖子的数据。在帖子索引页面底部点击 新建帖子 链接,查看此表单。
edit 动作显示一个表单,用于编辑现有的帖子。像 show 方法一样,这个方法不包含任何代码:
def edit
end
此表单使用之前讨论过的 set_post 方法检索的数据。
动作的简要偏离
在讨论 create、update 和 destroy 之前,让我们先了解一些关键的 Rails 主题,这些是理解这些方法所必需的。在本节中,我们将探讨参数、渲染/重定向、响应格式和 flash。
参数
参数 通常表示用于请求页面或表单值的 URL 部分,它们可以在控制器中作为名为 params 的哈希访问。例如,你之前看到的 set_post 方法从 params 哈希中检索请求的帖子的 id,如下所示:
@post = Post.find(params[:id])
你可以在终端中通过rails server命令的输出看到每个请求传递的参数。例如,访问http://localhost:3000/posts/1,然后查看终端中 Rails 服务器的输出:
Started GET "/posts/1" for 127.0.0.1 at 2014-03-31 20:30:03 -0500
Processing by PostsController#show as HTML
➊ Parameters: {"id"=>"1"}
Post Load (0.3ms) SELECT "posts".* FROM "posts"
WHERE "posts"."id" = ? LIMIT 1 [["id", "1"]]
Rendered posts/show.html.erb within layouts/application (233.9ms)
Completed 200 OK in 274ms (Views: 245.5ms | ActiveRecord: 26.2ms)
在这种情况下,URL 中的1表示请求帖子➊的id。因为我们请求的是单个帖子,所以调用show方法,并且这个id用于在set_post中查找帖子。
表单数据由一个嵌套哈希表示,哈希包含值。例如,编辑这个帖子会生成一个更像这样的params哈希:
{
"utf8"=>"✓",
"authenticity_token"=>"...",
➊ "post"=>{"title"=>"First Post", "body"=>""},
"commit"=>"Update Post",
➋ "id"=>"1"
}
你仍然可以访问params[:id] ➋来查找正确的帖子,也可以访问params[:post] ➊来查看用户提交的新值。因为这些是用户提交的值,你应确保应用程序仅接受适当属性的数据。恶意用户可能会发送带有无效参数的请求,试图攻击你的应用程序。
对于博客帖子,你只希望用户能够编辑title和body属性。Rails 包括一个名为强参数(Strong Parameters)的功能,使得指定应用程序接受哪些属性变得很容易。你可以在post_params方法中看到这个功能的实际应用:
def post_params
params.require(:post).permit(:title, :body)
end
该方法首先要求params哈希包含一个嵌套哈希,键为:post。然后,它只返回该嵌套哈希中允许的值(:title和:body)。使用前面的params哈希示例,post_params返回一个类似这样的哈希:
{"title" => "First Post", "body" => ""}
params[:post]哈希中的其他值会被默默忽略。记住,访问新创建或更新的帖子参数时,始终使用post_params方法。
渲染或重定向
每个动作必须渲染一个视图或重定向到另一个动作。默认情况下,动作会渲染与动作名称匹配的文件。例如,帖子控制器中的show方法会查找一个名为app/views/posts/show.html.erb的文件,并使用该文件构建返回给用户的 HTML 响应。
你可以通过render方法告诉 Rails 渲染不同动作的响应,方式如下:
render action: "edit"
指定动作的能力在需要根据用户输入呈现不同视图时非常有用。这个例子来自update方法。如果帖子无法使用用户提供的数据更新,该方法会重新渲染edit视图,给用户一个机会来修正数据。
有时你需要将用户重定向到他或她请求之外的页面。使用redirect_to方法来处理这种情况。例如,如果用户在创建或更新帖子时输入了有效数据,控制器动作会将用户重定向到该帖子:
redirect_to @post
当你调用redirect_to时,用户浏览器中的地址会更改,以反映新页面,并且会发出另一个请求。你可以通过提交表单数据时观察地址栏并查看终端中rails server的输出来看到这一点。
要查看此功能,首先在浏览器中访问http://localhost:3000/posts/new。这是新建文章的表单。输入新文章的标题,然后点击创建文章按钮。点击按钮后请密切关注地址栏。
表单向http://localhost:3000/posts发起 POST 请求。该请求会被路由到create方法。创建完文章后,你会被重定向到http://localhost:3000/posts/3,假设你的新文章的id是 3。地址会通过redirect_to方法自动更改。
响应格式
Rails 可以生成多种格式的响应,尽管到目前为止我们讨论的都是 HTML 格式。通过脚手架生成的控制器也可以包含 JavaScript 对象表示法(JSON)响应,这对于创建应用程序编程接口(API)非常有用。其他格式包括 XML 甚至 PDF。
你可以通过访问这个网址来尝试另一种响应类型:http://localhost:3000/posts.json。这个网址与之前使用的文章索引网址相同,只不过末尾加了.json。Rails 会识别这是一个 JSON 请求,并将文章集合以 JSON 格式呈现,如图 4-1 所示。

图 4-1. 文章以 JSON 格式呈现
你可以通过调用respond_to方法指定一个操作接受的格式以及每种格式的响应。这个方法接受一个块,块中有一个表示请求格式的单一参数。以下是destroy方法中的示例:
respond_to do |format|
format.html { redirect_to posts_url }
format.json { head :no_content }
end
这个方法在文章被销毁后立即调用。如果客户端请求 HTML 数据,这个代码块会重定向到posts_url,即索引页面。如果客户端请求 JSON 数据,添加.json到 URL 末尾,这个代码块会返回一个空的头部,表示该文章已不存在。
闪存
闪存消息是向用户发送的提示信息,仅在单次请求中有效。闪存消息通常存储在用户的会话中,通常是一个 cookie。它们通常使用不同的样式以便突出显示。例如,Rails 脚手架附带的样式表使用绿色文字来显示闪存消息。
闪存消息对于向用户发送错误信息或其他通知非常有用。它们通常在重定向时设置。以下是来自文章控制器create方法的示例:
redirect_to @post, notice: 'Post was successfully created.'
当文章创建成功后,用户会被重定向到新文章页面,并显示类似图 4-2 中的闪存消息。

图 4-2. 一条闪存消息
create闪存消息是绿色文字,且与之前添加的消息相匹配。
返回控制器操作
现在,你应该了解了足够的信息,能够理解 create、update 和 destroy 动作。由 scaffolding 生成的这些方法响应 HTML 和 JSON 数据的请求,并返回指示成功或错误的消息,但现在我们先专注于 HTML 响应。关于 JSON 响应的内容,我将在讲解如何构建你自己的 API 时详细讨论。
注意
每个方法中的格式已稍作调整,以更好地适应此页面。
create 方法负责使用新帖子表单中的 params 来创建一个帖子:
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.html { redirect_to @post,
notice: 'Post was successfully created.' }
format.json { render action: 'show',
status: :created, location: @post }
else
format.html { render action: 'new' }
format.json { render json: @post.errors,
status: :unprocessable_entity }
end
end
end
方法的第一行 @post = Post.new(post_params) 使用强参数来确保只有允许的参数能够传递给 new。在 respond_to 块内部,检查 @post.save 的返回值。如果返回 true,则用户将被重定向到新创建的帖子;如果返回 false,则重新渲染 new 动作,用户可以修正任何错误。
update 方法与 create 方法类似,主要区别在于代码检查的是 @post.update 的返回值,而不是 @post.save。
def update
respond_to do |format|
if @post.update(post_params)
format.html { redirect_to @post,
notice: 'Post was successfully updated.' }
format.json { render action: 'show',
status: :ok, location: @post }
else
format.html { render action: 'edit' }
format.json { render json: @post.errors,
status: :unprocessable_entity }
end
end
end
如果 @post.update 返回 true,代码将重定向用户到更新后的帖子;否则,它将重新渲染编辑表单,以便用户修正错误。
destroy 方法比 create 和 update 方法更简单,因为它不检查 @post.destroy 的返回值。
def destroy
@post.destroy
respond_to do |format|
format.html { redirect_to posts_url }
format.json { head :no_content }
end
end
在帖子被销毁后,代码会将用户重定向回索引页面 posts_url。
添加评论
你之前已经为 create 评论动作添加了一个路由,现在让我们为这个动作添加一个简单的控制器。你将在下一章中添加用于输入新评论的表单。
使用 Rails 生成器生成一个新的评论控制器:
$ **bin/rails generate controller comments**
➊ create app/controllers/comments_controller.rb
invoke erb
➋ create app/views/comments
invoke test_unit
create test/controllers/comments_controller_test.rb
invoke helper
create app/helpers/comments_helper.rb
invoke test_unit
create test/helpers/comments_helper_test.rb
invoke assets
invoke coffee
create app/assets/javascripts/comments.js.coffee
invoke scss
create app/assets/stylesheets/comments.css.scss
注意,我只指定了控制器,而不是 scaffolding。这段代码会生成一个空的控制器 ➊ 和一个空的 views 目录 ➋,同时还会生成 helpers、tests 和 assets 的文件。我们需要自己填充细节。首先打开编辑器中的文件 app/controllers/comments_controller.rb:
class CommentsController < ApplicationController
end
因为你正在实现 create 动作,首先需要一个 create 方法。你可以将其模仿为帖子控制器中的 create 方法。假设用户不会通过 API 添加评论,因此不需要生成 JSON 响应。
class CommentsController < ApplicationController
**def create**
1 **@post = Post.find(params[:post_id])**
2 **if @post.comments.create(comment_params)**
3 **redirect_to @post,**
**notice: 'Comment was successfully created.'**
**else**
**redirect_to @post,**
**alert: 'Error creating comment.'**
**end**
**end**
end
这段代码首先使用 params 哈希中的 post_id 查找正确的帖子 ➊。然后,利用 comments 关联来创建一个新的评论 ➋,并重定向回该帖子 ➌。每次调用 redirect_to 都会设置一个闪存消息以指示成功或失败。
由于你在应用程序中使用了强参数,你还需要添加 comment_params 方法以指定你希望接受的参数。
class CommentsController < ApplicationController
--*snip*--
**private**
**def comment_params**
**params.require(:comment).permit(:author, :body)**
**end**
end
对于评论,你只接受 author 和 body。其他参数会被忽略。在下一章,你将更新帖子 show 视图以显示现有评论,并包括一个用于创建新评论的表单。
总结
本章介绍了许多重要的 Rails 概念——REST、路由和控制器。我还讨论了参数、渲染与重定向、响应格式以及闪存。
我们在上一章从数据库开始,在本章逐步向前推进。在下一章,我们将深入到用户部分,覆盖 MVC 的最后一块拼图:视图。
练习
| 问题: | 1. 良好的错误信息对于任何应用程序都至关重要。如果出现问题,用户需要知道问题所在以及如何修正。目前,如果评论无法创建,用户会看到“创建评论时出错”这一消息。请更新CommentsController create方法,确保在警告框中也能显示错误消息列表。 |
|---|---|
| 问题: | 2. 在第三章的练习 1 中,你向Comment模型添加了一个email字段。请更新CommentsController中的comment_params方法,使其也能够接收这个字段。 |
第五章 视图
视图是用户与应用程序交互的界面。通常,视图包括用于显示数据库记录的网页,以及用于创建和更新这些记录的表单。视图有时也可以是对 API 请求的响应。
本章介绍了最常用的 Rails 视图模板类型——嵌入式 Ruby,以及视图特定的助手和布局。你还将学习如何通过部分模板避免 HTML 代码重复,并且如何生成表单以接受用户输入。
现在输入bin/rails server来启动 Rails 服务器。并保持服务器在终端窗口中运行,当你在本章示例中进行操作时,这样你可以在浏览器中看到你对应用程序所做的更改,并查看服务器的输出。
嵌入式 Ruby
嵌入式 Ruby(ERB),是 Rails 中的默认模板类型,用于构建视图模板。嵌入式 Ruby 模板包含 Ruby 代码和 HTML 的混合,类似于 ASP、JSP 或 PHP。
模板存储在app/views的子目录中,该子目录以控制器的名称命名。例如,你会在app/views/posts目录中找到帖子控制器的模板。Rails 的约定是根据操作命名模板,并且文件扩展名为.html.erb。index操作的默认模板是index.html.erb。
嵌入式 Ruby 包含三种特殊的标签用于执行 Ruby 代码。这些标签用于输出、控制流和注释。让我们来看看每一种标签。
输出
<%= %>标签(也称为输出标签)执行其包含的代码并将返回值打印在页面上。打开文件app/views/posts/show.html.erb,查看这个标签的几个示例。
例如,这个标签打印当前帖子的标题:
<%= @post.title %>
请注意,标题中的任何 HTML 默认都会被转义。也就是说,任何保留字符都会被转换为字符引用,并显示在页面上,而不是被解释为 HTML。这个保护措施可以防止恶意用户在页面上输入 HTML 代码,从而导致页面崩溃,甚至是跨站脚本攻击。跨站脚本攻击和其他安全问题会在第十一章中详细讨论。
控制流
<% %>标签执行其包含的代码,但不会在页面上打印任何内容。这个标签对于控制流语句(如循环或条件语句)非常有用。打开文件app/views/posts/index.html.erb,查看此标签的实际应用。
这个示例使用each方法遍历一个帖子数组:
<% @posts.each do |post| %>
<tr>
<td><%= post.title %></td>
<td><%= post.body %></td>
<td><%= link_to 'Show', post %></td>
<td><%= link_to 'Edit', edit_post_path(post) %></td>
<td><%= link_to 'Destroy', post, method: :delete,
data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
输出标签在循环内用于打印post.title和post.body的值。这个示例还展示了三个link_to助手的使用。这些助手为每个帖子创建指向show、edit和destroy操作的链接。我们将在下一节讨论助手。
注释
最后,<%# %> 标签用于输入注释。注释 通常是给自己或其他程序员的说明,描述你的代码在做什么。与 HTML 注释(以 <!-- 开始,以 --> 结束)不同,ERB 注释不会出现在 HTML 源代码中。使用 ERB 注释来添加你不希望在由视图模板生成的 HTML 中看到的注释。
你到目前为止看到的 ERB 模板是我们在最初创建博客时通过 Rails 脚手架生成的。它们没有注释,但你可以轻松地添加自己的注释。这里是一个例子:
<%# This code is crazy %>
除了给程序员的注释外,你还可以使用 ERB 注释临时从页面中移除代码。在任何其他 ERB 标签中的第一个百分号符号(%)后面加上井号符号(#),该标签内的代码将不会被执行。
助手方法
Helpers 是 Ruby 方法,用于简化视图中的代码,使其更易于阅读。Rails 提供了用于创建资源链接、格式化数字和其他常见任务的助手方法。你也可以轻松编写自己的助手方法。
通过使用助手方法,你可以避免在视图中放置过多的逻辑。如果显示一个值需要超过一行代码,那么这段代码可能应该放在一个助手方法中。
URL 助手方法
使用 link_to 助手方法创建链接:
link_to 'Show', post
这个示例生成一个 HTML 链接,像这样:<a href="/posts/1"> Show</a>,假设 post 的 id 为 1。
你还可以使用在上一章中看到的 URL 和路径助手方法来创建链接:
link_to 'Edit', edit_post_path(post)
这个示例生成一个链接,像这样:<a href="/posts/1/edit">Edit</a>。
你还可以为链接指定使用的 HTTP 动词以及其他数据属性。对于那些会在服务器上更改状态的链接,例如删除资源的链接,可以使用这种方式。记住,GET 请求不应该用于更改状态。
link_to 'Destroy', post, method: :delete,
data: { confirm: 'Are you sure?'}
这个示例生成一个链接,使用data-method="delete"和data-confirm="Are you sure?"。Rails 默认包含了 jQuery 非侵入式 JavaScript 库(jquery_ujs.js)。这个库使用 method 和 confirm 属性,在运行时构建一个隐藏表单,创建一个确认窗口,然后使用适当的 DELETE 请求提交删除链接。你不需要自己做这些,难道不高兴吗?
注意
Web 浏览器只能发出 GET 和 POST 请求。Rails 通过传递名为 _method 且值为 delete 的参数来模拟 DELETE 请求。当你更新记录时,PATCH 请求也是以相同的方式处理的。
数字助手方法
Rails 提供了几个方便的用于显示数字的方法:
number_to_currency
number_to_human
number_to_human_size
number_to_percentage
number_with_delimiter
number_with_precision
每个方法接受一个数字,并返回一个字符串,该字符串表示该数字并应用了一些格式。这个格式与方法名称末尾的单词有关。
number_to_currency 方法,如下所示,将给定的数字四舍五入到小数点后两位,并在前面加上美元符号:
number_to_currency 100
所以这个示例返回"$100.00"。
number_to_human 和 number_to_human_size 方法将数字转换为易于阅读的字符串表示。
number_to_human 1000000
number_to_human_size 1024
所以这些示例分别返回 "100 万" 和 "1 KB"。
使用number_to_percentage格式化百分比。默认情况下,这个方法将数字四舍五入到小数点后三位,并添加一个百分号。你可以通过选项指定精度。
number_to_percentage 12.345
number_to_percentage 12.345, precision: 1
这些示例分别返回 "12.345%" 和 "12.3%"。
除了 URL 和数字助手外,Rails 还内置了用于处理日期和资源(如图片、CSS 文件和 JavaScript 文件)的助手方法。在本章后面,我将介绍用于创建表单和表单字段的助手方法。
我无法在这里介绍 Rails 中的所有助手方法,所以目前我们先来看一下如何添加你自己的助手方法。
自定义助手方法
你可以通过在 app/helpers 目录中的适当文件中添加方法,轻松创建自己的助手方法。Rails 的脚手架生成器会自动为你在该目录中创建一些几乎为空的文件。
将只在单个控制器中需要的助手方法添加到该控制器的助手文件中。例如,只有在帖子视图中使用的助手方法应添加到 app/helpers/posts_helper.rb 中的 PostsHelper 模块。
将应用程序中使用的助手方法添加到ApplicationHelper模块中,文件路径是 app/helpers/application_helper.rb。打开这个文件,看看它是如何工作的:
module ApplicationHelper
**def friendly_date(d)**
**d.strftime("%B %e, %Y")**
**end**
end
这段代码定义了一个新的助手方法friendly_date。你可以在应用程序中的任何视图中使用这个方法来格式化日期以供显示。
friendly_date Time.new(2014, 12, 25)
这个示例返回 "2014 年 12 月 25 日"。如果你之后决定在整个应用程序中以不同格式显示日期,只需更改这个方法,而不需要更改所有的视图。
帖子索引页面
现在你对 Rails 中的视图工作原理有了更多了解,让我们更新索引视图,使其更像一个博客。打开浏览器并访问 http://localhost:3000/posts 查看索引页面,参见图 5-1。

图 5-1. 帖子索引页面
你的博客帖子当前是以表格的形式展示的。打开文件 app/views/posts/index.html.erb 以编辑:
<h1>Listing posts</h1>
➊ <table>
<thead>
<tr>
<th>Title</th>
<th>Body</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
➋ <% @posts.each do |post| %>
<tr>
<td><%= post.title %></td>
<td><%= post.body %></td>
<td><%= link_to 'Show', post %></td>
<td><%= link_to 'Edit', edit_post_path(post) %></td>
<td><%= link_to 'Destroy', post, method: :delete,
data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Post', new_post_path %>
这个模板首先创建一个 HTML 表格 ➊ 并向页面添加一个表头。然后它遍历每个帖子 ➋ 并在表格行中显示该帖子的属性。
一个合适的博客应将每个帖子标题作为标题显示,接着是帖子的正文,以段落的形式展示。更新索引视图,使其看起来像这样:
<h1>Listing posts</h1>
➊ **<% @posts.each do |post| %>**
➋ **<h2><%= link_to post.title, post %></h2>**
➌ **<p><i><%= friendly_date post.created_at %></i>**
**<p><%= post.body %>**
➍ **<p>**
**<%= link_to 'Edit', edit_post_path(post) %>**
**<%= link_to 'Destroy', post, method: :delete,**
data: { confirm: 'Are you sure?' } %>
**</p>**
**<% end %>**
<br>
<%= link_to 'New Post', new_post_path %>
模板仍然像以前一样循环遍历每个帖子 ➊。然而,它现在不再在表格单元格中显示帖子属性,而是将 title ➋ 显示为二级标题,并使用你在前一节中添加的 friendly_date 帮助器 ➌ 来格式化 created_at 日期。用于 edit 和 destroy 帖子的链接 ➍ 现在位于底部,而 show 帖子的链接现在围绕帖子 title。刷新浏览器中的页面查看更改,具体内容见图 5-2。

图 5-2. 更新后的帖子索引页面
我们的博客虽然还不能获得任何设计奖项,但它已经看起来更好了!
布局
你可能已经注意到,至今为止你看到的视图只包含了网页的内容,而没有其他必需的元素,如 html、head 和 body。这些元素是所有网页的基本结构。
检查终端中的服务器输出,看看在加载索引页面时发生了什么:
--*snip*--
➊ Started GET "/posts" for 127.0.0.1 at 2014-03-09 18:34:40 -0500
➋ Processing by PostsController#index as HTML
Post Load (0.2ms) SELECT "posts".* FROM "posts"
➌ Rendered posts/index.html.erb within layouts/application (62.5ms)
Completed 200 OK in 92ms (Views: 91.2ms | ActiveRecord: 0.2ms)
--*snip*--
这里,我们有一个 GET 请求 ➊,路径为 /posts。它由 PostsController 中的 index 方法 ➋ 处理。最后,服务器在 layouts/application 中渲染 posts/index.html.erb ➌。
在 Rails 中,布局 是包含每个页面所需基本 HTML 的文件。你无需在每个视图中重复相同的 HTML,而是只需在布局文件中写一次。这是 Rails 去除不必要重复的另一种方式。
让我们直接进入并解析你的博客的布局。服务器输出将其称为 layouts/application,因此打开 app/views/layouts/application.html.erb 以查看你应用的布局:
➊ <!DOCTYPE html>
<html>
<head>
<title>Blog</title>
<%= stylesheet_link_tag 'application', media: 'all',
➋ 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application',
➌ 'data-turbolinks-track' => true %>
➍ <%= csrf_meta_tags %>
</head>
<body>
➎ <%= yield %>
</body>
</html>
这个文件包含了你网站上每个页面的基本 HTML:HTML5 doctype ➊,然后是 head 部分和 body 部分。
head 部分设置页面的标题。接着,它包括 Rails 帮助器,用于链接你网站的 CSS ➋ 和 JavaScript ➌ 文件。它还包含一个帮助器 ➍,用于保护你的应用免受 跨站请求伪造(CSRF) 攻击,具体内容我将在第十一章中讲解。body 部分包括 yield 语句 ➎。
本节的其余部分涵盖了这些助手方法和 yield 语句。
资源标签助手
在 Rails 应用中,CSS、JavaScript 和图像等文件被称为 资产。资产是访问你应用的 Web 浏览器所需的外部文件。这些文件存储在 app/assets 目录的子目录中。
随着应用的增长,你可能需要多个 CSS 和 JavaScript 文件来控制网站的外观和客户端功能。Rails 服务器输出还列出了你的应用当前正在使用的 CSS 和 JavaScript 文件:
--*snip*--
➊ Started GET "/assets/scaffolds.css?body=1" for 127.0.0.1 at ...
➊ Started GET "/assets/application.css?body=1" for 127.0.0.1 at ...
➋ Started GET "/assets/turbolinks.js?body=1" for 127.0.0.1 at ...
➋ Started GET "/assets/jquery.js?body=1" for 127.0.0.1 at ...
➋ Started GET "/assets/posts.js?body=1" for 127.0.0.1 at ...
➋ Started GET "/assets/jquery_ujs.js?body=1" for 127.0.0.1 at ...
➋ Started GET "/assets/application.js?body=1" for 127.0.0.1 at ...
➊ Started GET "/assets/posts.css?body=1" for 127.0.0.1 at ...
如你所见,我们简单的博客已经使用了三个不同的 CSS 文件 ➊ 和五个 JavaScript 文件 ➋。Rails 并没有在布局中单独列出这些文件,而是使用名为 manifests 的 CSS 和 JavaScript 文件来引入各个独立的 CSS 和 JavaScript 文件。一个 manifest 文件实际上只是列出了你的应用程序所需的其他文件。
Rails 的一项功能,称为 资产管道,将这些 CSS 和 JavaScript 文件合并为两个文件,并在生产环境中运行时进行压缩。这些文件分别命名为 application.css 和 application.js。通过合并这些文件,你的应用程序可以减少来自用户的请求,从而提高性能。
布局的 head 部分包含了用于添加应用程序所需的 CSS 和 JavaScript manifest 文件的 ERB 标签。
stylesheet_link_tag
stylesheet_link_tag 方法为默认的 CSS manifest 文件 application.css 和 manifest 中引用的每个 CSS 文件添加了一个 HTML 链接标签。打开文件 app/assets/stylesheets/application.css 来查看它是如何工作的。
/*
--*snip*-
*
➊ *= require_tree .
➋ *= require_self
*/
该文件以一段注释开始,解释其目的,以及以 require_tree ➊ 和 require_self ➋ 开头的行。require_tree 语句包含了 app/assets/stylesheets 目录和子目录下的所有其他 CSS 文件。require_self 语句则意味着该 CSS 文件的内容会被包含在文件的底部。
javascript_include_tag
javascript_include_tag 方法为默认的 JavaScript manifest 文件 application.js 和 manifest 中列出的每个 JavaScript 文件添加了一个 script 标签。现在,打开 JavaScript manifest 文件 app/assets/javascript/application.js。
--*snip*-
//
//= require jquery
//= require jquery_ujs
//= require turbolinks
//= require_tree .
这个文件类似于 CSS manifest 文件。它以一段注释开始,解释其目的,然后默认包含了 jquery、jquery_ujs 和 turbolinks 三个 JavaScript 库,以及 app/assets/javascript 目录和子目录下的其他 JavaScript 文件。
Note
资产管道、turbolinks 和其他性能问题将在 第十二章 中更详细地讨论。
CSRF Meta Tags Helper
csrf_meta_tags 方法将两个 meta 标签添加到每个网页的 head 部分。这些标签旨在保护你的应用免受 CSRF 攻击。
如果你查看应用程序任何页面的源代码,你应该会看到一个名为 csrf-token 的 meta 标签,其中包含一长串随机的十六进制数字。这个令牌是唯一的,且与当前会话相关,每次提交表单时都会传递给你的应用程序。
<meta content="authenticity_token" name="csrf-param" />
<meta content="..." name="csrf-token" />
在 CSRF 攻击中,应用程序的一个可信用户访问了一个恶意网站。该恶意网站随后尝试以该可信用户的身份向你的应用程序提交请求。由于恶意网站无法知道这个秘密令牌,因此这些请求会失败。CSRF 和其他安全问题将在 第十一章 中详细讨论。
Yield
在布局中,yield 语句标识了视图中内容应该插入的位置。在这种情况下,app/views/posts/index.html 生成的 HTML 会插入到 body 标签之间,形成发送给用户的完整网页。
yield 语句不一定是 body 元素中的唯一语句。你可以根据需要向 body 中添加其他元素。例如,你可以在这里添加一个常见的页眉或页脚,出现在你应用程序的每个页面上。
部分模板
与帮助器一样,部分模板 用于将代码提取到有意义的单元中,避免在多个视图中重复相同的代码。不同之处在于,帮助器包含共享的 Ruby 代码,而部分模板包含共享的 HTML 代码。
部分模板存储在视图模板中,文件名以下划线开头。例如,app/views/posts/_form.html.erb 是一个渲染帖子表单的部分模板。
在多个页面中重复的代码通常会被分离到部分模板中,以使模板代码更易于理解。如果你查看新的帖子和编辑帖子模板,app/views/posts/new.html.erb 和 app/views/posts/edit.html.erb,你会看到它们都用这行代码渲染相同的表单部分:
<%= render 'form' %>
这里,部分模板的名称是 _form.html.erb,但在渲染时只是简单地称为 form。
如果你发现自己在多个页面中,或者在单个页面的多个位置重复相同的 HTML 代码,你应该将这段代码复制到一个部分模板中,并用 render 语句替换它。
集合
部分模板也可以用来消除视图模板中的循环。当你使用 :collection 选项时,模板中会为集合中的每个成员插入一个相应的部分。使用 :collection 不一定能完全消除代码重复,但它可以简化模板。
例如,你可以将 index.html.erb 中 <% @posts.each ... %> 块内的代码移动到一个名为 app/views/posts/_post.html.erb 的新文件中。然后你可以用一行类似这样的代码替换该块:
<%= render :partial => 'post', :collection => @posts %>
在这个例子中,Rails 知道 @posts 是一个包含帖子对象的数组,因此它会查找一个名为 app/views/posts/_post.html.erb 的部分,并在页面上为数组中的每个对象渲染一次。因为这个操作非常常见,你可以进一步简化为:
<%= render @posts %>
让我们通过向帖子展示页面添加评论来实践部分模板的使用。
显示评论
你在第三章添加了评论模型,在第四章添加了控制器,但你仍然无法在页面上看到它们。几乎每个帖子都应该有评论,而你不希望在每个页面中都重复这段代码,因此这是一个将部分模板应用于此的绝佳机会。
要开始使用部分模板,打开 app/views/posts/show.html.erb 文件:
<p id='notice'><%= notice %></p>
<p>
<strong>Title:</strong>
<%= @post.title %>
</p>
<p>
<strong>Body:</strong>
<%= @post.body %>
</p>
<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>
首先像处理帖子索引页面一样清理一下这个页面,将title包裹在标题标签中,将body包裹在段落标签中,如下所示:
<p id='notice'><%= notice %></p>
**<h2><%= @post.title %></h2>**
**<p><%= @post.body %></p>**
<%= link_to 'Edit', edit_post_path(@post) %> |
<%= link_to 'Back', posts_path %>
现在在页面底部添加一个标题和用于渲染评论的语句:
--*snip*--
**<h3>Comments</h3>**
**<%= render @post.comments %>**
这段代码通过使用部分模板渲染@post.comments集合,显示了标题下的评论。为了使其生效,你还需要为渲染单个评论创建一个部分模板。创建一个名为app/views/comments/_comment.html.erb的新文件,内容如下:
**<p><%= comment.author %> said:</p>**
**<blockquote>**
**<%= comment.body %>**
**</blockquote>**
如果你之前通过 Rails 控制台添加了评论,现在应该可以在页面底部看到它们。当然,你不能要求用户通过控制台添加评论;他们期望看到一个评论表单。让我们来看看在 Rails 应用程序中是如何创建表单的。
表单
接受用户输入可能是构建 web 应用程序中最困难的部分之一。Rails 包含了一个优雅的系统,用于生成表单。
Rails 提供了各种表单控件的辅助方法。当绑定到模型时,这些辅助方法会自动生成正确的 HTML 标记,以将值传递回控制器。
在浏览器中访问http://localhost:3000/posts/new,查看由 Rails scaffold 生成的“新建帖子”表单,如图 5-3 所示。

图 5-3. 新建帖子表单
这个简单的表单包含一个用于输入帖子title的文本框,一个用于输入帖子body的文本区域,以及一个标有“创建帖子”的按钮,用于提交表单。
表单辅助方法
你可以使用辅助方法生成表单以及所有必要的字段和标签。打开文件app/views/posts/_form.html.erb,查看 Rails 表单的示例:
➊ <%= form_for(@post) do |f| %>
➋ <% if @post.errors.any? %>
➌ <div id="error_explanation">
<h2><%= pluralize(@post.errors.count, 'error') %>
prohibited this post from being saved:</h2>
<ul>
<% @post.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
➍ <div class='field'>
<%= f.label :title %><br>
<%= f.text_field :title %>
</div>
<div class='field'>
<%= f.label :body %><br>
<%= f.text_area :body %>
</div>
<div class='actions'>
<%= f.submit %>
</div>
<% end %>
这个部分模板用于创建新帖子和编辑现有帖子。表单以调用form_for方法 ➊开始,后续表单内容放在一个块中。接下来,if语句 ➋ 检查帖子是否包含任何错误。如果表单存在错误,error_explanation div ➌ 会在表单的其余部分之前显示。否则,这里什么也不会显示。最后,你会看到表单控件 ➍。
表单错误
首先,我们来看一下用于显示错误的代码,类似于图 5-4 中所示的错误。记得我们在讨论控制器时提到过,如果create或update操作失败,表单会重新渲染。尝试创建一个没有标题的新帖子,看看错误信息。

图 5-4. 帖子创建错误
图 5-4 显示了error_explanation div,其中包含错误数量的标题,并后跟实际错误的项目符号列表。此外,title字段的标签现在具有红色背景,title的文本框也被红色边框围绕。Rails 通过将这些元素包装在一个类为field_with_errors的div中来实现这一点。
现在你已经知道如何显示错误,让我们看看form_for方法以及其他用于创建表单控件的助手方法。
表单控件
使用form_for块来创建绑定到模型的表单。例如,这个表单绑定到存储在@post中的模型:
<%= form_for(@post) do |f| %>
在这个块中,你可以使用辅助方法来添加控件,例如标签、文本框和按钮。使用表单构建器对象(在这个例子中是f)来调用这些方法。
<%= f.label :title %>
label助手用于为指定字段创建标签。上面的语句会生成如下 HTML:<label for="post_title"> Title</label>。Rails 会将字段名转换为字符串并将第一个字母大写。用户可以点击该标签,将光标聚焦到标题的文本框中。当然,你还需要创建那个文本框,Rails 也提供了相应的助手。
<%= f.text_field :title %>
text_field助手生成以下 HTML:<input id="post_title" name="post[title]" type="text" />。请注意,这个输入框的id(post_title)与前面段落中的标签的for值匹配。同时注意这个字段的名称。Rails 会为表单字段设置名称,以表示模型(post)和要修改的属性(title)。
接下来的几行代码添加了一个用于帖子body的label,并在其后添加了一个用于输入body文本的text_area。这些控件与title字段的工作方式相同。text_area助手会生成如下 HTML:<textarea id="post_body" name="post[body]"></textarea>。
除了输入title和body文本的控件外,你还需要一个按钮来提交表单:
<%= f.submit %>
submit助手生成一个提交按钮。按钮的标签基于当前模型的类名以及该模型是否已保存到数据库。如果是新帖子,则值为"Create Post",对应的 HTML 如下:<input name="commit" type="submit" value="Create Post" />。如果帖子已经保存到数据库中,则值为"Update Post"。
Rails 为你需要的每个字段都包括了表单助手,你也可以随时添加自己的助手来创建自定义字段。内置的示例包括check_box、hidden_field、password_field、radio_button和text_area。
还包括 HTML5 字段类型的助手方法,例如email_field、phone_field和url_field。这些字段看起来像普通的文本框,但在移动设备上会显示不同的键盘。使用这些字段类型可以确保你的应用程序是移动友好的。
评论表单
现在让我们利用你新学的表单知识,添加评论表单。首先,在app/views/posts/show.html.erb文件的帖子显示页面末尾添加另一个标题:
<h4>New Comment</h4>
在新标题下方添加用于创建评论的表单,如下所示。传递给form_for方法的数组包含@post和@post.comments.build。因为每个评论都属于一个帖子,所以你必须将帖子和评论一起传递给该方法。在这种情况下,你正在使用当前帖子和由@post.comments.build创建的新评论。
<%= form_for [@post, @post.comments.build] do |f| %>
<div class='field'>
<%= f.label :author %><br>
<%= f.text_field :author %>
</div>
<div class='field'>
<%= f.label :body %><br>
<%= f.text_area :body %>
</div>
<div class='actions'>
<%= f.submit %>
</div>
<% end %>
其余的评论表单应该看起来与发布表单相似;即使是字段名称也一样。在浏览器中刷新页面,确保表单像图 5-5 中所示那样呈现。

图 5-5. 新评论表单
现在输入作者名和评论正文,然后点击创建评论按钮。提交表单后,应该显示你的新评论,并在页面顶部添加一条闪烁消息,显示“评论已成功创建”。
检查终端中rails server命令的输出,查看到底发生了什么。假设你的帖子id为 1,你应该首先看到对路径/posts/1/comments的 POST 请求。这会调用CommentsController#create方法。
你在上一章中添加了这个控制器和方法;回想一下,create方法创建并保存一个新评论,然后将用户重定向回帖子页面。你应该能在输出中看到这次重定向,它是对/posts/1的 GET 请求。这发生在用户被重定向回帖子显示页面时。
总结
花些时间处理应用程序的视图。我们稍微清理了一下索引页面,但我建议你进一步改善它。其他页面也可以做一些改进。以下练习应能给你一些灵感。
在下一章中,你将设置 Git 进行版本控制,并将应用部署到网络上,让所有人都能看到。
练习
| 问题: | 1. 我们博客的标题只出现在index页面。将h1元素从帖子索引页面移动到应用程序布局中。在此过程中,想出比“列出帖子”更有趣的标题。另外,将“新建帖子”和“编辑帖子”页面上的h1标题更改为h2标题。 |
|---|---|
| 问题: | 2. 在第三章中,你向帖子表添加了一个author字段。请在帖子表单中添加一个文本字段用于author,并在PostsController中更新post_params方法,允许author作为参数。 |
| Q: | 3. 用户现在可以创建评论,但无法删除它们。你需要能够删除不可避免的垃圾帖子!首先,在config/routes.rb中更新评论资源,添加destroy操作的路由。:only选项应该是:only => [:create, :destroy]。接着,在 CommentsController 中添加destroy操作,类似于帖子中的destroy操作。最后,在app/views/comments/_comment.html.erb的底部添加指向该操作的链接: |
<%= link_to 'Destroy', [comment.post, comment],
method: :delete, data: { confirm: 'Are you sure?' } %>
第六章 部署
现在你已经构建了一个应用程序,我们来把它放到网上供大家查看。Rails 应用程序可以通过多种方式部署。Rails 可以运行在从简单的共享主机到专用服务器,再到云端虚拟服务器等各种环境中。
被称为 Heroku 的云应用平台是部署应用程序最简单的方法之一,我将在本章中讲解它。Heroku 使用 Git 版本控制系统来部署应用程序,所以我们需要先讨论版本控制系统。
版本控制
版本控制系统 (VCS) 会记录文件随时间变化的情况,因此你可以轻松地回到某个特定版本。版本库是一个数据结构,通常存储在服务器上,保存了 VCS 中文件的副本和这些文件的历史变化列表。使用 VCS 时,你可以在修改源代码时,知道自己始终可以回到最后一个有效版本。
最初,版本控制系统是集中式的。也就是说,源代码库存储在单一的服务器上。开发者可以连接到该服务器并检出文件以对代码进行修改。但集中式系统也存在单点故障的问题。集中式版本控制系统的例子包括并发版本系统 (CVS) 和 Subversion。
当今最流行的版本控制系统类型是分布式的。在分布式版本控制系统中,每个客户端都会存储源代码库的完整副本。这样,如果某个客户端出现故障,其他人仍然可以继续工作且不会丢失数据。
在分布式系统中,通常仍会使用中央服务器。开发者将他们的更改推送到该服务器,并拉取其他开发者所做的更改。流行的分布式版本控制系统包括 Git 和 Mercurial。由于 Heroku 使用 Git 部署应用程序,所以我将重点讲解 Git。
Git
Git 最初由 Linus Torvalds 于 2005 年为 Linux 内核开发。git 这个词是英国俚语,指的是一个可鄙的人。Torvalds 曾开玩笑说,他将所有的项目都以自己命名。
Git 很快便传播到 Linux 社区之外,现在大多数 Ruby 项目都使用 Git,包括 Ruby on Rails。如果你还没有安装 Git,可以在 Ruby, Rails, and Git 中找到安装说明。
设置
在开始使用 Git 之前,设置你的名字和电子邮件地址。打开一个终端窗口并输入以下命令来设置你的名字:
$ **git config --global user.name "** ***Your Name***"
--global 标志告诉 Git 将此更改应用于全局配置。没有此标志时,更改仅会应用于当前版本库。同时,设置你的电子邮件地址:
$ **git config --global user.email "** ***you@example.com*** **"**
现在每次提交更改时,你的名字和电子邮件地址都会被包含在内,这样在团队协作时就能轻松看到是谁在什么时候做了哪些更改。
入门
现在你已经准备好为博客创建一个版本库了。进入你的code/blog 目录并输入以下命令:
$ **git init**
Initialized empty Git repository in /Users/tony/code/blog/.git/
这会在隐藏的 .git 子目录中初始化一个空的 Git 仓库。接下来,让我们将应用程序的所有文件添加到仓库中:
$ **git add .**
add 命令接受文件名或目录路径,并将其添加到 Git 的暂存区。暂存区中的文件准备好提交到仓库。当你执行提交时,Git 会拍摄项目当前状态的快照,并将其存储在仓库中。命令中的点表示当前目录。因此,运行此命令后,当前目录及其任何子目录中的所有文件都准备好提交。
现在将所有已暂存的文件提交到仓库:
➊ $ **git commit -m "Initial commit"**
[master (root-commit) e393590] Initial commit
85 files changed, 1289 insertions(+)
create mode 100644 .gitignore
create mode 100644 Gemfile
--*snip*--
create mode 100644 test/test_helper.rb
create mode 100644 vendor/assets/javascripts/.keep
create mode 100644 vendor/assets/stylesheets/.keep
请注意,我通过 -m 标志指定了提交信息 "Initial commit" ➊。如果不加上此标志,Git 会打开你的默认编辑器,以便你输入提交信息。如果你没有输入提交信息,提交会失败。
如果你想查看当前仓库的提交历史,输入 git log 命令。列表按从最新到最旧的顺序显示之前的提交。每个条目都包括提交者和时间,以及提交信息。
$ **git log**
➊ commit e3935901a2562bf8c04c480b3c5681c102985a4e
Author: Your Name <you@example.com>
Date: Wed Apr 2 16:41:24 2014 -0500
Initial commit
每次提交都会由一个独特的 40 字符十六进制哈希值表示 ➊。这些哈希值可以缩写为前七个字符——在这个例子中是 e393590——如果你需要再次引用这个特定的提交。
基本用法
在使用 Git 开发项目时,遵循这个基本的工作流:
-
根据需要编辑本地文件。
-
使用
git add命令将文件暂存,以便提交。 -
使用
git commit命令将更改提交到仓库。
你可以根据需要频繁提交更改,但我发现将与单一简单功能或 bug 修复相关的更改一起提交会更有帮助。这样,所有更改都与一个提交绑定,若需要回滚或移除某个功能时会更容易。结束一个工作会话时,提交所有未完成的更改也是个好主意。
其他有用的命令
Git 包含许多额外的命令;输入 git --help 查看最常用的命令列表。你已经见过 init、add、commit 和 log 命令,但这里还有一些在使用 Git 时特别有用的命令。
git status 命令显示已更改和新增文件的列表:
$ **git status**
On branch master
nothing to commit, working directory clean
在这种情况下,没有任何更改。编辑项目中的文件,例如 README.rdoc,然后再次输入 git status 命令:
**$ git status**
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes...)
➊ modified: README.rdoc
no changes added to commit (use "git add" and/or "git commit -a")
git status 命令显示当前工作目录和暂存区的状态。在这里,它列出了所有已暂存以待提交的文件,以及那些未暂存但有更改的文件 ➊。
git diff 命令显示文件的详细更改:
$ **git diff**
diff --git a/README.rdoc b/README.rdoc
index dd4e97e..c7fabfa 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -1,4 +1,4 @@
➊ -== README
+== Blog
This README would normally document whatever steps are necessary to get the
application up and running.
在这里,我将文件第一行中的 README 改为 Blog ➊。使用此命令可以在执行 git add 之前查看将要提交的具体更改。如果只关心单个文件的更改,也可以将文件名传递给此命令。
git checkout 命令可以撤销文件的更改:
➊ $ **git checkout -- README.rdoc**
$ **git status**
On branch master
nothing to commit, working directory clean
在这里,我通过使用 git checkout 后跟两个破折号和文件名 ➊ 来丢弃对 README.rdoc 文件的更改。此命令没有产生任何输出。然后,我使用 git status 确认更改已被丢弃。
git clone 命令会创建一个远程仓库的本地副本:
$ **git clone** *url*
远程仓库由 <url> 表示。Git 是一个非常适合协作的工具,许多开源项目都在使用它。这个命令使得这一切成为可能。在你开始处理一个现有项目之前,你需要 克隆 该仓库的副本到你的电脑上。
分支
你可能已经注意到 git status 命令中包含了“On branch master”这一短语。在 Git 中,分支 是一组命名的更改。默认分支被称为 master,它代表了开发的主线。到目前为止,我所做的所有更改都已提交到主分支。
如果你正在处理一个可能需要较长时间完成的大功能,你可以创建一个单独的分支来存储正在进行的更改,而不会影响主分支。这样,你可以在自己的分支上工作,而不会影响团队的其他成员。一旦新功能完成,你将 合并 你的新分支到主分支。
使用 git branch 命令后跟你选择的分支名称来创建一个新分支。在这个示例中,我将我的分支命名为 testing:
$ **git branch testing**
输入 git branch 命令而不指定分支名称,可以查看当前仓库中已存在的所有分支:
$ **git branch**
* master
testing
星号显示的是当前选中的分支。我创建了一个新分支,但我仍然在查看主分支。要切换到另一个分支,使用 git checkout 命令:
$ **git checkout testing**
Switched to branch 'testing'
现在我在 testing 分支上。在这里提交的更改不会影响主分支。完成更改后,checkout 主分支并将你的更改合并到主分支中:
$ **git checkout master**
Switched to branch 'master' $
**git merge testing**
Already up-to-date.
来自 testing 分支的所有更改现在也出现在主分支上。你可以使用 git log 命令确认这一点。现在你已经完成了 testing 分支的工作,使用 -d 标志将 git branch 命令删除它:
$ **git branch -d testing**
Deleted branch testing (was e393590).
在分支合并后你不必删除分支,但删除它们可以保持分支列表的清晰。
远程
到目前为止,我们的所有更改都存储在本地,但你应该在另一服务器上存储一份仓库的备份副本,并且便于其他人克隆你的仓库。为此,你需要设置一个远程仓库。远程 只是另一个仓库在特定 URL 上的昵称。使用 git remote add 命令将昵称与 URL 关联起来:
git remote add *name url*
一旦你添加了远程仓库,使用 git push 命令将更改发送到远程仓库,使用 git pull 命令来获取远程的更改。在下一节中,你将看到这个操作的实际示例。
Heroku
Heroku 是一个云应用程序平台,用于部署 Web 应用程序。这种平台有时被称为平台即服务(PaaS),意味着 Heroku 负责服务器配置和管理,这样你就可以专注于应用程序开发。该服务还包括一系列丰富的附加组件。开始使用是免费的,但需要更多处理器资源和内存的大型应用程序可能会变得非常昂贵。
完成初步设置后,你可以使用git push命令部署你的应用程序,并在网页上访问它。
开始使用
首先,在【http://www.heroku.com】(http://www.heroku.com)注册一个免费账户。记住你选择的密码;你需要它来登录。
接下来,如果你还没有安装 Heroku Toolbelt,请安装它(有关说明,请参见【http://toolbelt.heroku.com/】(http://toolbelt.heroku.com/))。Toolbelt 是 Heroku 提供的一组工具,用于将你的应用程序部署到 Heroku 平台。
现在,打开终端窗口,导航到你的博客目录,并登录 Heroku:
$ **heroku login**
Enter your Heroku credentials.
Email: you@example.com
Password (typing will be hidden):
Authentication successful.
该命令会提示你输入你的电子邮件地址和之前创建的密码,然后它会检查你电脑上是否存在有效的安全外壳(SSH)公钥。你的公钥是用于通过 SSH 身份验证的公私钥对的一部分。当你尝试登录时,你的私钥会用于生成一个加密数字签名。Heroku 随后使用你的公钥验证该数字签名,从而确认你的身份。
如果你还没有公钥,在提示时按Y以创建一个。公钥创建后会自动上传到 Heroku。Heroku 使用你的公钥进行身份验证,这样你每次部署应用程序时就不需要输入密码。
现在你已经登录到 Heroku,你需要准备好你的应用程序进行部署。
更新你的 Gemfile
无论你正在构建什么类型的应用程序,你都需要安装某些 gem 来与 Heroku 交互并部署你的应用程序。在这一部分,我们将讨论你需要添加到应用程序的Gemfile中的两个 gem。
Heroku 的服务器使用 PostgreSQL 数据库服务器。我们在本地使用 SQLite 进行开发,而不是安装 PostgreSQL。你需要确保在生产环境中安装名为 pg 的 PostgreSQL gem。
Heroku 还需要 rails_12factor gem,它确保 Heroku 的服务器可以提供你的应用程序的资产,并确保你的应用程序的日志文件被发送到正确的位置。
打开位于 Rails 应用程序根目录下的文件Gemfile,找到gem 'sqlite3'这一行。你将在生产环境中使用 PostgreSQL gem,但你仍然需要 SQLite gem 用于开发和测试,因此更新这一行,按照如下所示添加group: [:development, :test]:
gem 'sqlite3'**, group: [:development, :test]**
这条指令告诉bundle命令仅在开发和测试环境中安装该 gem。
现在你需要安装前面提到的 pg 和 rails_12factor gem。你只需要在生产环境中使用这些 gem,因此在你刚刚更新的那一行下面添加以下几行:
**# gems required by Heroku**
**gem 'pg', group: :production**
**gem 'rails_12factor', group: :production**
在你做完这些更改后,保存并关闭 Gemfile。因为你更改了应用的 Gemfile,需要重新运行 bundle 命令来更新依赖。
$ **bin/bundle install --without production**
因为你是在本地运行此命令,进行应用的开发和测试,所以你不需要安装生产环境的 gem,因此需要添加 --without production 标志。Bundler 会记住传递给 bundle install 的标志,因此从现在开始每次运行该命令时,都会默认使用 --without production。
最后,你需要将这些更改添加并提交到你的 Git 仓库。输入以下命令来更新 Git:
$ **git add .**
$ **git commit -m "Update Gemfile for Heroku"**
[master 0338fc6] Update Gemfile for Heroku
2 files changed, 13 insertions(+), 1 deletion(-)
你可以在 Update Gemfile for Heroku 位置输入任何消息,但提交信息在描述你所做更改时会更有帮助。
现在,你的账户已经设置好了,应用也几乎准备好部署。最后一步是创建一个 Heroku 应用:
$ **heroku create**
➊ Creating glacial-journey-3029... done, stack is cedar
http://glacial-journey-3029.herokuapp.com/ | git@he...
➋ Git remote heroku added
这个命令 ➊ 在 Heroku 的服务器上创建一个具有随机生成名称的新应用。你本可以在 create 命令后指定一个名称,但该名称必须是唯一的。如果需要,你可以稍后更改名称。create 命令还 ➋ 为你自动设置了一个名为 heroku 的 Git 远程仓库。
部署你的应用
一切准备就绪,现在你可以最终部署你的应用了。使用 git push 命令将主分支的当前状态推送到 Heroku:
$ **git push heroku master**
Initializing repository, done.
Counting objects: 102, done.
Delta compression using up to 8 threads.
--*snip*
------> Launching... done, v6
http://glacial-journey-3029.herokuapp.com/ deployed to Heroku
To git@heroku.com:glacial-journey-3029.git
* [new branch] master -> master
Heroku 会识别这个 git push 命令,并自动检测到正在部署一个 Ruby on Rails 应用,安装 Gemfile 中指定的生产环境 gem,更新应用的数据库配置,预编译应用的资源,并启动应用。
当你第一次部署应用时,还需要运行数据库迁移,以便在 Heroku 的数据库服务器中创建应用所需的数据库表。使用 heroku run 命令在 Heroku 服务器上执行 rake db:migrate 命令:
$ **heroku run rake db:migrate**
Running `rake db:migrate` attached to terminal... up, run.1833
Migrating to CreatePosts (20140315004352)
--*snip*--
如果你对应用进行了更多的数据库更改,记得将更改提交到 Git 的主分支,推送主分支到 Heroku,并再次运行此命令。
现在你可以打开网页浏览器,访问 Heroku 为你创建的 URL,或者通过输入以下命令让 Heroku 为你处理:
$ **heroku open**
你的默认网页浏览器应该会自动打开并加载你的博客应用程序。
现在你的应用已经在 Heroku 上设置好了,你可以随时通过提交更改到 Git 仓库并将更改推送到 Heroku 来进行部署。
Github
在 Rails 书籍中讨论 Git 时,如果没有至少提到 GitHub,那将是不完整的。GitHub 是全球最大的源代码托管平台。GitHub 提供项目管理功能,如维基、问题追踪和通过拉取请求进行代码审查。
Rails 社区已将 GitHub 作为协作开源软件的最佳平台。Rails 本身托管在 GitHub 上,地址是 github.com/rails/rails/。如果你还没有 GitHub 账户,快去注册一个免费账户,加入这个社区吧!
总结
你的博客现在已安全地存储在 Git 分布式版本控制系统中。对源代码的更改正在被跟踪,并且可以轻松撤销。你的博客也通过 Heroku 对全世界可用。现在,你可以通过 git push 命令来部署新功能。
第一部分备注
本章标志着本书第一部分的结束。我们已经覆盖了 Ruby 和 Rails 的基础知识。模型代表你应用的数据;视图是你应用的用户界面;控制器是将它们连接在一起的粘合剂。你将使用这些概念来构建任何你想要的应用。
查看你在第一部分构建的应用,你会发现有很多可以改进的地方。例如,任何人都可以编辑甚至删除你博客上的帖子。而且,如果你写了成千上万的帖子,会发生什么呢?索引页面可能会在显示所有帖子之前就超时!虽然你现在可能还没有足够的工具来解决这些问题,但一旦你深入学习第二部分,情况就会有所不同。
在本书的下一部分,我们将构建一个新的社交网络应用,并讨论更高级的话题,如更复杂的数据建模、身份验证、测试、安全性、性能优化和调试。
学习了这些概念后,你将能够解决博客中的这些问题,并构建各种其他应用。
练习
| 问: | 1. 练习对你的应用进行修改,添加并提交这些更改到你的本地 Git 仓库,然后将更改推送到 Heroku。许多 Rails 开发者每天会进行多次部署,因此请熟悉这个过程。 |
|---|---|
| 问: | 2. 在 GitHub 上创建一个账户,学习如何在其服务器上创建一个新的仓库,并将你的应用推送到 GitHub。GitHub 提供了一个在线帮助区域,如果你遇到任何问题,可以参考这个帮助来完成操作。另外,使用 GitHub 的 Explore 功能来查看其服务器上流行项目的仓库。 |
| 问: | 3. 最后,看看你能否“解决你自己的痛点”。基于你的兴趣,创建一个简单的 Rails 应用。比如,创建一个你最喜欢书籍的目录,或者一个用来追踪你黑胶唱片收藏的应用。 |
第二部分:构建一个社交网络应用
第七章. 高级 Ruby
你在第一章学习了 Ruby 的基本概念。本章涵盖了一些语言的高级特性,包括模块、Ruby 对象模型、反射以及一些元编程。
模块在 Rails 应用程序中经常用于将类似的功能分组,并在类之间共享行为。Ruby 对象模型决定了方法如何在继承类的层次结构中查找和调用,以及如何从模块共享的代码中查找方法。反射通过允许你查看类的内部结构,从而支持多态性,帮助你了解类能理解哪些方法。元编程允许你的类在运行时通过定义方法来响应那些不存在的方法。
打开终端窗口并启动 IRB 开始实验。本章中的一些例子较长,输入时可能比平常更困难。你可以将代码输入到编辑器中,将其保存为扩展名为rb的文件,然后通过在终端输入ruby filename.rb来运行示例。或者,你也可以直接将代码从编辑器复制粘贴到 IRB 中。
模块
正如你在第一章中看到的,模块是一个方法和常量的集合,不能实例化。你定义模块的方式基本上和定义类相同。模块定义以module开头,后跟一个大写字母的名称,然后以end结束。
为了展示如何使用模块,首先我们需要定义一个类。让我们定义一个简单的Person类:
class Person
➊ attr_accessor :name
➋ def initialize(name)
@name = name
end
end
这个类使用attr_accessor ➊来定义实例变量@name的获取器和设置器,并在创建时设置@name的值 ➋。
类名通常是名词,因为它们代表对象。模块名通常是形容词,因为它们代表行为。许多 Ruby 模块在此约定的基础上更进一步,使用以able结尾的形容词命名,例如Comparable和Forwardable。
这里有一个简单的例子,展示如何使用模块:
module Distractable
def distract
puts "Ooh, kittens!"
end
end
在 IRB 中输入这个模块,将其包含到你在本章早些时候创建的Person类中,看看你是否能分散某人的注意力:
irb(main):001:0> **class Person**
irb(main):002:1> **include Distractable**
irb(main):003:1> **end**
=> Person
irb(main):004:0> **p = Person.new("Tony")**
=> #<Person:0x007fceb1163de8 @name="Tony">
irb(main):005:0> **p.distract**
Ooh, kittens!
=> nil
在第五章中,你也在使用 Rails 辅助方法时定义了一个模块方法。ApplicationHelper是一个模块,Rails 会自动将其混入所有控制器中。
模块在 Ruby 中有两个用途:
-
模块用于将相关方法分组并防止名称冲突。
-
模块定义了可以混入类中的方法,以提供额外的行为。
随着应用程序的成长,组织代码变得越来越重要。通过提供命名空间并使类之间的代码共享变得容易,模块帮助你将代码拆分成可管理的部分。让我们看看这两个用途。
模块作为命名空间
Ruby 模块可以用作命名空间,即包含常量或相关功能方法的代码容器。
Math模块是一个作为命名空间使用的内建 Ruby 模块。它定义了常量E和PI以及许多常见的三角函数和超越函数方法。双冒号运算符(::)用于访问 Ruby 中的常量。以下示例访问Math模块中的常量PI:
irb(main):006:0> **Math::PI**
=> 3.141592653589793
在模块中定义的方法通过点(.)运算符访问,就像类中的方法一样:
irb(main):007:0> **Math.sin(0)**
=> 0.0
模块作为混入
Ruby 模块也可以作为混入,为类提供额外的功能。Ruby 只支持单一继承,即一个类只能继承一个父类。模块使你能够实现类似于多重继承的功能:一个类可以包含多个模块,将每个模块的方法添加到自己的类中。
你可以通过三种方式将模块的方法添加到类中,使用include、prepend或extend。接下来,我将讨论这些关键字的效果。
include
include语句将模块的方法作为实例方法添加到类中,是将模块混入类中的最常见方式。
Comparable模块是 Ruby 中常用的混入模块。它在包含时将比较运算符和between?方法添加到类中。类只需要实现<=>运算符。该运算符比较两个对象,并根据接收者是否小于、等于或大于另一个对象,返回–1、0或1。
要将此模块作为混入模块使用,添加它到你之前创建的Person类中:
class Person
➊ include Comparable
➋ def <=>(other)
name <=> other.name
end
end
该类现在包含了Comparable模块➊,并定义了<=>运算符➋,用于将该对象的名称与另一个对象的名称进行比较。
在 IRB 中输入这个命令后,创建一些人物并检查它们是否能相互比较:
irb(main):008:0> **p1 = Person.new("Tony")**
=> #<Person:0x007f91b40140a8 @name="Tony">
irb(main):009:0> **p2 = Person.new("Matt")**
=> #<Person:0x007f91b285fea8 @name="Matt">
irb(main):010:0> **p3 = Person.new("Wyatt")**
=> #<Person:0x007f91b401fb88 @name="Wyatt">
irb(main):011:0> **p1 > p2**
=> true
在这里,p1大于p2,因为T在字母顺序上大于M。between?方法告诉你一个对象是否位于另两个对象之间:
irb(main):012:0> **p1.between? p2, p3**
=> true
在这个例子中,between?返回true,因为T在字母表顺序上位于M和W之间,这意味着它按预期工作。
prepend
prepend语句也将模块的方法添加到类中,但prepend将模块的方法插入到类的方法之前。这意味着,如果模块定义了与类同名的方法,模块的方法将优先执行而不是类的方法。通过使用prepend,你可以通过在模块中编写同名方法来覆盖类中的方法。
prepend的一个实际用法是记忆化。记忆化是一种优化技术,程序将计算结果存储起来,以避免多次重复相同的计算。
例如,假设你想在 Ruby 中实现斐波那契数列。斐波那契数列的前两个数字是零和一。每个后续数字是前两个数字的和。以下是在 Ruby 中计算斐波那契数列第n项的方法:
class Fibonacci
def calc(n)
return n if n < 2
➊ return calc(n - 1) + calc(n - 2)
end
end
注意 calc 方法是递归的。每次用大于 1 的 n 值调用 calc 时,都将导致对自身的两次调用 ➊。试着创建这个类的实例并计算一些小的 n 值:
irb(main):013:0> **f = Fibonacci.new**
=> #<Fibonacci:0x007fd8d3269518>
irb(main):014:0> **f.calc 10**
=> 55
irb(main):015:0> **f.calc 30**
=> 832040
当你对更大的 n 值调用该方法时,方法执行的时间会显著增加。对于约 40 的 n 值,方法需要几秒钟才能返回一个答案。
Fibonacci 的 calc 方法很慢,因为它重复进行相同的计算多次。但是,如果你定义一个模块来实现记忆化,计算应该会显著缩短时间。现在我们来做这个:
module Memoize
def calc(n)
➊ @@memo ||= {}
➋ @@memo[n] ||= super
end
end
Memoize 模块也定义了一个 calc 方法。这个方法有几个有趣的特点。首先,如果尚未初始化,它会初始化一个名为 @@memo ➊ 的类变量为空哈希表。这个哈希表存储每个 n 值对应的 calc 方法的结果。接着,如果该值尚未分配,它会将 super 的返回值赋给 @@memo 中键为 n 的位置 ➋。因为我们使用 prepend 将这个模块添加到 Fibonacci 中,super 会调用类定义的原始 calc 方法。
每次调用 calc 方法时,@@memo 会存储 n 值对应的 Fibonacci 数字。例如,在调用 calc(3) 后,@@memo 哈希表会包含如下键值对:
{
0 => 0,
1 => 1,
2 => 1,
3 => 2
}
在每一行中,键(第一个数字)是 n 的值,值(第二个数字)是对应的 Fibonacci 数字。Fibonacci 数字对于 0 是 0,对于 1 是 1,对于 2 是 1,对于 3 是 2。通过存储这些中间值,calc 方法就不需要重复计算相同的值。使用 prepend Memoize 将 Memoize 模块添加到 Fibonacci 类中,并试试看:
irb(main):016:0> **class Fibonacci**
irb(main):017:1> **prepend Memoize**
irb(main):018:1> **end**
=> Fibonacci
irb(main):019:0> **f.calc 40**
=> 102334155
现在 calc 的值已经被记忆化,你应该能够对更大的 n 值调用 calc 并几乎立刻得到答案。试试 n = 100 或甚至 n = 1000。注意,你不需要重启 IRB 或实例化一个新的 Fibonacci 对象。Ruby 中的方法查找是动态的。
extend
当你使用 include 或 prepend 将一个模块添加到类中时,模块的方法会作为实例方法被添加到类中。在第一章中,你学习了也有一些类方法,它们是直接在类上调用,而不是在类的实例上调用。extend 语句将模块的方法作为类方法添加到类中。使用 extend 可以将行为添加到类本身,而不是类的实例。
Ruby 标准库包含一个名为 Forwardable 的模块,你可以使用它来扩展一个类。Forwardable 模块包含了对委托很有用的方法。委托意味着依赖另一个对象来处理一组方法调用。委托是一种通过将某些方法调用的责任分配给另一个类来重用代码的方式。
例如,假设有一个名为 Library 的类,用来管理一本书的集合。我们将书籍存储在一个名为 @books 的数组中:
class Library
def initialize(books)
@books = books
end
end
我们可以存储我们的书籍,但目前还无法对它们做任何操作。我们可以使用 attr_accessor 来使 @books 数组在类外部可用,但那样会让数组的所有方法对类的使用者开放。这样,用户就可以调用诸如 clear 或 reject 等方法,将图书馆中的所有书籍移除。
让我们将一些方法委托给 @books 数组,以提供我们需要的功能——获取图书馆大小和添加书籍的方法。
1 require 'forwardable'
class Library
2 extend Forwardable
3 def_delegators :@books, :size, :push
def initialize(books)
@books = books
end
end
Forwardable 模块在 Ruby 标准库中,而不是 Ruby 核心库中,因此我们首先需要 require 它 ➊。接着,我们使用 extend 将 Forwardable 方法添加到我们的类中作为类方法 ➋。最后,我们可以调用 def_delegators 方法 ➌。这个方法的第一个参数是一个符号,表示我们要委托方法的实例变量。
在这种情况下,实例变量是 @books。其余的参数是表示我们要委托的方法的符号。size 方法返回数组中元素的数量。push 方法将一个新元素追加到数组的末尾。
在下面的例子中,lib.size 初始值为 2,因为我们图书馆中有两本书。添加一本书后,大小更新为 3。
irb(main):020:0> **lib = Library.new ["Neuromancer", "Snow Crash"]**
=> #<Library:0x007fe6c91854e0 @books=["Neuromancer", "Snow Crash"]>
irb(main):021:0> **lib.size**
=> 2
irb(main):022:0> **lib.push "The Hobbit"**
=> ["Neuromancer", "Snow Crash", "The Hobbit"]
irb(main):023:0> **lib.size**
=> 3
Ruby 对象模型
Ruby 对象模型 解释了 Ruby 在调用方法时如何查找该方法。在继承和模块的情况下,你可能会想知道某个方法到底是在哪里定义的,或者在有多个同名方法的情况下,哪一个方法是由特定调用实际调用的。
祖先
继续使用之前定义的简单 Person 类,我们可以在 IRB 中了解关于这个类的许多信息。首先,让我们看看哪些类和模块定义了 Person 类的方法:
irb(main):024:0> **Person.ancestors**
=> [Person, Distractable, Comparable, Object, Kernel, BasicObject]
类方法 ancestors 返回 Person 类继承的类和它包含的模块的列表。在这个例子中,Person、Object 和 BasicObject 是类,而 Distractable、Comparable 和 Kernel 是模块。你可以通过调用 class 方法来找出这些是类还是模块,具体内容在下面的 Class 部分会解释。
Object 是所有 Ruby 对象的默认根类。Object 继承自 BasicObject 并混入了 Kernel 模块。BasicObject 是 Ruby 中所有类的父类。你可以把它看作是一个空白类,所有其他类都建立在这个类之上。Kernel 定义了许多 Ruby 方法,这些方法在没有接收者的情况下调用,比如 puts 和 exit。每次你调用 puts 时,实际上是在调用 Kernel 模块中的实例方法 puts。
这个列表的顺序表示 Ruby 查找方法的顺序。Ruby 首先会在 Person 类中查找方法定义,然后继续在列表中查找,直到找到该方法。如果 Ruby 没有找到该方法,它会抛出一个 NoMethodError 异常。
方法
你可以通过分别调用methods和instance_methods来查看类定义的类方法和实例方法。这些列表默认包含所有父类定义的方法。传递参数false可以仅排除这些方法:
irb(main):025:0> **Person.methods**
=> [:allocate, :new, :superclass, :freeze, :===, :==, ... ]
irb(main):026:0> **Person.methods(false)**
=> []
irb(main):027:0> **Person.instance_methods(false)**
=> [:name, :name=, :<=>]
Person类包含了几乎 100 个从其祖先类继承的类方法,但它自己并没有定义任何类方法,因此调用methods(false)会返回一个空数组。调用instance_methods会返回由attr_accessor定义的name和name=方法,以及我们在类体内定义的<=>方法。
类
对象模型的最后一部分涉及到Person类本身。在 Ruby 中,一切都是对象,也就是说,它是某个类的实例。因此,Person类必须是某个类的实例。
irb(main):028:0> **Person.class**
=> Class
所有 Ruby 类都是Class类的实例。定义一个类,例如Person,实际上是创建了Class类的一个实例,并将其赋值给一个全局常量,这里是Person。Class类中最重要的方法是new,它负责为新对象分配内存并调用initialize方法。
Class有自己的祖先列表:
irb(main):029:0> **Class.ancestors**
=> [Class, Module, Object, Kernel, BasicObject]
Class继承自Module类,Module类继承自Object类。Module类包含了本节中使用的多个方法的定义,例如ancestors和instance_methods。
反射
反射,也叫做自省,是指程序运行时,能够检查对象的类型及其他属性。你已经看到过如何通过调用class来确定对象的类型,以及如何通过调用methods和instance_methods来获取对象定义的方法列表,但 Ruby 的Object类还定义了几个用于自省对象的方法。例如,给定一个对象,你可能想确定它是否属于某个特定的类:
irb(main):030:0> **p = Person.new("Tony")**
=> #<Person:0x007fc0ca1a6278 @name="Tony">
irb(main):031:0> **p.is_a? Person**
=> true
如果给定的类是接收对象的类,is_a?方法会返回true。在这个例子中,它返回true,因为对象p是Person类的实例。
irb(main):032:0> **p.is_a? Object**
=> true
如果给定的类或模块是接收对象的祖先类,is_a?方法也会返回true。在这个例子中,Object是Person的祖先,所以is_a?返回true。
如果你需要准确判断创建一个对象时使用了哪个类,可以使用instance_of?方法:
irb(main):033:0> **p.instance_of? Person**
=> true
irb(main):034:0> **p.instance_of? Object**
=> false
instance_of?方法只有在接收对象是给定类的实例时才返回true。对于祖先类和继承自给定类的类,这个方法返回false。这种自省方式在某些情况下很有用,但通常你不需要知道创建对象时使用的具体类——只需要知道对象的能力。
鸭子类型
在鸭子类型中,你只需要知道一个对象是否接受你需要调用的方法。如果对象能响应所需的方法,你就不必关心类名或继承关系。鸭子类型的名字来源于那句话:“如果它走起来像鸭子,叫起来像鸭子,那就把它叫做鸭子。”
在 Ruby 中,你可以使用respond_to?方法查看一个对象是否响应某个特定的方法。如果respond_to?返回false,那么调用该方法会抛出NoMethodError异常,如前所述。
例如,想象一个简单的方法,用来将带有时间戳的信息打印到文件中:
def write_with_time(file, info)
file.puts "#{Time.now} - #{info}"
end
你可以在 IRB 中尝试这个方法。
➊ irb(main):001:0> **f = File.open("temp.txt", "w")**
=> #<File:temp.txt>
➋ irb(main):002:0> **write_with_time(f, "Hello, World!")**
=> nil
➌ irb(main):003:0> **f.close**
=> nil
首先,打开当前目录下名为temp.txt的File并将File实例存储在变量f中 ➊。然后,将f和消息"Hello, World!"传递给write_with_time方法 ➋。最后,使用f.close关闭File ➌。
当前目录下的文件temp.txt现在包含类似于下面这一行的内容:
2014-05-21 16:52:07 -0500 - Hello, World!
这个方法运行得很好,直到有人不小心传递了一个不是文件的值,比如nil。这是可能的修复方案:
def write_with_time(file, info)
➊ if file.instance_of? File
file.puts "#{Time.now} - #{info}"
else
raise ArgumentError
end
end
这个修复通过检查file是否是File类的实例来解决问题 ➊,但它也限制了这个方法的适用性。现在它仅仅适用于文件。如果你想通过Socket写入网络,或者使用STDOUT写入控制台,怎么办呢?
与其测试file的类型,不如测试它的功能:
def write_with_time(file, info)
➊ if file.respond_to?(:puts)
file.puts "#{Time.now} - #{info}"
else
raise ArgumentError
end
end
你知道write_with_time方法调用了puts方法,所以检查file是否响应puts方法 ➊。现在,write_with_time可以与任何响应puts方法的数据类型一起使用。
使用鸭子类型编程可以使代码更加易于复用。在构建应用程序时,寻找更多应用鸭子类型编程的机会。
元编程
元编程是编写与代码而非数据打交道的代码的实践。在 Ruby 中,你可以编写代码,在运行时定义新的行为。本节中的技术可以节省时间并消除代码中的重复,允许 Ruby 在程序加载时或运行时生成方法。
本节介绍了两种动态定义方法的不同方式:define_method和class_eval。它还涉及了method_missing,使你能够响应那些未定义的方法。
define_method
假设我们有一个应用程序,其中包含可以为用户启用的功能列表。User类将这些功能存储在名为@features的哈希表中。如果某个用户可以访问某个功能,那么对应的哈希值将为true。
我们希望添加形式为can_ feature! 和 can_ feature?的方法,分别用于启用某个功能和检查某个功能是否启用。与其编写多个大致相同的方法,不如迭代可用功能的列表,并使用define_method来定义这些方法,如下所示:
class User
➊ FEATURES = ['create', 'update', 'delete']
FEATURES.each do |f|
➋ define_method "can_#{f}!" do
@features[f] = true
end
➌ define_method "can_#{f}?" do
➍ !!@features[f]
end
end
def initialize
@features = {}
end
end
User类首先创建了一个常量数组 ➊,命名为FEATURES,其中包含可用的功能。然后,它使用each遍历FEATURES,并调用define_method来创建形如can_feature! ➋的方法,允许用户访问某个功能。仍然在each块中,类还定义了形如can_feature? ➌的方法,用来判断用户是否具有访问该功能的权限。这个方法通过使用两个 NOT 运算符 ➍将@features[f]的值转换为true或false。
注意
使用两个 NOT 运算符并非绝对必要,因为@features哈希表对没有值的键返回nil,而 Ruby 将nil视为false,但这种技巧通常被使用。
现在,让我们创建一个新的User并尝试动态定义的方法:
irb(main):001:0> **user = User.new**
=> #<User:0x007fc01b95abe0 @features={}>
irb(main):002:0> **user.can_create!**
=> true
irb(main):003:0> **user.can_create?**
=> true
irb(main):004:0> **user.can_update?**
=> false
irb(main):005:0> **user.can_delete?**
=> false
如果你想更多地练习define_method,看看你能否添加形如cannot_feature!的方法,用于禁用用户的某个功能。更多细节可以在本章末的练习 3 中找到。
class_eval
class_eval方法将代码字符串作为类定义中的代码直接执行。使用class_eval是向类在运行时添加实例方法的一个简单方法。
当我在第一章中讨论attr_accessor时,你了解到它为类中的实例变量定义了 getter 和 setter 方法,但我并没有详细讨论这些方法是如何定义的。attr_accessor方法是 Ruby 内置的,你不需要自己定义它,但你可以通过实现自己的attr_accessor版本来了解class_eval。
➊ class Accessor
➋ def self.accessor(attr)
class_eval "
➌ def #{attr}
@#{attr}
end
➍ def #{attr}=(val)
@#{attr} = val
end
"
end
end
在这里,你定义了一个名为Accessor的类 ➊,并且它有一个名为accessor的类方法 ➋。这个方法的工作方式类似于内建的attr_accessor。它接受一个参数,表示你正在为其创建 getter 和 setter 方法的属性。将字符串传递给class_eval,它使用字符串插值将attr的值插入到需要的地方,从而定义两个方法。第一个方法的名称与属性相同,并返回属性的值 ➌。第二个方法的名称是属性名后跟一个等号。它将属性设置为指定的值val ➍。
例如,如果attr是:name,那么accessor通过将attr替换为name来定义name和name=这两个方法。这在没有示例的情况下有些难以理解。以下代码在一个类中使用了accessor方法:
➊ class Element < Accessor
➋ accessor :name
def initialize(name)
@name = name
end
end
首先,你让Element类继承自Accessor类 ➊,这样就可以使用accessor方法。然后,将实例变量的名称传递给accessor ➋。在这里,你传递了符号:name。当程序运行时,对class_eval的调用会自动在Element类中生成如下代码:
➊ def name
@name
end
➋ def name=(val)
@name = val
end
name 方法返回实例变量 @name 的当前值 ➊。name= 方法接受一个值并将其赋给 @name ➋。通过创建一个 Element 类的实例并尝试获取和设置 name 的值来测试它:
➊ irb(main):001:0> **e = Element.new "lead"**
=> #<Element:0x007fc01b840110 @name="lead">
➋ irb(main):002:0> **e.name = "gold"**
=> "gold"
➌ irb(main):003:0> **puts e.name**
gold
=> nil
首先,创建一个新的 Element 并将其名称初始化为 "lead" ➊。接下来,使用 name= 方法将新名称 "gold" 赋给它 ➋。最后,使用 name 方法显示 @name 的值 ➌。就这样,通过一点元编程的魔法,你将铅变成了金。
method_missing
每当 Ruby 找不到一个方法时,它会在接收者上调用 method_missing。该方法会接收原始方法名(作为符号)、一个参数数组以及传递给方法调用的任何块。
默认情况下,method_missing 会调用 super,这会将方法向上传递到祖先链,直到找到包含该方法的祖先类。如果方法到达 BasicObject 类,它会抛出一个 NoMethodError 异常。你可以通过在类中定义自己的实现来覆盖 method_missing,拦截这些方法调用并添加自己的行为。
让我们从一个简单的例子开始,这样你就能看到它是如何工作的。这个类会将任何未知的方法调用返回给你三次:
class Echo
def method_missing(name, *args, &block)
word = name
puts "#{word}, #{word}, #{word}"
end
end
现在,method_missing 被覆盖了,如果你尝试在该类的实例上调用一个不存在的方法,你会在终端中看到该方法的“回音”:
irb(main):001:0> **echo = Echo.new**
=> #<Echo:0x007fa8131c9590>
irb(main):002:0> **echo.hello**
=> hello, hello, hello
method_missing 的一个现实应用是 Rails 的动态查找器。通过使用动态查找器,你可以写出像 Post.find_by_title("First Post") 这样的 Active Record 查询,而不是 Post.where(title: "First Post").first。
动态查找器可以使用 method_missing 实现。让我们定义我们自己的动态查找器版本。我们将使用 query_by_attribute 而不是像 find_by_attribute 这样的方式,这样可以避免与内置方法发生冲突。
打开你博客目录中的 app/models/post.rb 文件,按照这个例子继续操作:
class Post < ActiveRecord::Base
validates :title, :presence => true
has_many :comments
➊ **def self.method_missing(name, *args, &block)**
➋ **if name =~ /\Aquery_by_(.+)\z/**
➌ **where($1 => args[0]).first**
**else**
➍ **super**
**end**
**end**
end
首先,定义 method_missing 类方法 ➊,因为我们的 query_by_attribute 方法将被调用到 Post 类上。接下来,测试名称是否符合正则表达式 ➋。
最后,使用正则表达式捕获的字符串和传递给方法的第一个参数来调用内置的 where 方法 ➌。如果字符串不匹配,一定要调用 super ➍;这确保了未知的方法会被发送到父类。
注意
正则表达式 /\Aquery_by_(.+)\z/ 匹配以 “query_by_” 开头的字符串,并使用括号捕获字符串的其余部分。正则表达式的全面讨论超出了本书的范围。网站 rubular.com/ 是一个在线编辑和测试正则表达式的好方法。
真实的动态查找器还会检查捕获的字符串是否与模型的属性匹配。如果你尝试用不存在的列调用我们的 query_by_attribute 方法,它会抛出一个 SQLException。
irb(main):001:0> **Post.query_by_title "First Post"**
=> #<Post id: 1, ...>
我们实现的query_by_attribute还有一个问题:
irb(main):002:0> **Post.respond_to? :query_by_title**
=> false
因为我们重写了method_missing来调用这个方法,Ruby 不知道Post类能够响应它。为了解决这个问题,我们还需要在Post模型的app/models/post.rb中重写respond_to_missing?方法。
class Post < ActiveRecord::Base
--*snip*--
**def self.respond_to_missing?(name, include_all=false)**
➊ **name.to_s.start_with?("query_by_") || super**
**end**
end
我们不再使用method_missing中的正则表达式,而是检查方法名是否以"query_by_"开头 ➊。如果是,这个方法会返回true。否则,调用super。现在重新启动 Rails 控制台并再次尝试:
irb(main):001:0> **Post.respond_to? :query_by_title**
=> true
在做出这个改变之后,respond_to?按预期返回true。记住,在使用method_missing时,始终要覆盖respond_to_missing?。否则,使用你类的用户就无法知道它接受哪些方法,之前提到的鸭子类型技巧也会失效。
总结
如果你写足够多的 Ruby 代码,你最终会在实际的程序中看到本章所介绍的所有技巧。到那时,你可以确信你能理解代码的作用,而不仅仅是认为元编程是一种神奇的东西。
在下一章中,你将从头开始构建一个新的 Rails 应用程序。在这个过程中,我将介绍一些高级数据建模技巧,你还将深入了解 Active Record。
现在,尝试这些练习吧。
练习
| 问题: | 1. Rails 框架广泛使用模块作为命名空间,并向类添加行为。在你的blog目录中打开 Rails 控制台,并查看Post的祖先类。它有多少个祖先?根据它们的名称,你能猜出它们的作用吗? |
|---|---|
| 问题: | 2. 更新define_method示例,添加一个cannot_ feature!方法。此方法应将@features哈希中对应正确键的值设置为false。 |
| 问题: | 3. 通过调用Element.instance_methods(false)验证class_eval是否创建了你预期的实例方法。然后重新打开Element类,并调用accessor :symbol,为名为@symbol的实例变量添加两个方法。 |
第八章 高级活动记录
在构建一个新应用程序时,首先要确定数据模型。数据模型是对程序中模型的描述,包括它们的属性和关联。首先,确定所需的模型及其关系,然后为这些模型创建表,并在 Rails 控制台中进行测试。一旦数据模型正确工作,构建其余的应用程序就会容易得多。
一些人听到数据模型这个词时,会想到带有框和箭头的图示。如果你理解模型之间的关系,图示是没有必要的。不过,本章确实包含了一些基本的图示,用于说明不同的关联。在每个图示中,箭头从子模型中的外键指向父模型中的主键。
在本章中,你将从零开始构建一个新的应用程序。这个应用程序是一个类似于 Tumblr 的社交网络。用户创建帐户,然后发布文本和图片供其他用户查看。用户可以关注其他用户,这样他们朋友的帖子就会出现在主页的时间线上。
首先,我将讨论几种高级数据建模技术。然后,我们将一起完成你新社交网络网站所需的模型构建。
高级数据建模
在构建博客时,你使用了has_many和belongs_to关联。现实世界的应用程序通常需要更复杂的关联。
例如,有时你需要建模两个相同类型模型之间的关联,或者你可能需要建模模型之间的多对多关系。你还可能需要将对象层次结构存储在数据库中,但关系型数据库并不真正支持继承。最后,你可能需要建模一个可以与多种不同类型模型关联的类。
本节将讨论这四种情况,首先介绍使用自连接关联建模两个相同类型模型之间的关系。
自连接关联
假设有一个用于管理员工的应用程序。除了每个员工的姓名、职位和薪资等数据外,还需要存储每个员工经理的姓名。每个员工属于一个经理,而一个经理拥有多个下属。经理也是员工,所以你需要在同类型的两个模型之间建立关联。
回想一下,belongs_to关联意味着模型需要一个外键来将其与另一个模型链接。外键是一个字段,用于标识关联另一方的模型。因此,employees表需要一个名为manager_id的字段,用于将每个员工与经理关联起来。图示图 8-1 展示了这一关系是如何运作的。
一个 自连接关联 允许你使用单个表来建模组织结构图或其他树形结构。manager_id 外键指向员工经理的 id。此类型的关联也用于建模其他树形结构,比如嵌套评论,其中回复包括一个 parent_id,指向父评论。

图 8-1. 自连接关联
一旦 manager_id 字段添加到 employees 表中,你可以在 Employee 模型中定义关联:
class Employee < ActiveRecord::Base
➊ has_many :subordinates, class_name: 'Employee',
➋ foreign_key: 'manager_id'
➌ belongs_to :manager, class_name: 'Employee'
end
首先,为下属添加一个 has_many 关联。因为这个关联指向的是 Employee 模型,而不是一个名为 Subordinate 的模型,所以你必须指定 class_name: 'Employee' ➊。你还必须指定外键名称,在这种情况下是 manager_id ➋。最后,为 manager 添加一个 belongs_to 关联。同样,你必须明确声明模型的类名,因为 Rails 无法仅根据关联名称推测出来 ➌。
有了这些关联,你可以调用 subordinates 方法来获取经理的下属列表。你还可以使用 manager 和 manager= 方法来获取和设置员工的经理。几乎每个员工都应该有一个 manager_id,如 表 8-1 中所示。如果你的 manager_id 是 nil,那么你一定是老板!
表 8-1. 员工表
| id | name | manager_id |
|---|---|---|
| 1 | Alice | NULL |
| 2 | Bob | 1 |
注意,Bob 的 manager_id 是 1。这意味着 Alice 是 Bob 的经理。Alice 的 manager_id 是 NULL,在 Ruby 中是 nil。她是这家只有两个人的公司的 CEO。
多对多关联
而一对多关联仅涉及两个表,多对多关联总是涉及一个第三个表,称为 连接表。连接表存储关联两端的外键。它 belongs_to 关联中的每个模型。
Rails 提供了两种不同的方式来设置多对多关联。
has_and_belongs_to_many
如果你使用连接表仅仅为了关联而不需要额外的数据,那么可以使用 has_and_belongs_to_many 关联。你仍然需要创建连接表,但不需要为其定义模型。连接表必须以它连接的两个模型的名称命名。
例如,作者写许多书籍,而某些书籍有多个作者。你需要的所有数据都存储在作者或书籍模型中,因此你可以在作者和书籍之间创建一个 has_and_belongs_to_many 关联,如 图 8-2 中所示。

图 8-2. has_and_belongs_to_many 关联
图 8-2 展示了Author和Book模型以及它们之间的连接表。按照以下方式定义这些模型之间的关联:
class Author < ActiveRecord::Base
has_and_belongs_to_many :books
end
一位作者可能写很多书,但一本书也可以有多个作者:
class Book < ActiveRecord::Base
has_and_belongs_to_many :authors
end
为了使此关联生效,authors和books之间的连接表必须命名为authors_books,并且必须包含author_id和book_id字段。使用rails generate命令创建一个空的迁移文件:
$ **bin/rails g migration CreateAuthorsBooks**
invoke active_record
create db/migrate/..._create_authors_books.rb
然后编辑迁移文件,移除主键并创建两个外键:
class CreateAuthorsBooks < ActiveRecord::Migration
def change
create_table :authors_books**, id: false** do |t|
➊ **t.references :author, null: false, index: true**
**t.references :book, null: false, index: true**
end
end
end
t.references :author 语句 ➊ 表示此字段是一个外键,引用了Author模型。该字段名为author_id。null: false选项添加了一个约束,禁止 NULL 值,index: true选项则为该字段创建了数据库索引,以加速查询。下一行创建了book_id字段,也有 NULL 约束和数据库索引。
你也可以在迁移中使用create_join_table方法来创建连接表。此方法接受关联的名称,并创建正确的表,其中没有主键,每个关联都有外键并带有 NULL 约束。此方法不会自动为外键创建索引,你可以按照以下方式添加索引:
class CreateAuthorsBooks < ActiveRecord::Migration
def change
**create_join_table :authors, :books do |t|**
**t.index :author_id**
**t.index :book_id**
**end**
end
end
在创建了连接表后,你无需做任何额外操作来使关联生效。连接表不需要关联模型。使用has_and_belongs_to_many关联时,Rails 会为你管理连接表。
has_many :through
如果你希望在连接表中存储除关联模型的外键之外的其他信息,可以使用has_many :through关联。例如,你可以使用名为performances的连接表来建模乐队与场馆之间的关联。图 8-3 展示了乐队、演出和场馆之间的关系。

图 8-3. has_many :through 关联
每个演出都属于一个乐队和一个场馆。它还具有演出时间。模型如下所示:
class Band < ActiveRecord::Base
has_many :performances
has_many :venues, through: :performances
end
一个乐队进行多场演出,因此乐队通过其演出与许多不同的场馆建立关联:
class Venue < ActiveRecord::Base
has_many :performances
has_many :bands, through: :performances
end
一个场馆承办多场演出。场馆通过它所承办的演出与多个不同的乐队建立关联:
class Performance < ActiveRecord::Base
belongs_to :band
belongs_to :venue
end
演出将乐队与场馆关联起来。场馆还可以在performances表中存储额外的数据,例如演出的时间:
单表继承
有时候你需要在数据库中存储类的层次结构。大多数关系型数据库不支持继承,但你可以使用单表继承来创建这些模型并在数据库中存储继承结构。
例如,假设你正在编写一个管理宠物商店的应用。你需要一种方式来建模不同类型的宠物,例如狗和鱼。宠物狗和宠物鱼有很多相同的属性和方法,所以它们都继承自一个名为 Pet 的父类是很有意义的。
在 Rails 中,你可以为宠物创建一个单独的表,然后将 Dog 和 Fish 这两个子类的记录存储在同一个表中。Rails 使用名为 type 的列来跟踪每行中存储的对象类型。除了父模型所需的列之外,你还需要将子模型所需的所有列添加到表中。你需要这么做,因为所有模型都存储在同一个表中。
父模型 Pet 是一个普通的 Active Record 模型。Pet 模型继承自 ActiveRecord::Base:
class Pet < ActiveRecord::Base
end
Dog 模型继承自 Pet:
class Dog < Pet
end
Fish 模型也继承自 Pet:
class Fish < Pet
end
在这些模型就位后,你可以将所有三种类型的记录存储在一个名为 pets 的单一表中,如表 8-2 所示。
表 8-2. 宠物表
| id | 类型 | 名称 | 费用 |
|---|---|---|---|
| 1 | 狗 | 柯利犬 | 200 |
| 2 | 鱼 | 金鱼 | 5 |
| 3 | 狗 | 可卡犬 | 100 |
这三行来自 pets 表,包含了 Dog 和 Fish 模型的数据。你现在可以像 Pet.count 一样调用来计算表中的宠物数量。调用 Dog.count 返回 2,Fish.count 返回 1。因为 Rails 知道每条记录的类型,pet = Pet.find(2) 会返回一个 Fish 类型的对象。
在下一节中,你将看到单表继承的另一个示例,当时你会为新应用创建帖子模型。
多态关联
使用多态关联,一个模型可以通过单一关联属于多个其他模型。多态关联的经典示例是允许对多种类型的对象进行评论。例如,你可能希望让人们对帖子和图片都能发表评论。以下是你使用多态关联时,评论模型可能的样子:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
不使用 belongs_to :post 或 belongs_to :image,而是指定评论 belongs_to 一个叫做 :commentable 的东西。这个名字可以是你喜欢的任何名字,但惯例是将其命名为模型名称的形容词形式。
comments 表需要两个字段来支持这种关联,一个名为 commentable_id 的整数字段和一个名为 commentable_type 的字符串字段。commentable_type 字段保存拥有此评论的对象的类名。这个设置类似于你在前一节中看到的单表继承的 type 列。commentable_id 是一个外键,指向拥有此评论的对象的 id。
在可以有评论的模型中,包含 as: :commentable 到 has_many :comments 关联中:
class Post < ActiveRecord::Base
has_many :comments, as: :commentable
end
class Image < ActiveRecord::Base
has_many :comments, as: :commentable
end
has_many 关联的工作方式与以前相同。像@post.comments这样的调用会返回与帖子相关的评论列表。它是通过查找与@post对象的id以及类名Post匹配的评论来实现的。
如果你的应用增长,并且你需要在其他模型上添加评论,你可以在新模型中添加相同的has_many关联,而不需要更改Comment模型。
现在理论部分已经够多了,让我们将一些知识付诸实践。
社交应用
在这一节中,你将为一个类似 Tumblr 的社交网络服务构建数据模型。你需要为用户和帖子创建模型。你还需要表示用户如何关注其他用户,以及几种不同类型的帖子,用户应该能够对帖子进行评论。
从在你的代码目录中创建一个新的空 Rails 应用开始:
$ **cd code**
$ **rails new social**
$ **cd social**
我把我的应用称为social,但你可以随意命名。谁知道呢,或许有一天你会发布这个应用并以十亿美元的价格出售!
现在让我们来构建这个应用所需的模型。
用户模型
如果这是一个社交网站,首先你需要一个用户模型以及用户之间关系的模型。像 Twitter 一样,Tumblr 不使用“朋友”这一概念,而是通过“关注”其他用户来订阅他们的更新。
从创建一个名为User的新资源开始。现在,添加 name 和 email 的字符串字段。你可以随时通过创建新的数据库迁移来添加更多字段。以下命令将为用户创建控制器、模型、数据库迁移以及其他文件:
$ **bin/rails generate resource User name email**
通过运行这个新的数据库迁移来创建users表:
$ **bin/rake db:migrate**
接下来,你需要创建一个模型来表示订阅的概念。订阅是一种自我连接类型,但它是多对多关联,因此你需要一个连接表。这个模型应该包含什么呢?你通过关注另一个用户的帖子来订阅他们。你可以把你关注的用户称为“领导者”。所以,你需要在subscriptions表中存储leader_id和follower_id。
当一个用户关注另一个用户时,关注者的id会存储在follower_id字段中,另一位用户的id会存储在leader_id字段中。这样的设置让你能够轻松找到一个用户的关注者和领导者列表。
$ **bin/rails g model Subscription leader:references follower:references**
invoke active_record
create db/migrate/..._create_subscriptions.rb
create app/models/subscription.rb
invoke test_unit
create test/models/subscription_test.rb
create test/fixtures/subscriptions.yml
因为这是一个连接表,使用模型生成器来创建订阅的数据库迁移和模型。别忘了更新你的数据库:
$ **bin/rake db:migrate**
既然你已经创建了表格,现在需要更新模型文件以定义关联。首先,在编辑器中打开文件app/models/subscription.rb:
class Subscription < ActiveRecord::Base
belongs_to :leader**, class_name: 'User'**
belongs_to :follower**, class_name: 'User'**
end
在创建模型时,你使用了leader:references和follower:references,因此 Rails 模型生成器为你在Subscription模型中添加了两个belongs_to关联。:leader和:follower实际上都指向一个User,所以你需要添加类名User。默认情况下,Rails 会查找与关联名称匹配的模型名称。如果你不指定类名,Rails 会查找名为Leader和Follower的模型。图 8-4 显示了users和subscriptions的表格。
注意
实际上,这些表格也包括created_at和updated_at时间戳,但为了简洁起见,我在本章的图表中省略了它们。

图 8-4. 订阅关联
在subscriptions表中,leader_id和follower_id都是外键,指向一个用户。现在Subscription关联已完成,我们来添加User关联。打开你的编辑器中的app/models/user.rb文件:
class User < ActiveRecord::Base
➊ **has_many :subscriptions, foreign_key: :follower_id,**
➋ **dependent: :destroy**
➌ **has_many :leaders, through: :subscriptions**
end
从用户拥有多个订阅的事实开始。在这种情况下,你需要指定使用的外键。通常,你会将其命名为user_id,但因为你在建模领导者和追随者,所以应该将其命名为follower_id ➊。同时,使用dependent: :destroy ➋来指定如果该用户被删除时会发生什么。这告诉 Rails,如果该用户被销毁,则销毁所有相关的订阅。最后,向领导者添加has_many:through关联➌。
接下来,为模型添加一些方法,使得操作关联更简单。你也可以使用这些方法在 Rails 控制台中测试关联:
class User < ActiveRecord::Base
has_many :subscriptions, foreign_key: :follower_id,
dependent: :destroy
has_many :leaders, through: :subscriptions
➊ **def following?(leader)**
**leaders.include? leader**
**end**
➋ **def follow!(leader)**
➌ **if leader != self && !following?(leader)**
**leaders << leader**
**end**
**end**
end
首先,添加一个谓词方法,返回true或false值,命名为following? ➊,用来判断当前用户是否正在关注另一个用户。此方法会检查当前用户的leaders集合中是否包含作为参数传入的leader。
然后,添加follow!方法➋,表示当前用户正在关注另一个用户。此方法确保当前用户不会试图关注自己,且不会已经关注了另一个用户➌。如果两种情况都不成立,传递给该方法的leader会使用<<插入到当前用户的leaders集合中,这是插入操作符。
有了这些方法后,你现在可以启动 Rails 控制台并测试你的关联:
$ **bin/rails console**
从创建两个用户开始:
irb(main):001:0> **alice = User.create name: "Alice"**
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "users" ...
(0.8ms) commit transaction
=> #<User id: 1, name: "Alice", ...>
irb(main):002:0> **bob = User.create name: "Bob"**
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO "users" ...
(0.8ms) commit transaction
=> #<User id: 2, name: "Bob", ...>
现在,在alice上调用follow!方法,并传入bob。然后在alice上调用following?方法,确认follow是否正常工作。最后,再次调用following?来查看bob是否正在关注alice:
irb(main):003:0> **alice.follow! bob**
User Exists (0.2ms) SELECT ...
(0.1ms) begin transaction
SQL (16.1ms) INSERT INTO ...
(20.4ms) commit transaction
User Load (0.3ms) SELECT ...
=> #<ActiveRecord::Associations::CollectionProxy ...>
irb(main):004:0> **alice.following? bob**
=> true
irb(main):005:0> **bob.following? alice**
User Exists (0.2ms) SELECT ...
=> false
调用 alice.follow! bob 会将 bob 添加到 alice 的 leaders 集合中。接下来,调用 alice.following? bob 会检查 alice.leaders 集合中是否包含 bob。结果是包含的,所以方法返回 true。当然,它实际上并没有查找 bob,而是查找指向 bob 的 User 的 id。调用 bob.following? alice 返回 false。因为 bob.leaders 集合是空的,所以 bob 并没有关注 alice。表 8-3 和 表 8-4 显示了 alice 关注 bob 后的 users 和 subscriptions 表(时间戳字段被省略)。
表 8-3. 用户表
| id | name | |
|---|---|---|
| 1 | Alice | NULL |
| 2 | Bob | NULL |
users 表保存了 alice 和 bob 的记录。
表 8-4. 订阅表
| id | leader_id | follower_id |
|---|---|---|
| 1 | 2 | 1 |
subscriptions 表保存了一个记录,表示 alice 和 bob 之间的关联。leader_id 是 2,即 bob 的 id;follower_id 是 1,即 alice 的 id。这意味着 alice 正在关注 bob。
此时,你可以通过调用 leaders 方法获取 alice 正在关注的每个用户的列表。拥有这个列表是有帮助的,但这只是你所需要的一半。你还希望能够列出一个用户的关注者。为此,使用 subscriptions 表,但这次是反向操作。
你需要在 Subscription 模型中添加另一个 has_many 关联,它是现有关联的反向关联。然后,你可以使用该关联来查找关注者。
class User < ActiveRecord::Base
has_many :subscriptions, foreign_key: :follower_id,
dependent: :destroy
has_many :leaders, through: :subscriptions
➊ **has_many :reverse_subscriptions, foreign_key: :leader_id,**
➋ **class_name: 'Subscription',**
**dependent: :destroy**
➌ **has_many :followers, through: :reverse_subscriptions**
def following?(leader)
leaders.include? leader
end
def follow!(leader)
if leader != self && !following?(leader)
leaders << leader
end
end
end
这个关联是现有 :subscriptions 关联的反向关联。没有什么巧妙的词语来描述订阅的反向,所以将关联命名为 :reverse_subscriptions。该关联使用 leader_id 字段作为外键 ➊。因为关联名称与模型名称不匹配,你还需要指定类名 ➋。和订阅关联一样,指定 dependent: :destroy,这样如果用户被删除,subscriptions 表中就不会留下孤立的记录。添加 :reverse_subscriptions 关联后,你可以使用它来为 :followers 添加另一个 has_many :through 关联 ➌。
重启 Rails 控制台以使这些更改生效,然后尝试新的关联:
➊ irb(main):001:0> **alice = User.find(1)**
User Load (0.3ms) SELECT ...
=> #<User id: 1, name: "Alice", ...>
irb(main):002:0> **bob = User.find(2)**
User Load (0.3ms) SELECT ...
=> #<User id: 2, name: "Bob", ...>
➋ irb(main):003:0> **alice.followers**
User Load (0.2ms) SELECT ...
=> #<ActiveRecord::Associations::CollectionProxy []>
➌ irb(main):004:0> **alice.followers.to_a**
=> []
irb(main):005:0> **bob.followers.to_a**
User Load (0.2ms) SELECT ...
=> [#<User id: 1, name: "Alice", ...>]
由于你重启了控制台,你首先需要在数据库中查找你的用户 ➊。调用 followers 方法查看 alice 是否有任何关注者 ➋。该方法返回一种称为 ActiveRecord::Associations::CollectionProxy 的关系类型。我通过在 followers 后面链式调用 to_a 来使输出更容易阅读,这会将输出转换为数组 ➌。
输出显示alice没有关注者,而bob有一个关注者——alice。User关联和方法到目前为止工作正常。现在,用户可以相互关注,我们可以继续讨论帖子功能。
帖子模型
人们在社交网络上不仅仅想分享纯文本——他们还想分享图片、链接和视频。我们应该允许用户为每种类型的内容创建不同的帖子类型,尽管这些帖子类型将共享一些共同的功能。这听起来像是继承的完美应用场景。
首先,创建一个名为Post的基础模型,然后从该类继承创建TextPost、ImagePost等模型。你可以使用单表继承来创建这些模型,并在数据库中存储继承结构。由于posts表存储所有类型的帖子记录,你必须向posts表中添加其他模型所需的列。除了常见的title和body字段外,还需要添加一个url字段来存储图像帖子的图像地址,以及一个type字段用于单表继承。
考虑到这些需求,生成帖子资源并更新应用的数据库:
$ **bin/rails g resource Post title body:text url type user:references**
$ **bin/rake db:migrate**
user:references选项会添加一个user_id字段,这样你就可以将帖子与用户关联起来。别忘了更新应用的数据库。
现在,你已准备好为不同类型的帖子创建资源。
$ **bin/rails g resource TextPost --parent=Post --migration=false**
$ **bin/rails g resource ImagePost --parent=Post --migration=false**
在这里,我为资源生成器传递了两个选项。--parent=Post选项表示这些模型继承自Post,而--migration=false选项告诉生成器不要为此资源创建数据库迁移。因为这些资源存储在之前创建的posts表中,所以不需要数据库迁移。
首先,让我们更新新创建的Post模型,位于app/models/post.rb,确保所有帖子都有一个关联的用户和类型:
class Post < ActiveRecord::Base
belongs_to :user
➊ **validates :user_id, presence: true**
➋ **validates :type, presence: true**
end
我们社交应用中的所有帖子都属于个别用户。这个验证确保了Post在没有关联user_id的情况下无法创建➊。类型验证➋确保所有记录要么被识别为TextPost,要么是ImagePost。
现在为TextPost和ImagePost模型添加验证。首先,编辑app/models/image_post.rb,并为ImagePost模型添加一个 URL 验证:
class ImagePost < Post
**validates :url, presence: true**
end
url字段保存ImagePost的图像地址。用户可以从如 Flickr 或 Imgur 这样的图片分享网站复制 URL。应用不应允许没有图像url的ImagePost被保存。
然后,在app/models/text_post.rb中更新TextPost模型,检查是否有帖子正文:
class TextPost < Post
**validates :body, presence: true**
end
应用还不应允许没有body文本的TextPost被保存。
在编辑模型时,还要在app/models/user.rb的其余has_many关联下,为新帖模型添加关联:
class User < ActiveRecord::Base
has_many :subscriptions, foreign_key: :follower_id,
dependent: :destroy
has_many :leaders, :through => :subscriptions
has_many :reverse_subscriptions, foreign_key: :leader_id,
class_name: 'Subscription',
dependent: :destroy
has_many :followers, through: :reverse_subscriptions
**has_many :posts, dependent: :destroy**
**has_many :text_posts, dependent: :destroy**
**has_many :image_posts, dependent: :destroy**
--*snip*--
现在,你可以重新启动 Rails 控制台并使用这些新模型:
➊ irb(main):001:0> **alice = User.find(1)**
User Load (42.0ms) SELECT ...
=> #<User id: 1, ...>
irb(main):002:0> **post1 = alice.text_posts.create(body: "First Post")**
(0.1ms) begin transaction
SQL (0.7ms) INSERT INTO ...
(1.9ms) commit transaction
=> #<TextPost id: 1, ...>
irb(main):003:0> **post2 = alice.image_posts.create(**
**url: "http://i.imgur.com/Y7syDEa.jpg")**
(0.1ms) begin transaction
SQL (0.7ms) INSERT INTO ...
(1.9ms) commit transaction
=> #<ImagePost id: 2, ...>
➋ irb(main):004:0> **alice.posts.to_a**
Post Load (32.3ms) SELECT ...
=> [#<TextPost id: 1, ...>, #<ImagePost id: 2, ...>]
➌ irb(main):005:0> **alice.text_posts.to_a**
TextPost Load (0.4ms) SELECT ...
=> [#<TextPost id: 1, ...>]
因为你重新启动了控制台,首先找到表示 alice 的 User ➊。然后创建一个属于 alice 的 TextPost 和一个 ImagePost。User 模型上的 posts 方法返回与该用户关联的所有帖子,无论类型如何 ➋。请注意,你刚刚创建的 TextPost 和 ImagePost 都会在同一个集合中返回。text_posts 方法只会返回 TextPost 对象 ➌。
评论模型
现在用户和帖子模型已经到位,接下来为应用程序创建评论模型。添加一个文本字段来保存评论的 body,一个 post_id 来引用拥有此评论的帖子,以及一个 user_id 来引用发表评论的用户。
请注意,我没有在这些评论中使用多态关联。因为我的不同帖子类型都继承自基类 Post,所以我可以简单地将 Comment 与 Post 关联,从而允许对任何类型的帖子进行评论。
$ **bin/rails g resource Comment body:text post:references user:references**
$ **bin/rake db:migrate**
还需要在 User 和 Post 模型中添加 has_many :comments 以完成用户、帖子和评论之间的关联。图 8-5 展示了你在本章中创建的表及其关联。

图 8-5. 社交应用数据模型,省略了时间戳
到此,你已经完成了所有模型的创建,并且已经朝着构建新的社交网络迈出了重要一步。
摘要
我在本章中讲解了一些相当高级的数据库建模技巧。User 模型有多个复杂的关联。不同类型的帖子展示了单表继承。幸运的是,Comment 模型没有包含任何意外的复杂性。
在下一章中,我将讲解认证,并且你将开始添加控制器操作和视图,以便用户可以注册并登录到你的社交网络。
练习
| Q: | 1. 在本章中,你在所有的 has_many 关联上都指定了 dependent: :destroy,以确保相关的依赖模型会被移除。例如,因为 Post 模型与 User 模型有 dependent: :destroy 关联,如果一个 User 被销毁,那么该用户的所有帖子也会被销毁。你认为如果在 belongs_to 关联上指定 dependent: :destroy 会发生什么情况? |
|---|---|
| Q: | 2. 为 Comment 模型添加验证,以确保每个评论都属于一个 User 和一个 Post。你的应用程序不应允许没有 user_id 和 post_id 的评论被创建。你还应该确保所有评论的 body 字段都有文本内容。 |
| Q: | 3. 使用 Rails 控制台创建一个新的 User。为该 User 创建一个 TextPost 或 ImagePost,并至少创建一个 Comment。然后销毁该 User,确保与之关联的 Post 和 Comment 也被销毁。 |
第九章 认证
身份是任何社交网络中的核心概念,而认证是向系统证明你身份的行为。你希望用户能够注册新账户并登录到你的应用程序。尽管像 devise 和 authlogic 这样的宝石为 Rails 应用程序提供了完整的认证系统,但在本章中,你将亲自动手构建自己的系统。
除了注册、登录和登出操作外,你还需要添加获取当前登录用户身份和将匿名用户重定向到登录页面的方法。这个认证系统将需要控制器和视图,因此在开始之前,先花点时间使用 Bootstrap 框架为你的站点添加一些样式。
Bootstrap
Bootstrap 是一个开源前端框架,最初由 Twitter 创建。它提供了一组 CSS 和 JavaScript 文件,你可以将其集成到网站中,以提供愉悦的排版、适应桌面和移动浏览器的响应式布局,以及模态对话框、下拉菜单、工具提示等功能。Bootstrap 的完整文档可以在线访问,地址是getbootstrap.com/。
你可以下载 Bootstrap 框架并手动将 CSS 和 JavaScript 文件集成到你的应用程序中,但 bootstrap-sass gem 可以为你完成所有这些工作。既然你已经在构建自己的认证系统,这里可以省点力——编辑你的应用程序的Gemfile并添加这个 gem。
**gem 'bootstrap-sass'**
然后运行bin/bundle install命令以更新已安装的 gem。现在 gem 安装完成,你需要对 CSS 和 JavaScript 文件做一些修改以包含 Bootstrap。首先,更新app/assets/stylesheets/application.css,如下所示:
--*snip*-
*= require_tree.
***= require bootstrap**
*= require_self
*/
这段代码将 Bootstrap CSS 文件包含到你的应用程序中。接下来,通过编辑 app/assets/javascripts/application.js 文件并将这行代码添加到文件末尾,来包含 Bootstrap 的 JavaScript 文件:
**//= require bootstrap**
最后,更新应用程序布局以使用 Bootstrap 样式。打开文件app/views/layouts/application.html.erb,并像下面这样更改页面主体的内容:
*--snip--*
**<body>**
**<div class="container">** ➊
**<%= yield %>** ➋
**</div>**
</body>
</html>
带有 class="container" 的 div 包裹着页面的内容 ➊,并提供根据屏幕宽度调整的边距,使得你的站点在桌面和移动屏幕上看起来都很清晰。yield 语句 ➋ 被 Rails 用来将视图模板的内容插入到布局中。
现在你已经有了样式表、JavaScript 和基本的布局,你可以开始使用 Bootstrap 了。
认证系统
认证系统的目的是识别当前用户,并仅显示用户想要查看或被授权查看的页面。你将使用电子邮件地址和密码的组合来识别用户。电子邮件地址是一个不错的选择,因为它们是全球唯一的。没有两个用户会拥有相同的电子邮件地址。
在你的应用程序中,匿名用户只能查看用于登录或注册新账户的页面。其他所有页面都应该受到限制。
帖子索引与展示
在开始构建认证系统之前,你需要保护的数据来自匿名用户。我们先为上章创建的Post模型添加index和show页面。首先,添加控制器操作。打开文件app/controllers/posts_controller.rb并添加以下index和show方法:
class PostsController < ApplicationController
➊ **def index**
**@posts = Post.all**
**end**
➋ **def show**
**@post = Post.find(params[:id])**
**end**
end
这两个操作类似于博客中的index和show操作,参见第四章。index操作 ➊ 从数据库中检索所有帖子并将其分配给@posts变量,然后渲染app/views/posts/index.html.erb视图。show操作 ➋ 使用params哈希中的id找到请求的帖子,将其分配给@post,并渲染app/views/posts/show.html.erb视图。
现在,你需要为这些操作创建相应的视图模板。创建一个名为app/views/posts/index.html.erb的新文件,并添加以下代码:
➊ <div class="page-header">
<h1>Home</h1>
</div>
➋ <%= render @posts %>
index视图使用 Bootstrap 的page-header类 ➊ 添加了一个标题,并使用部分渲染了@posts集合 ➋。
由于你使用部分来渲染帖子,接下来添加这些部分;你将为每种帖子类型添加一个部分——目前有两种类型——因此你需要两个部分文件。
首先,创建文件app/views/text_posts/_text_post.html.erb并打开进行编辑:
➊ <div class="panel panel-default">
➋ <div class="panel-heading">
<h3 class="panel-title">
➌ <%= text_post.title %>
</h3>
</div>
➍ <div class="panel-body">
<p><em>By <%= text_post.user.name %></em></p>
<%= text_post.body %>
</div>
</div>
这个部分使用了 Bootstrap 的面板组件来显示TextPost。panel类 ➊ 在内容周围添加了灰色边框。panel-heading类 ➋ 添加了浅灰色背景。然后,title被渲染在<h3>元素中,内容为<%= text_post.title %> ➌。panel-body类 ➍ 为了匹配标题添加了内边距。帖子作者和正文在此部分中渲染。
然后,创建文件app/views/image_posts/_image_post.html.erb,并添加以下内容。ImagePost部分与TextPost部分稍有不同:
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<%= image_post.title %>
</h3>
</div>
<div class="panel-body">
<p><em>By <%= image_post.user.name %></em></p>
➊ <%= image_tag image_post.url, class: "img-responsive" %>
<%= image_post.body %>
</div>
</div>
这个部分使用 ERB 的image_tag助手来添加一个图像标签,其源设置为image_post.url ➊,即图像的存储位置。该行还为图像添加了 Bootstrap 的img-responsive类,使其根据浏览器宽度自动缩放。
设置好这些视图后,启动 Rails 服务器并查看应用程序:
$ **bin/rails server**
现在在浏览器中访问http://localhost:3000/posts。Post index视图应该类似于图 9-1,具体取决于你在 Rails 控制台中创建了多少个帖子。

图 9-1. 帖子索引视图
你在上一章创建了两个帖子,当前应用程序的Post index视图显示了这两个帖子。由于在上一章没有添加标题,因此标题为空。
现在,Post部分已创建,Post show视图也可以使用这些部分。创建新文件app/views/posts/show.html.erb并添加以下内容:
<div class="page-header">
<h1>Post</h1>
</div>
➊ <%= render @post %>
<%= link_to "Home", posts_path,
➋ class: "btn btn-default" %>
show视图与index视图类似,有两个区别。它渲染单个帖子 ➊,而不是帖子集合,并且包含一个按钮 ➋,该按钮链接回帖子索引页面。
访问http://localhost:3000/posts/1查看实际效果,如图 9-2 所示。

图 9-2. 帖子显示视图
现在应用程序已经有了用于显示帖子(posts)的操作和视图,接下来让我们添加身份验证,保护这些操作不被匿名用户访问。
注册
在这里,你将实现一个用户注册流程,要求输入电子邮件地址、密码和密码确认。如果用户输入的电子邮件地址尚未存在于数据库中,并且密码匹配,系统将创建一个新的User并感谢用户注册。
你已经能够存储新用户的电子邮件地址,因为在users表中有一个名为email的字符串字段。然而,你需要更加小心地处理密码。绝对不要以明文形式存储用户密码。相反,应存储密码的哈希版本,这称为密码摘要。Rails 的安全密码功能提供了对密码哈希的内建支持,使用一种叫做 bcrypt 的哈希算法。Bcrypt 是一种安全的单向哈希算法。
你可以通过在 Rails 模型中调用has_secure_password方法来启用安全密码功能。该方法为模型添加了password和password_confirmation属性,并要求模型具有一个名为password_digest的字符串字段。它添加了验证,要求在创建时password和password_confirmation属性必须匹配。如果这两个属性匹配,系统会自动对密码进行哈希并将其存储在password_digest字段中。
首先,编辑应用程序的Gemfile并添加 bcrypt gem。因为许多应用程序包含身份验证系统,所以该 gem 的注释行已存在。去掉该行前面的注释符号并保存文件。
gem 'bcrypt', '~> 3.1.7'
每次更改Gemfile时,你还需要运行bin/bundle install命令,以更新系统上安装的 gem:
$ **bin/bundle install**
下一步是向users表添加password_digest字段,并运行数据库迁移命令bin/rake db:migrate,这样你就可以存储用户的哈希密码:
$ **bin/rails g migration AddPasswordDigistToUsers password_digest**
现在你需要为User模型启用安全密码功能。打开app/models/user.rb并在上章添加的has_many关联下方添加has_secure_password行。在编辑该文件时,还需要为电子邮件字段添加presence和uniqueness验证:
class User < ActiveRecord::Base
--*snip*--
**has_secure_password**
**validates :email, presence: true, uniqueness: true**
--*snip*--
end
创建新用户的默认路由是 http://localhost:3001/users/new。这个路由有效,但像 http://localhost:3001/signup 这样的自定义路由可能更容易记住。
编辑 config/routes.rb 并为注册页面添加路由。在用户注册账户或登录到应用程序后,你希望将用户重定向到主页。因此,在编辑此文件时,将 root 路由设置为 posts index 页面。
Rails.application.routes.draw do
resources :comments
resources :image_posts
resources :text_posts
resources :posts
resources :users
**get 'signup', to: 'users#new', as: 'signup'**
**root 'posts#index'**
end
打开 app/controllers/users_controller.rb 并为 UsersController 添加创建新 Users 的必要操作:
class UsersController < ApplicationController
➊ **def new**
**@user = User.new**
**end**
➋ **def create**
**@user = User.new(user_params)**
**if @user.save**
**redirect_to root_url,**
**notice: "Welcome to the site!"**
**else**
**render "new"**
**end**
**end**
**private**
**def user_params**
**params.require(:user).permit(:name, :email, :password,**
**:password_confirmation)**
**end**
**end**
new 方法 ➊ 实例化一个空的新的 User 对象并渲染注册表单。create 方法 ➋ 使用表单传递的参数实例化一个 User 对象。如果用户可以保存,它会将用户重定向到网站根目录并显示欢迎消息。否则,它会重新渲染新用户表单。
现在控制器操作已到位,接下来在 app/views/users/new.html.erb 中添加注册表单:
<div class="page-header">
<h1>Sign Up</h1>
</div>
<%= form_for(@user) do |f| %>
➊ <% if @user.errors.any? %>
<div class="alert alert-danger">
<strong>
<%= pluralize(@user.errors.count, "error") %>
prevented you from signing up:
</strong>
<ul>
<% @user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
➋ <div class="form-group">
<%= f.label :email %>
➌ <%= f.email_field :email, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password %>
<%= f.password_field :password, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation,
class: "form-control" %>
</div>
<%= f.submit class: "btn btn-primary" %>
<% end %>
该表单的前半部分显示错误消息 ➊(如果有)。表单使用具有 Bootstrap 类 form-group 的 div 来将标签和输入控件分组 ➋,并且为输入控件添加了 form-control 类 ➌。Bootstrap 使用这些类来为表单应用样式。
在浏览器中访问 http://localhost:3000/signup 以查看注册表单,如 图 9-3 所示。

图 9-3. 注册表单
在 create 动作中,你为新用户添加了一个闪现消息以表示欢迎,但你的视图中还没有显示闪现消息的地方。Bootstrap 包含一个非常适合显示闪现消息的 alert 类。打开 app/views/layouts/application.html.erb 的应用布局,并按照下面的示例添加一个闪现消息的区域:
--*snip*--
<body>
<div class="container">
➊ **<% if notice %>**
**<div class="alert alert-success"><%= notice %></div>**
**<% end %>**
➋ **<% if alert %>**
**<div class="alert alert-danger"><%= alert %></div>**
**<% end %>**
<%= yield %>
</div>
</body>
</html>
本应用使用了两种不同类型的闪现消息:notice 消息 ➊ 表示成功。notice 消息使用 Bootstrap 的 alert-success 类显示为绿色。alert 消息 ➋ 表示错误。alert 消息使用 Bootstrap 的 alert-danger 类显示为红色。
在上一章中,你没有为创建的用户添加电子邮件地址或密码。如果你想使用 alice 或 bob 登录,可以在 Rails 控制台中更新他们的帐户。
➊ irb(main):001:0> **alice = User.find(1)**
User Load ...
=> #<User id: 1, name: "Alice", ...>
➋ irb(main):002:0> **alice.email = "alice@example.com"**
=> "alice@example.com"
irb(main):003:0> **alice.password = "password"**
=> "password"
irb(main):004:0> **alice.password_confirmation = "password"**
=> "password"
➌ irb(main):005:0> **alice.save**
--*snip*--
=> true
在使用 bin/rails console 启动 Rails 控制台后,通过 id 查找 User ➊。然后为 email、password 和 password_confirmation 赋值 ➋。最后,使用 alice.save 保存 User ➌。为另一个 User 重复这些步骤。确保每个用户的 email 唯一。
现在你已经了解了如何为用户创建一个注册账户的表单,接下来我们来看看如何让他们登录。
登录
用户注册账户时,填写类似 图 9-3 中的表单,并在数据库中创建一个新的用户记录。另一方面,登录没有模型表示,登录不会在数据库中创建记录。相反,用户的身份存储在 session 中,这是用于标识从特定浏览器到 Web 服务器的请求的小量数据。
会话
通常,Web 服务器是 无状态的。也就是说,它们不会记住用户从一次请求到下一次请求的身份。您必须添加此功能,即通过将当前登录用户的 user_id 存储在会话中来实现。
Rails 默认将会话信息存储在一个 cookie 中。会话 cookie 已签名并加密,以防篡改。用户无法查看存储在其会话 cookie 中的数据。
Rails 中的会话值使用键值对存储,并像哈希一样访问:
session[:user_id] = @user.id
此命令将 @user.id 存储在当前用户计算机上的一个 cookie 中。该 cookie 会随每个请求自动发送到服务器。
当用户成功登录到您的网站时,您需要将 user_id 存储在 session 中。然后,您需要在每个请求中查找 session 中的 user_id。如果找到了 user_id 并且 User 记录与该 id 匹配,那么您就知道该用户已通过身份验证。否则,您应将用户重定向到登录页面。
实现
现在让我们实现登录过程。首先,使用 Rails 生成器创建一个 sessions 控制器:
$ **bin/rails g controller Sessions**
接下来,打开 config/routes.rb。添加一个新的资源 :sessions,并添加登录和登出的路由:
Rails.application.routes.draw do
resources :comments
resources :image_posts
resources :text_posts
resources :posts
resources :users
**resources :sessions**
get 'signup', to: 'users#new', as: 'signup'
**get 'login', to: 'sessions#new', as: 'login'**
**get 'logout', to: 'sessions#destroy', as: 'logout'**
root 'posts#index'
end
现在,创建一个名为 app/views/sessions/new.html.erb 的新文件,并添加登录表单:
<div class="page-header">
<h1>Log In</h1>
</div>
➊ <%= form_tag sessions_path do %>
<div class="form-group">
<%= label_tag :email %>
<%= email_field_tag :email, params[:email],
class: "form-control" %>
</div>
<div class="form-group">
<%= label_tag :password %>
<%= password_field_tag :password, nil,
class: "form-control" %>
</div>
<%= submit_tag "Log In", class: "btn btn-primary" %>
<% end %>
请注意,我在这里使用的是 form_tag ➊ 而不是 form_for。注册过程使用了 form_for,因为该表单与 User 模型关联。现在使用 form_tag,因为登录表单与任何模型无关。
会话控制器处理登录和登出。编辑 app/controllers/sessions_controller.rb 以添加以下操作:
class SessionsController < ApplicationController
➊ **def new**
**end**
➋ **def create**
**user = User.find_by(email: params[:email])**
**if user && user.authenticate(params[:password])**
**session[:user_id] = user.id**
**redirect_to root_url, notice: "Log in successful!"**
**else**
**flash.now.alert = "Invalid email or password"**
**render "new"**
**end**
**end**
➌ **def destroy**
**session[:user_id] = nil**
**redirect_to root_url, notice: "Log out successful!"**
**end**
end
new 方法 ➊ 渲染登录表单。控制器操作无需做任何事情。请记住,操作默认会呈现与其名称匹配的视图文件。在这种情况下,new 方法会渲染位于 /app/views/sessions/new.html.erb 的视图。create 方法 ➋ 通过电子邮件地址查找用户记录。如果找到了匹配的用户,并且该用户能够通过提供的密码进行身份验证,它会将 user_id 存储在会话中并重定向到主页。否则,它会将错误消息添加到 flash 中并重新显示登录表单。destroy 方法 ➌ 清除存储在会话中的 user_id 并重定向到主页。
访问 http://localhost:3000/login 查看在 图 9-4 中显示的登录表单。

图 9-4. 登录表单
用户现在可以登录和登出,但应用程序的其余部分无法知道当前用户的任何信息。当你向应用程序添加新功能时,当前用户的身份将被频繁使用。例如,应用程序使用当前用户来决定显示哪些帖子,并将所有新的帖子或评论分配给当前用户。接下来,让我们添加使身份验证系统可供应用程序其余部分使用的方法。
当前用户
首先,你需要能够识别当前登录的用户。在 app/controllers/application_controller.rb 中将 current_user 方法添加到 ApplicationController 中,并使其成为一个 helper 方法。这样,它将在所有控制器和视图中可用,为应用程序中的其他部分访问当前登录的用户奠定基础:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
**private**
**def current_user**
**if session[:user_id]**
**@current_user ||= User.find(session[:user_id])**
**end**
**end**
**helper_method :current_user**
end
current_user 方法返回一个 User 对象,表示当前登录的用户。当没有用户登录时,该方法返回 nil,因此你也可以在条件语句中使用它,当没有用户登录时会产生不同的结果。
例如,使用 current_user 方法在用户已登录时添加登出链接,或者在没有用户登录时显示登录和注册的链接。打开 app/views/layouts/application.html.erb 文件,并将此代码添加到 yield 语句上方:
*--snip--*
**<div class="pull-right">**
**<% if current_user %>**
**<%= link_to 'Log Out', logout_path %>**
**<% else %>**
**<%= link_to 'Log In', login_path %> or**
**<%= link_to 'Sign Up', signup_path %>**
**<% end %>**
**</div>**
<%= yield %>
</div>
</body>
</html>
现在,已登录用户应该看到一个登出链接,而匿名用户应该看到登录或注册的链接。
验证用户
在任何社交应用程序中,某些页面不应该对匿名用户开放。你需要的最后一件事是限制页面,使得只有经过身份验证的用户才能查看它们。你可以使用 Rails 的 before_action 方法来实现这一点。
before_action 是一个在控制器中其他任何动作之前自动执行的方法。这些方法有时用于通过加载多个不同操作所需的数据来消除重复。before_action 还可以通过渲染或重定向到另一个位置来终止当前请求。
创建一个名为 authenticate_user! 的方法,如果没有当前用户,则将其重定向到登录页面。将此方法添加到 app/controllers/application_controller.rb 中的 ApplicationController,这样它将在所有控制器中可用:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
private
def current_user
if session[:user_id]
@current_user ||= User.find(session[:user_id])
end
end
helper_method :current_user
**def authenticate_user!**
**redirect_to login_path unless current_user**
**end**
end
因为你将 posts index 页面设置为应用程序的主页,接下来我们在帖子控制器中尝试这个方法。打开 app/controllers/posts_controller.rb 文件并添加一个 before_action:
class PostsController < ApplicationController
**before_action :authenticate_user!**
--*snip*--
end
现在,如果匿名用户尝试访问首页,他或她应该会自动被重定向到登录页面。确保不要将此 before_action 添加到会话页面。如果这样做,匿名用户将无法访问登录页面!
使用当前用户
现在你的应用程序知道了谁已经登录,你可以将主页更改为仅显示当前用户创建的帖子或当前用户关注的任何人发布的帖子。此类主页通常按时间顺序排列,称为 时间线。
你需要做的第一件事是为User模型添加一个方法,返回一个user_id列表,用于查询帖子。我们称这个方法为timeline_user_ids。打开app/models/user.rb文件,并在文件末尾添加此方法:
--*snip*--
**def timeline_user_ids**
➊ **leader_ids + [id]**
**end**
end
在第八章中添加的has_many :leaders关联会自动添加一个名为leader_ids的方法,该方法返回一个包含该用户领导者(即用户关注的人的id值)id的数组。timeline_user_ids方法将当前用户的id添加到leader_ids返回的数组中,并返回新的数组➊,该数组应包含你想在时间轴上显示的每个用户。
现在打开app/controllers/posts_controller.rb并更新index动作以使用这个方法:
def index
**user_ids = current_user.timeline_user_ids**
**@posts = Post.where(user_id: user_ids)**
**.order("created_at DESC")**
end
与其直接使用Post.all获取所有帖子,index动作首先获取current_user.timeline_user_ids返回的user_ids列表。然后,它初始化@posts,包括所有应该出现在时间轴中的帖子,并根据这些 id 筛选。还需要添加order子句,因为时间轴是按逆时间顺序显示的。
登录后查看图 9-5 页面。

图 9-5. 登录后的 Post 索引视图
点击注销链接,并确认你被重定向到登录页面。
总结
你的应用程序现在真的是开始成型了。得益于 Bootstrap,你已经有了一些相当不错的样式。用户现在可以注册、登录和注销。你还可以根据用户是否通过身份验证来限制对页面的访问。
你写了很多代码,但到目前为止你只是通过在浏览器中点击来测试它。当你只有少数几个动作需要测试时,这还不算太糟。但随着应用程序中的动作增多,这种测试方式会变得繁琐。
在下一章中,你将学习如何进行模型和控制器的自动化测试。我们将研究 Rails 自带的默认测试框架,编写针对应用程序各部分的测试,并了解一些关于测试驱动开发的内容。
练习
| 问题: | 1. 你添加了一个帖子show动作和视图,但目前无法通过输入 URL 访问单个帖子的页面。使用 Rails 的time_ago_in_words助手,在TextPost和ImagePost部分中,根据created_at字段创建一个指向帖子的链接。 |
|---|
| 问题: | 2. 为帖子添加评论。这个过程与第五章结束时为博客添加评论类似。首先,更新app/views/posts/show.html.erb中的帖子show页面,在底部渲染评论集合并显示添加新评论的表单,如下所示:
*--snip--*
**<h3>Comments<h3>**
**<%= render @post.comments %>**
**<h4>New Comment</h4>**
**<%= form_for @post.comments.build do |f| %>**
**<div class="form-group">**
**<%= f.label :body %><br>**
**<%= f.text_area :body, class: "form-control" %>**
**</div>**
➊ **<%= f.hidden_field :post_id %>**
**<%= f.submit class: "btn btn-primary" %>**
**<% end %>**
表单包括当前帖子post_id的隐藏字段 ➊。接下来,在app/controllers/comments_controller.rb中添加create动作:|
**def create**
**@comment = current_user.comments.build(comment_params)**
**if @comment.save**
**redirect_to post_path(@comment.post_id),**
**notice: 'Comment was successfully created.'**
**else**
**redirect_to post_path(@comment.post_id),**
**alert: 'Error creating comment.'**
**end**
**end**
还要在CommentsController中添加私有的comment_params方法。除了评论body,还要允许在params中传递的post_id:|
**def comment_params**
**params.require(:comment).permit(:body, :post_id)**
**end**
确保只有经过身份验证的用户才能访问这个控制器。最后,创建评论部分 app/views/comments/_comment.html.erb。这个部分需要显示添加评论的用户的名字以及评论的内容。|
| 问: | 3. 认证系统的安全性如何?查看password_digest字段中一个User的内容。还要检查你登录应用后放置在电脑上的 cookie。你能猜出这两者中包含的数据吗? |
|---|
第十章:测试
到目前为止,你一直在 Rails 控制台中输入代码并点击浏览器中的链接来测试代码。然而,随着你为应用程序添加更多功能,这种方式将无法扩展。即便使用了更有效的测试方法,你仍然需要记得在每次添加新功能后重新测试应用程序中的所有内容。否则,你可能会错过回归错误。
与其手动测试你的应用程序,不如在 Ruby 中编写自动化测试,确保你的代码是正确的并满足所有需求。一旦你设置好一套自动化测试,你就可以运行整个测试套件来捕捉回归,从而帮助你更有信心地重构代码。
Ruby 提供了几种不同的测试框架。本章重点介绍 Rails 默认使用的测试框架:MiniTest。
在 Rails 中进行测试
基本的测试框架会在生成 Rails 模型和控制器时自动创建在test目录中。这些只是起点:它们并不真正测试任何内容,但有了框架,添加你自己的测试将变得更加容易。
在本章中,我将讨论如何测试模型和控制器。你将学习如何测试单独的组件以及组件之间的交互。但首先,让我们为测试准备好环境。
准备测试
到目前为止,你一直在 Rails development 环境中构建应用程序。Rails test 环境已经预先配置好用于测试,但在运行测试之前,你仍然需要做一些准备工作。
test 环境使用一个独立的数据库专门用于运行测试。首先,确保通过运行数据库迁移来更新应用程序的db/schema.rb:
$ **bin/rake db:migrate**
测试数据库会在每次测试运行之前自动重新创建,以确保测试不依赖于数据库中已经存在的数据。
运行测试
现在测试数据库已经设置好,你可以开始运行你的测试了。Rails 提供了几种不同的 rake 任务,用于运行你将创建的各种类型的测试。
默认情况下,bin/rake test 命令会运行所有测试。如果你在命令行中包含某个测试文件的名称,它只会运行该文件中的测试。在处理特定模型或控制器时,运行与该类相关的测试会更快。
这个命令会运行文件test/models/user_test.rb中的所有测试:
$ **bin/rake test test/models/user_test.rb**
稍等片刻,你应该会看到类似下面的输出:
Run options: --seed 46676
# Running:
Finished in 0.001716s, 0.0000 runs/s, 0.0000 assertions/s.
0 runs, 0 assertions, 0 failures, 0 errors, 0 skips
如最后一行所示,还没有定义任何测试。打开test/models/user_test.rb,让我们添加一些测试。
➊ require 'test_helper'
➋ class UserTest < ActiveSupport::TestCase
➌ # test "the truth" do
# assert true
# end
end
这个测试文件首先引入了文件test/test_helper.rb ➊,该文件包含了所有测试的配置。测试助手还会加载所有fixtures(样本数据),并且可以包含测试的辅助方法。接下来,测试文件通过继承自ActiveSupport::TestCase ➋ 定义了一个名为UserTest的测试用例。测试用例是一组与某个类相关的测试。在测试用例内部,注释中提供了一个简单的示例测试 ➌。
注释掉的测试即使取消注释也并未真正进行测试,因此可以将其移除。但这些行展示了所有测试的基本结构,所以在继续之前让我们先检查一下:
➊ test "the truth" do
➋ assert true
end
test 方法 ➊ 接受一个测试名称和一个要执行的代码块。这个代码块包含一个或多个断言 ➋。断言 用于测试代码行的预期结果。这里显示的 assert 方法期望其参数评估为 true。如果断言为真,测试通过并输出一个点。如果断言为假,测试失败,并输出一个 F 以及标识失败测试的消息。
现在让我们按照这个基本的测试结构向文件中添加一个真实的测试。我觉得同时打开正在测试的模型文件(在本例中是 app/models/user.rb)和测试文件很有帮助。我通常会为模型中添加的任何自定义方法编写测试,并验证模型的验证是否按预期工作。查看 user 模型时,你会看到几个 has_many 关联,接着是 Rails 的 has_secure_password 方法、一个验证以及你编写的方法。
首先,确保你能够创建一个有效的用户。记住,has_secure_password 方法会为名为 password 和 password_confirmation 的属性添加验证。用户还需要有一个唯一的电子邮件地址,因此要创建有效的用户,你必须提供 email、password 和 password_confirmation。
test "saves with valid attributes" do
➊ user = User.new(
email: "user@example.com",
password: "password",
password_confirmation: "password"
)
➋ assert user.save
end
在这里,你用有效的属性实例化一个新的 User 对象 ➊ 并断言它能够成功保存 ➋。
再次运行此文件中的测试:
$ **bin/rake test test/models/user_test.rb**
Run options: --seed 40521
# Running:
➊.
Finished in 0.067091s, 14.9051 tests/s, 14.9051 assertions/s.
➋ 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
单个点 ➊ 代表单个测试。最后一行输出 ➋ 告诉你你运行了一个测试,包含一个断言且没有失败。
你可以继续添加更多的测试,但手动为所有测试创建用户会变得很繁琐。幸运的是,Rails 提供了之前提到的 fixtures,它可以自动创建需要的多个带有示例数据的模型对象。
使用 Fixtures
Fixtures 为测试提供示例数据,它们是用 YAML 格式编写的。YAML 最初代表 Yet Another Markup Language,但现在是 YAML Ain’t Markup Language 的递归首字母缩略词。Fixtures 会被 tests/test_helper.rb 文件自动加载到 test 数据库中,并对所有测试用例可用。
用户 Fixtures
打开文件 test/fixtures/users.yml,清空其内容,并创建两个示例用户:
➊ user1:
➋ email: user1@example.com
➌ password_digest: <%= BCrypt::Password.create "password" %>
user2:
email: user2@example.com
password_digest: <%= BCrypt::Password.create "password" %>
这段代码为两个用户添加了示例数据。YAML 文件以第一个 fixture 的名称开始,后面跟着冒号 ➊。在本例中,fixture 名为 user1。在名称下缩进的行指定了属性。第一个用户的电子邮件地址是 user1@example.com ➋。
你甚至可以使用 ERB 来帮助向数据中添加内容。与其预先计算password_digest字段的值,不如使用BCrypt::Password.create方法动态生成password_digest ➌。这个方法是你在第九章中安装的 bcrypt gem 的一部分。
在测试中,使用users方法并传递你想要的用户的名字来引用这些用户。例如,users(:user1)返回之前定义的第一个用户。
返回到test/models/user_test.rb中的用户测试,试试新的数据:
test "validates email presence" do
➊ @user1 = users(:user1)
➋ @user1.email = nila
➌ assert_not @user1.valid?
end
这个测试使用数据初始化一个用户 ➊,将该用户的email设置为nil ➋,并使用assert_not方法确保用户无效 ➌。只有当assert_not方法的条件为假值时,才会通过。
这个测试证明了电子邮件是必需的;现在你将添加一个测试,确保电子邮件的唯一性。
test "validates email uniqueness" do
➊ @user1 = users(:user1)
@user2 = users(:user2)
➋ @user1.email = @user2.email
➌ asseart_not @user1.valid?
end
这个测试使用数据初始化两个用户 ➊,将第一个用户的email设置为第二个用户的email ➋,并验证 ➌ 第一个用户不再有效。第二个用户仍然有效,因为第一个用户由于无效数据不能被保存。你可以查看log/test.log中的测试日志,查看每个测试运行的查询。
数据有基于数据名称的id值,这些值始终是相同的。例如,@user1的id是 206669143\。这个值永远不会改变。数据之间的关联是通过名称创建的,因为每个数据的id是基于其名称的。接下来讨论的Post数据包括与之前创建的User数据的关联。
后置数据
Rails 自动为TextPost和ImagePost类型创建了数据文件。你将把这两种类型的数据包括在Post文件中。其他类型的数据文件会导致错误,因此在继续之前,请删除文件test/fixtures/text_posts.yml和test/fixtures/image_posts.yml。
现在打开文件test/fixtures/posts.yml并创建一些示例帖子:
post1:
title: Title One
body: Body One
type: TextPost
user: user1
post2:
title: Title Two
url: http://i.imgur.com/Y7syDEa.jpg
type: ImagePost
user: user1
post3:
title: Title Three
body: Body Three
type: TextPost
user: user2
这里你有三个帖子。前两个属于名为user1的User,第三个属于user2。稍后你会在为Post模型添加测试时用到这些帖子。
将断言应用于实践
断言是测试的构建块。你已经在之前写的测试中看到了几个断言,如assert和assert_not。MiniTest 库中包含了更多断言,Rails 还添加了一些自己的断言。这里是一些最常用的断言:
assert test
如果test表达式的值为真,则测试通过
assert_empty obj
如果obj.empty?为真,则测试通过
assert_equal expected, actual
如果expected值等于actual值,则测试通过
assert_includes collection, obj
如果collection.includes?(obj)返回真,则测试通过
assert_instance_of class, obj
如果obj.instance_of?(class)为真,则测试通过
assert_match regexp, string
如果给定的 string 与正则表达式 regexp 匹配,则测试通过
assert_nil obj
如果 obj.nil? 为真,测试通过
每个断言也有“不”的形式。例如,assert_not 如果被测试的表达式为假则通过,assert_not_equal 如果预期值与实际值不相等则通过。断言还接受一个可选的消息参数,这是一个在断言失败时打印的字符串。
让我们将对断言的知识付诸实践,并为 user 模型添加更多测试。以下是第一个测试:
test "should follow leader" do
➊ @user1 = users(:user1)
@user2 = users(:user2)
➋ @user1.follow!(@user2)
➌ assert_equal 1, @user1.leaders.count
assert_equal 1, @user2.followers.count
end
该测试使用 fixtures 创建两个用户 ➊,然后调用 @user1 的 follow! 方法,并将 @user2 作为参数 ➋。接着确保 @user1 有一个领导者,而 @user2 有一个追随者 ➌。
接下来的测试验证了 following? 方法是否正常工作:
test "following? should be true" do
@user1 = users(:user1)
@user2 = users(:user2)
@user1.follow!(@user2)
assert @user1.following?(@user2)
end
它再次使用 fixtures 创建两个用户,然后调用 @user1 的 follow! 方法,并将 @user2 作为参数,最后确保 @user1.following?(@user2) 返回 true。
使用回调消除重复
你所编写的测试应该都能正确工作,但我引入了一些代码重复。几乎每个测试都使用 fixtures 来创建用户。记住,不要重复自己。幸运的是,测试用例包括两个回调,可以帮助消除这些重复。回调 是在每个测试之前和之后自动调用的方法。
setup 方法在每个测试之前调用,而 teardown 方法在每个测试之后调用。这些方法通常用于初始化在多个测试中使用的对象。你可以使用 setup 方法自动初始化 @user1 和 @user2 的值。
class UserTest < ActiveSupport::TestCase
➊ def setup
@user1 = users(:user1)
@user2 = users(:user2)
end
--*snip*--
➋ test "following? should be true" do
@user1.follow!(@user2)
assert @user1.following?(@user2)
end
end
现在,@user1 和 @user2 在 setup 方法中初始化 ➊,你可以从每个测试中去除重复部分,如重写后的 following? 测试 ➋ 所示。
模型测试
到目前为止,你看到的测试都是模型测试。模型测试 验证了应用程序模型的行为。这类测试以前被称为 单元测试。我通常会为验证和我编写的任何自定义方法添加测试。
我已经为 User 模型覆盖了这两个回调,那么现在让我们为 Post 模型添加测试。在编写测试时,你可能还想参考 app/models/post.rb 中的 Post 模型。
class Post < ActiveRecord::Base
belongs_to :user
has_many :comments, dependent: :destroy
validates :user_id, presence: true
validates :type, presence: true
end
Post 模型仍然非常简单。一个帖子属于一个用户,并且可以有多个评论。它还验证了 user_id 和 type 的存在。让我们添加一个测试来验证 Post 是否有 user_id。在编辑器中打开 test/models/post_test.rb 文件:
require 'test_helper'
class PostTest < ActiveSupport::TestCase
➊ def setup
@post1 = posts(:post1)
@post2 = posts(:post2)
end
➋ test "validates user_id presence" do
@post1.user_id = nil
assert_not @post1.valid?
end
end
setup 方法 ➊ 初始化了两个你可以在测试中引用的帖子。第一个测试 ➋ 验证没有 user_id 的 Post 是无效的。
由于现在你已经为用户和帖子编写了模型测试,你可以使用 bin/rake test:models 命令来运行所有模型测试:
$ **bin/rake test:models**
Run options: --seed 47072
# Running:
......
Finished in 0.234202s, 25.6189 runs/s, 29.8887 assertions/s.
6 runs, 7 assertions, 0 failures, 0 errors, 0 skips
如果此命令导致错误,请删除之前提到的TextPost和ImagePost模型中未使用的 fixture 文件。删除test/fixtures/text_posts.yml和test/fixtures/image_posts.yml。
其他帖子类型有自己的验证。例如,TextPost验证body的存在,ImagePost验证url的存在。既然我们已经有了TextPost和ImagePost的 fixture,那么让我们为这两个验证添加测试:
test "TextPost requires body" do
➊ assert_instance_of TextPost, @post1
➋ @post1.body = nil
➌ assert_not @post1.valid?
end
test "ImagePost requires url" do
assert_instance_of ImagePost, @post2
@post2.url = nil
assert_not @post2.valid?
end
这两个测试遵循相同的模式。首先,验证@post1是否为TextPost的实例 ➊。接着,将@post1的body设置为nil ➋。最后,验证@post1不再有效 ➌。ImagePost的断言做了相同的事情,但针对@post2。
控制器测试
控制器测试通过模拟请求到你的应用程序并验证响应,来验证单个控制器的操作。控制器测试确保控制器操作能够成功响应有效请求,并正确渲染视图或重定向到正确的位置。这类测试之前被称为功能测试。
控制器测试辅助方法
Rails 包含了几个辅助方法和变量,使控制器测试更易编写。
方法get、post、put、patch、head和delete模拟对控制器操作的请求。这些方法可以接受两个可选的哈希:一个表示请求参数,另一个表示当前会话。
在使用上述六个方法之一发出请求后,以下四个哈希变得可用:
assigns |
包含在控制器操作中分配的实例变量 |
|---|---|
cookies |
包含操作中设置的任何 cookie 值 |
flash |
包含操作中设置的闪存值 |
session |
包含操作中设置的任何会话值 |
你的测试还可以访问三个实例变量:@controller包含正在处理请求的控制器;@request是正在处理的请求;@response是控制器对该请求的响应。
控制器测试断言
Rails 为控制器测试添加了几个断言,除了你已经见过的那些之外。控制器操作总是会渲染响应或重定向到不同的 URL。
assert_response type
如果 HTTP 响应与特定状态码匹配,则通过测试。对于type,可以使用状态码或符号:success、:redirect、:missing或:error。
assert_redirected_to options
如果请求导致重定向到options中给定的路径,则通过测试。
assert_template expected
如果请求渲染了expected模板,则通过测试。
这些断言验证控制器操作是否正确响应请求。对于简单的 GET 请求,assert_response :success可能是唯一需要的测试。如果控制器操作分配了实例变量,你还应验证该赋值操作。
让我们为UsersController中的new和create动作添加控制器测试。首先,测试new动作是否成功渲染带有新创建的User模型实例的注册表单。打开文件test/controllers/users_controller_test.rb,添加以下测试:
test "should get new with new user" do
➊ get :new
➋ user = assigns(:user)
➌ assert user.new_record?
assert_response :success
end
这个测试发出一个 GET 请求到新用户页面 ➊,获取控制器中分配给实例变量@user的值 ➋,并验证user是一个新记录 ➌,且响应成功。
下一个测试检查在给定有效数据时创建新用户的能力:
test "should create user" do
➊ params = {
user: {
email: "user@example.com",
password: "password",
password_confirmation: "password"
}
}
➋ post :create, params
➌ assert_redirected_to root_url
end
这个测试稍微复杂一些,因为create动作需要一个包含新用户数据的哈希 ➊。这个测试通过params哈希向create动作发出 POST 请求 ➋,然后验证该动作是否重定向到root_url ➌。
上一个测试检查了User成功保存时发生的情况。你应该测试控制器动作中的另一条路径,即当User无法保存时。你可以添加一个测试,尝试创建一个具有无效属性的用户,并验证是否再次渲染新用户模板。
使用bin/rake test:controllers命令运行新的控制器测试:
$ **bin/rake test:controllers**
UsersController的测试应该成功通过,那么我们继续进行PostsController的测试。验证before_action方法authenticate_user!是否正常工作,以确保应用程序不会向未认证用户展示帖子。
打开文件test/controllers/posts_controller_test.rb,并添加以下测试:
test "redirects anonymous users to login" do
➊ get :index
➋ assert_redirected_to login_url
end
test "get index for authenticated users" do
➌ user1 = users(:user1)
➍ get :index, {}, { user_id: user1.id }
assert_response :success
end
第一个测试尝试 GET 请求文章index页面 ➊,并验证该动作是否重定向到登录页面 ➋。第二个测试使用一个数据固定项初始化一个用户 ➌,然后带着user_id在会话中发出 GET 请求访问index页面 ➍。通过在会话中包含有效的user_id来模拟登录用户,应该能获得一个成功的响应。
集成测试
集成测试验证不同控制器之间的交互。这些测试通常用于测试应用程序中多个页面之间的流程。一个流程的例子可能是登录应用、查看页面,然后执行某个其他操作。这些操作中的每一个都可以通过控制器测试来覆盖。集成测试确保它们能一起工作。
集成辅助工具
因为集成测试通常涉及在应用程序页面之间移动,所以你的测试不仅需要向动作发出请求,还需要跟随任何重定向。辅助方法redirect?和follow_redirect!分别检查最后的请求是否导致重定向,并跟随重定向响应。
如果你知道一个请求会导致重定向,可以使用更具体的方法。你可以使用get_via_redirect、post_via_redirect、put_via_redirect、patch_via_redirect或者delete_via_redirect来发出适当的请求并跟随重定向。
测试流程
Rails 不会像模型和控制器测试那样自动创建集成测试,因为 Rails 无法知道你想要测试哪些流程。尽管它们不会自动创建,但 Rails 确实提供了一个生成器,你可以用来创建集成测试。
让我们添加一个集成测试,验证用户是否可以登录应用程序、查看主页然后退出。首先,使用bin/rails generate命令创建一个新的集成测试:
$ **bin/rails g integration_test user_flow**
这个命令创建一个新文件,命名为test/integration/user_flow_test.rb。在你的编辑器中打开该文件,让我们添加一个测试:
require 'test_helper'
class UserFlowTest < ActionDispatch::IntegrationTest
test "user login, browse, and logout" do
user = users(:user1)
➊ get "/login"
a
assert_response :success
➋ post_via_redirect "/sessions",
email: user.email,
password: "password"
assert_equal "/", path
➌ get_via_redirect "/logout"
assert_equal "/login", path
end
end
这个测试看起来像是一个扩展的控制器测试。该测试使用get请求页面➊,然后验证响应是否成功。你知道用户是通过向会话路径发送 POST 请求来登录应用程序,然后被重定向到主页,所以你使用post_via_redirect方法提交用户的电子邮件地址和密码,随后自动跟随重定向➋。最后,测试发出一个 GET 请求到登出页面➌,并被重定向回登录页面。
输入以下命令来运行集成测试:
$ **bin/rake test test/integration/user_flow_test.rb**
Run options: --seed 51886
# Running:
.
Finished in 1.049118s, 0.9532 runs/s, 2.8595 assertions/s.
1 runs, 3 assertions, 0 failures, 0 errors, 0 skips
该测试确认用户能够登录应用程序、查看主页并成功退出。
目前,这条路径基本上是用户在应用程序中可以走的唯一路径。当你向应用程序添加更多操作时,可以创建集成测试来验证其他流程是否正常工作。
使用测试驱动开发添加功能
到目前为止编写的所有测试都验证了现有功能,但一些 Rails 开发者使用测试在实现功能之前定义功能,这种做法被称为测试驱动开发(TDD)。在 TDD 中,你首先编写一个测试,然后添加代码使测试通过。一旦测试通过,你可以根据需要重构代码。如果你遵循 TDD,就不必担心以后解析代码来弄清楚需要验证哪些功能。
TDD 通常是一个三步过程,称为红-绿-重构:
-
编写一个失败的测试(红色)。
-
编写代码使测试通过(绿色)。
-
根据需要重构(refactor)。
通过遵循这个过程,你可以确信新功能符合测试中指定的要求,并且没有引入任何回归问题。
让我们使用 TDD 向我们的社交应用程序添加功能。尽管许多功能仍然缺失,但我们先集中关注这些:
-
添加一个用户
show页面,显示用户的帖子并包含一个“关注”按钮。 -
让用户能够创建新帖子。
对于这些功能,你将首先编写一个失败的测试,然后编写代码使测试通过。
显示用户
用户的 show 页面显示了用户的姓名和帖子,应该还包括一个按钮,允许其他用户关注该用户。为了添加用户的 show 页面,你需要在用户控制器中添加一个 show 方法并创建相应的视图。你知道控制器应该为视图分配一个名为 @user 的实例变量并返回成功响应,所以我们来为此添加一个测试。
打开文件 test/controllers/users_controller_test.rb 并添加这个测试:
test "should show user" do
user = users(:user1)
get :show, id: user.id
assert assigns(:user)
assert_response :success
end
现在,运行测试并确保它失败:
$ **bin/rake test test/controllers/users_controller_test.rb**
运行这个测试应该会导致一个错误。由于你还没有创建 show 操作,所以找不到 UsersController 的 show 操作。现在让我们向 app/controllers/users_controller.rb 中添加 show 操作:
class UsersController < ApplicationController
**def show**
**@user = User.find(params[:id])**
**@posts = @user.posts.order("created_at DESC")**
**end**
--*snip*--
保存文件并再次运行测试。这次你应该会看到一个不同的错误。模板丢失了。创建一个新的文件 app/views/users/show.html.erb,并现在添加该模板:
<div class="page-header">
<h1>User</h1>
</div>
<p class="lead"><%= @user.name %></p>
<h2>Posts</h2>
<%= render @posts %>
<%= link_to "Home", root_path,
class: "btn btn-default" %>
保存这个文件并再次运行测试。所有测试现在应该都通过了,但你仍然有一个问题。这个页面显示了用户的电子邮件地址和用户的帖子,但没有人可以关注该用户!
关注一个用户会在数据库的 subscriptions 表中创建一条记录。由于这必须在服务器端进行,添加关注按钮需要一个控制器操作和一个新的路由到该操作。
在 test/controllers/users_controller_test.rb 中添加另一个控制器测试,以描述这个操作:
test "should follow user" do
➊ user1 = users(:user1)
user2 = users(:user2)
➋ get :follow, { id: user2.id }, { user_id: user1.id }
➌ assert user1.following? user2
assert_redirected_to user_url(user2)
end
这个测试首先使用数据填充(fixtures)创建两个用户 ➊。接下来,它使用第二个用户的 id 作为参数,且将第一个用户的 id 放入会话中,发出对 follow 操作的 GET 请求 ➋。这模拟了 user1 关注 user2 的场景。最后,它验证 user1 已经关注了 user2,并且请求会重定向回 user2 的 show 页面 ➌。
现在打开 app/controllers/users_controller.rb 文件,并在其他操作之后但在 private 方法之前添加 follow 操作:
class UsersController < ApplicationController
--*snip*--
**def follow**
➊ **@user = User.find(params[:id])**
➋ **if current_user.follow!(@user)**
➌ **redirect_to @user, notice: "Follow successful!"**
**else**
**redirect_to @user, alert: "Error following."**
**end**
**end**
private
--*snip*--
这个方法使用 id 参数 ➊ 找到正确的用户,调用当前用户的 follow! 方法 ➋,然后重定向到 @user ➌。
现在打开 config/routes.rb 并为新的 follow 操作添加一个路由:
Rails.application.routes.draw do
--*snip*-
get 'signup', to: 'users#new', as: 'signup'
**get 'follow/:id', to: 'users#follow', as: 'follow_user'**
--*snip*-
end
我将其添加到了 signup 路由下,因为这两个操作都在用户控制器中。现在,回到 app/views/users/show.html.erb 中,你可以添加关注按钮了:
--*snip*-
<p class="lead"><%= @user.name %></p>
**<%= link_to "Follow", follow_user_path(@user),**
**class: "btn btn-default" %>**
<h2>Posts</h2>
--*snip*--
关注按钮类似于首页按钮;它实际上是一个带有 Bootstrap btn 和 btn-default 样式的链接,使其看起来像一个按钮。
现在你可以重新运行控制器测试,确保它们全部通过。如果 Rails 服务器尚未启动,你也可以启动它,然后在你的浏览器中访问 http://localhost:3000/users/1 来查看第一个用户的 show 页面,正如 图 10-1 所示。

图 10-1. 用户显示页面
图 10-1 是show页面,包含用户的姓名、关注该用户的按钮和该用户的帖子。
创建帖子
现在,让我们为用户提供添加帖子功能。添加帖子需要两个控制器动作:new和create。new动作也需要一个相应的视图。create动作应重定向到新创建的帖子,因此不需要视图。
你的应用程序有两种不同类型的帖子。从添加创建TextPost类型帖子的功能开始。TextPostsController中的new动作应实例化一个新的TextPost对象,并为该对象渲染一个表单。在test/controllers/text_posts_controller_test.rb中添加一个失败的测试,然后开始工作:
test "get new with new post" do
➊ user1 = users(:user1)
➋ get :new, {}, { user_id: user1.id }
text_post = assigns(:text_post)
assert text_post.new_record?
assert_response :success
end
测试首先使用数据集创建一个新用户 ➊,然后发送一个 GET 请求到new动作,并在会话中设置user_id ➋。这一步是必要的,因为TextPostsController要求一个经过身份验证的用户。测试接着获取text_post实例变量,验证它是一个新记录,并验证响应成功。运行测试并观察这个测试失败:
$ **bin/rake test test/controllers/text_posts_controller_test.rb**
错误消息应指出TextPostsController中缺少new动作。打开app/controllers/text_posts_controller.rb,并添加new动作:
class TextPostsController < ApplicationController
**def new**
**@text_post = TextPost.new**
**end**
end
你几乎已经完成了测试的所有步骤。最后一步是添加相应的视图。创建文件app/views/text_posts/new.html.erb,并添加以下内容:
<div class="page-header">
<h1>New Text Post</h1>
</div>
<%= render 'form' %>
这个视图是一个页面头部,后跟用于表单部分的render命令。让我们现在添加这个部分。首先,创建文件app/views/text_posts/_form.html.erb,并添加这个表单:
<%= form_for @text_post do |f| %> ➊
<div class="form-group">
<%= f.label :title %>
<%= f.text_field :title, class: "form-control" %> ➋
</div>
<div class="form-group">
<%= f.label :body %>
<%= f.text_area :body, class: "form-control" %> ➌
</div>
<%= f.submit class: "btn btn-primary" %> ➍
<%= link_to 'Cancel', :back, class: "btn btn-default" %>
<% end %>
这个部分为分配给@text_post ➊的新TextPost创建了一个表单。表单包括一个用于帖子标题的文本框 ➋、一个用于帖子内容的文本区域 ➌,以及提交表单或取消并返回的按钮 ➍。
在编辑视图时,在主页上添加一个创建新文本帖子按钮。打开app/views/posts/index.html.erb,然后在页面头部下方添加这个链接:
<p>
<%= link_to "New Text Post", new_text_post_path,
class: "btn btn-default" %>
</p>
现在你应该能够成功运行TextPostController的测试。现在,在test/controllers/text_posts_controller_test.rb中添加另一个控制器测试,用于描述创建一个TextPost:
test "should create post" do
➊ user = users(:user1)
➋ params = {
text_post: {
title: "Test Title",
body: "Test Body"
}
}
➌ post :create, params, { user_id: user.id }
text_post = assigns(:text_post)
➍ assert text_post.persisted?
assert_redirected_to post_url(text_post)
end
与之前的TextPostsController控制器测试类似,本测试首先从一个数据集初始化一个新的用户 ➊。接下来,它为一个新的TextPost设置必要的参数 ➋,然后发送一个 POST 请求 ➌到create动作,附带params哈希和会话中的user.id。最后,它确保 ➍ 新的文本帖子已成功保存到数据库,并且请求重定向到新帖子的 URL。
使这个测试通过的第一步是向TextPostsController添加一个create动作。打开文件app/controllers/text_posts_controller.rb,并添加以下方法:
class TextPostsController < ApplicationController
--*snip*--
**def create**
**@text_post =**
➊ **current_user.text_posts.build(text_post_params)**
**if @text_post.save**
➋ **redirect_to post_path(@text_post),**
**notice: "Post created!"**
**else**
➌ **render :new, alert: "Error creating post."**
**end**
**end**
end
create 方法使用表单中的 params 为当前用户创建一个新的文本帖子➊。如果能够保存该新帖子,它将重定向用户➋ 到新创建的帖子页面。否则,它会再次呈现带有错误消息的新建文本帖子表单 ➌。
最后,添加 text_post_params 方法来处理 Rails 强参数。这一方法会在 create 操作中调用,以获取新建 TextPost 的允许参数。将此私有方法添加到 TextPostsController 类的底部:
class TextPostsController < ApplicationController
--*snip*--
**private**
**def text_post_params**
➊ **params.require(:text_post).permit(:title, :body)**
**end**
end
这个方法确保➊ params 哈希包含 :text_post 键,并允许在 :text_post 键下的 :title 和 :body 键值对。进行此更改后,所有测试应重新通过。在首页点击 新建文本帖子 按钮,如图 10-2 所示,查看创建 TextPost 的表单。

图 10-2. 新建文本帖子表单
创建新的 ImagePost 的过程类似。本章末的练习 3 将引导你完成必要的步骤。
这些新功能使我们的应用更加接近一个完整的社交网络。
总结
本章涵盖了很多内容。你了解了 MiniTest 框架,编写了模型、控制器和集成测试。我们讨论了测试驱动开发,并使用它向社交网络添加了功能。
你可以在代码之前或之后编写测试,且可以使用任何测试框架——重要的是你要编写测试。能够输入一个命令并验证应用是否正常工作,值得花费一些时间来实现。在应用的生命周期中,全面的测试集带来的好处是无法估量的。
练习
| 问: | 1. 目前你无法通过点击 URL 到达用户的 show 页面。请更新 TextPost 和 ImagePost 的局部视图,使得用户的 name 成为指向该用户 show 页面链接。同时,在应用布局的顶部,Log Out 链接旁边,添加一个名为 Profile 的链接,指向当前用户的 show 页面。 |
|---|
| 问:| 2. follow 操作不应对匿名用户开放。请在 UsersController 中调用 before_action :authenticate_user! 并使用 only 选项,要求在执行 follow 操作前进行身份验证。更新 UsersController 后,以下测试应该通过:
test "follow should require login" do
user = users(:user1)
get :follow, { id: user.id }
assert_redirected_to login_url
end
此外,用户 show 页面上的 Follow 按钮不应在匿名用户或当前用户已关注该展示用户的情况下显示。请更新 show 视图来解决这个问题。|
| 问: | 3. 为图像帖子添加new和create操作,以及create操作中使用的私有image_post_params方法,位置在app/controllers/image_posts_controller.rb中。然后在app/views/image_posts/new.html.erb中为new操作创建视图,并在app/views/image_posts/_form.html.erb中为ImagePost表单创建部分视图。 |
|---|
将以下控制器测试添加到test/controllers/image_posts_controller_test.rb中。在你将操作添加到ImagePostsController并创建相关视图后,两个测试应当通过。
test "get new with new post" do
user1 = users(:user1)
get :new, {}, { user_id: user1.id }
image_post = assigns(:image_post)
assert image_post.new_record?
assert_response :success
end
test "should create post" do
user = users(:user1)
params = {
image_post: {
title: "Test Title",
url: "http://i.imgur.com/Y7syDEa.jpg"
}
}
post :create, params, { user_id: user.id }
image_post = assigns(:image_post)
assert image_post.persisted?
assert_redirected_to post_url(image_post)
end
你对这些操作和视图的实现应类似于 TextPost 的new和create操作以及视图。如果你想练习 TDD,欢迎先添加这些测试并确认它们失败,再开始实现操作。
第十一章:安全
当用户在你的网站上注册账户时,他们信任你会保护他们的数据安全。遗憾的是,随着你的应用越来越受欢迎,攻击的可能性也会增加。即使你的应用目前不太受欢迎,它仍然可能成为自动化系统的目标,这些系统会扫描网络,寻找易受攻击的网站。
在本章中,你将学习四种最常见的安全漏洞,以及如何保护你的站点免受它们的侵害。我们将讨论授权、注入攻击、跨站脚本攻击和跨站请求伪造攻击。
授权攻击
你在第九章中创建了一个认证系统,但认证并不等于授权。认证是用来识别用户的。授权则是指定一个已登录用户可以在你的应用中访问的内容。你的认证系统通过电子邮件地址和密码来识别用户。授权系统通常处理角色或权限。
到目前为止,你还没有为应用中的用户定义角色,但应该为一些权限做出规定。例如,用户应该能够查看和编辑自己发布的帖子,但只能查看其他用户发布的帖子。用户还应该能够管理自己帖子下的评论,即使这些评论是由其他用户添加的。
授权攻击发生在用户设法绕过权限,访问属于另一个用户的资源时。最常见的授权攻击类型是不安全的直接对象引用,这意味着用户可以通过操控 URL 来访问你应用中受限的资源。
让我们来看一个来自社交应用的示例。这段代码示例创建了一个方法,允许用户编辑之前创建的文本帖子,但它包含一个资源查找,这使得不安全的直接对象引用成为可能:
def edit
@text_post = TextPost.find(params[:id])
end
这个方法使用作为 URL 一部分传入的id参数来查找要编辑的TextPost,无论最初是谁创建的。因为这段代码没有检查哪个用户在尝试访问该帖子,所以任何经过身份验证的用户都可以编辑应用中的任何帖子。用户只需打开自己的一个帖子进行编辑,找出 URL 中表示帖子的id的部分,并将该值更改为另一个帖子的id。
你只希望用户能够编辑他们自己的帖子。下面的列表展示了一种更好的处理这种查找的方法:
def edit
**@text_post = current_user.text_posts.find(params[:id])**
end
通过使用current_user.text_posts,find方法仅限于查找当前用户的帖子。现在,如果用户在尝试修改其他用户的帖子时更改了 URL 中的id,find将会失败,用户应该会看到 404 错误页面。如果某个资源属于某个用户,在数据库中查找该资源时,始终引用该用户。
现在你已经知道了正确的找到要编辑的帖子的方法,将之前的方法添加到文本帖子控制器 app/controllers/text_posts_controller.rb 中。当用户提交edit文本帖子表单时,修改内容会发送到update操作。使用相同的授权方法,为文本帖子添加update方法:
def update
➊ @text_post = current_user.text_posts.find(params[:id])
➋ if @text_post.update(text_post_params)
redirect_to post_path(@text_post), notice: "Post updated!"
else
render :edit, alert: "Error updating post."
end
end
该方法会找到属于当前用户的正确文本帖子 ➊,并使用文本帖子表单中的params调用update方法 ➋。如果update调用成功,文本帖子会在数据库中更新,用户会被重定向到更新后的帖子。否则,edit视图会重新渲染,并显示错误信息。
接下来,创建文件 app/views/text_posts/edit.html.erb,并为文本帖子添加edit视图:
<div class="page-header">
<h1>Edit Text Post</h1>
</div>
<%= render 'form' %>
这个视图与文本帖子的new视图相同,唯一不同的是标题。这个视图重用了你在上一章中创建的表单局部视图。最后,在 app/views/text_posts/_text_post.html.erb 中的TextPost局部视图添加指向edit操作的链接。
<%= text_post.body %>
➊ **<% if text_post.user == current_user %>**
**<p>**
**<%= link_to 'Edit', edit_text_post_path(text_post),**
**class: "btn btn-default" %>**
**</p>**
**<% end %>**
</div>
</div>
该链接仅在文本帖子属于当前用户 ➊ 时显示。编辑图片帖子遵循相同的模式。将edit和update方法添加到 app/controllers/image_posts_controller.rb,为图片帖子在 app/views/image_posts/edit.html.erb 创建edit视图,并在 app/views/image_posts/_image_post.html.erb 中的ImagePost局部视图添加指向edit操作的链接。这些步骤在本章末的练习 1 中讲解。
注入攻击
注入攻击发生在用户的输入作为应用程序的一部分被执行时。注入攻击非常常见,尤其是在较旧的应用程序中。
避免注入攻击的第一条规则是绝不信任用户输入。如果一个应用程序没有确保用户输入的所有数据都是安全的,那么它就容易受到注入攻击。记住这一点,我们将在本节中讨论两种类型的注入攻击:SQL 注入和跨站脚本攻击。
SQL 注入
在SQL 注入攻击中,用户输入会直接添加到 SQL 语句中。如果恶意用户提供实际的 SQL 代码作为输入,他或她可能绕过应用程序的授权系统,查询应用程序的数据库,并获取或删除受限信息。
例如,考虑一个没有使用 Rails 内建安全密码功能的应用程序。相反,开发者将用户名和密码存储在数据库中,并编写了自己的authenticate方法来验证用户的凭证。这个自定义的User.authenticate方法展示了不该做的事情,因为它容易受到 SQL 注入攻击:
class User < ActiveRecord::Base
➊ def self.authenticate(username, password)
➋ where("username = '#{username}' " +
"AND password = '#{password}'").first
end
end
该方法接受username和password ➊ 作为参数。这些值由用户输入并作为参数传递给控制器。然后,这些变量会使用字符串插值添加到where调用中 ➋。
这个方法对于有效的username和password组合会返回正确的用户对象。例如,假设一个User的username为 tony,password为 secret,这个方法会返回该User:
User.authenticate("tony", "secret")
=> #<User id: 1, username: ...>
方法调用然后生成以下 SQL 代码:
SELECT * FROM "users"
WHERE (username = 'tony' AND password = 'secret')
ORDER BY "users"."id" ASC
LIMIT 1
该方法在传入无效的username和password组合时也能正确工作:
User.authenticate("tony", "wrong")
=> nil
在这种情况下,password无效,因此方法返回nil。到目前为止,一切正常!
绕过认证系统
不幸的是,经验丰富的攻击者知道一个方便的 SQL 字符串,可以完全绕过这个authenticate方法:' OR '1'='1。在 SQL 中,'1'='1'语句的结果是TRUE,所以如果它与OR一起添加到任何其他条件语句中,整个条件语句都会被计算为TRUE。
让我们看看当这个字符串传递给authenticate方法的username和password时会发生什么:
User.authenticate("' OR '1'='1", "' OR '1'='1")
=> #<User id: 1, username: ...>
我没有向方法传递任何有效的数据,那么authenticate方法是如何成功的呢?方法调用生成的 SQL 代码揭示了其中的技巧:
SELECT * FROM "users"
WHERE (username = '' OR '1'='1' AND password = '' OR '1'='1')
ORDER BY "users"."id" ASC
LIMIT 1
即使数据库中没有username和password为空字符串的用户,OR '1'='1'的添加使得WHERE子句计算为TRUE,然后该方法返回数据库中的第一个用户。攻击者现在以第一个用户身份登录。这个攻击的危害加剧,因为数据库中的第一个用户通常是应用程序的创建者,他可能还拥有特殊权限。
防止 SQL 注入
幸运的是,您通常可以通过仔细检查代码找到 SQL 注入错误。如果您看到where方法内部有字符串插值,假设它是危险的并且需要修正。
如果您必须构建自己的查询字符串,请切换到哈希条件:
def self.authenticate
username = params[:username]
password = params[:password]
where(username: username,
password: password).first
end
在这里,字符串被完全移除出对where方法的调用。
跨站脚本
跨站脚本攻击(XSS)是另一种常见的注入攻击。在跨站脚本攻击中,攻击者被允许向您的应用程序中输入恶意的 JavaScript 代码。任何文本字段都可能被用于跨站脚本攻击。当另一个用户查看包含恶意 JavaScript 的页面时,用户的浏览器会将代码作为应用程序的一部分执行。
跨站脚本漏洞可以被利用来篡改您的网站,甚至显示假的登录表单,试图窃取用户凭据。如果攻击者能够向您的网站注入代码,几乎可以做任何事情。
内置保护
Rails 默认提供跨站脚本保护。除非您明确绕过此保护,否则您的应用程序是安全的,免受 XSS 攻击。作为快速检查,尝试在新文本帖子的正文中输入以下 JavaScript 代码:
**<script>alert('XSS');</script>**
保存此帖子后,你会看到在显示文本之前,Rails 会首先转义所有 HTML 标签,通过将特殊字符替换为相应的字符实体,正如图 11-1 所示。

图 11-1. 带有转义 HTML 的文本帖子
例如,小于号被替换为 <,大于号被替换为 >。这些代码不会被执行,而是像其他文本一样显示在页面上。所以如果你不打算允许用户在你的网站中输入 HTML,你的应用程序就可以避免跨站脚本攻击。
不幸的是,用户可能会希望在你的应用程序中输入 HTML 标签来格式化他们的帖子。在这种情况下,你的网站至少需要接受一些 HTML 标签。你可以通过在视图中使用 raw 辅助方法来关闭 HTML 标签的自动转义功能。打开 app/views/text_posts/_text_post.html.erb 并在 text_post.body 前添加 raw:
--*snip*--
<%= **raw** text_post.body %>
--*snip*--
现在,当你在浏览器中刷新页面时,script 标签将不会被转义,你应该会看到一个弹出窗口,显示“XSS”,如图 11-2 所示。

图 11-2. 带有 XSS 漏洞的文本帖子
诀窍是让你的应用程序只接受安全的标签,如 <strong> 用于加粗,<em> 用于斜体,<p> 用于标记段落,同时拒绝诸如 <script> 这样的危险标签。你可能会想自己编写一个辅助方法来处理这些危险标签,但幸运的是,Rails 提供了 sanitize 辅助方法来帮你处理这些问题。
sanitize 方法
sanitize 辅助方法会移除所有未明确允许的 HTML 标签,只有在白名单中的标签才会被保留。你可以通过在 Rails 控制台输入ActionView::Base.sanitized_allowed_tags查看允许的标签列表。
在 Rails 控制台尝试一些 sanitize 方法的示例,熟悉它是如何工作的:
irb(main):001:0> **helper.sanitize("<p>Hello</p>")**
=> "<p>Hello</p>"
irb(main):002:0> **helper.sanitize("<script>alert('XSS')</script>")**
=> ""
你可以通过在 options 哈希中为 tags 键指定值,来自定义允许的标签数组:
irb(main):003:0> **helper.sanitize("<p>Hello</p>", tags: ["em", "strong"])**
=> "Hello"
现在你已经看到 sanitize 方法的实际效果,将 TextPost 部分中之前编辑的 raw 方法调用替换为 sanitize。
--*snip*--
<%= **sanitize** text_post.body %>
--*snip*--
再次刷新页面,你应该不再看到警告。
跨站请求伪造攻击
跨站请求伪造(CSRF)攻击发生在你的应用程序的用户访问了一个被攻击者修改过的站点,该站点专门针对你的站点。恶意站点试图利用你应用程序对该用户的信任,向你的应用程序提交请求。
要利用 CSRF 漏洞,攻击者必须首先在你的应用程序中找到这个漏洞。接下来,他或她必须创建一个页面,里面包含指向该漏洞的链接。最后,攻击者必须诱使你的应用程序用户访问这个恶意页面并激活链接。
CSRF 是如何工作的
假设你正在构建一个在线支付应用程序。你的应用程序包括一个transfer操作,接受amount和to参数,指定要将多少钱转账给另一个用户。
攻击者可能会研究你的网站生成的请求,并尝试在他或她自己的网站上复制这些请求,使用像 HTML 图像标签这样简单的方式:
<img src="http://*yoursite.com*/transfer?amount=100&to=attacker">
每次有人访问此页面时,用户的浏览器会发出一个 GET 请求来加载这个图像。如果访问者已登录到你的站点,并且你的站点容易受到 CSRF 攻击,那么$100 会从访问者的账户转到攻击者的账户。
你并没有构建一个支付站点,但你的站点容易受到 CSRF 攻击。在第十章中,你为一个用户添加了一个方法,可以让他/她在网站上关注另一个用户。在这样做时,你向 config/routes.rb 添加了以下一行:
get 'follow/:id', to: 'users#follow', as: 'follow_user'
通过查看我点击“关注”按钮时生成的请求,我可以创建一个恶意链接来利用这个漏洞。假设我的账户id是 10,链接会像这样:
<img src="http://*yoursite.com*/follow/10">
现在,我只需要说服其他用户访问一个包含这个图像标签的页面,他们就会自动关注我。
防止 CSRF
你可以通过两个步骤来防止 CSRF 攻击。首先,包含一个用户特定的令牌在所有改变应用程序状态的请求中,并忽略任何不包含此令牌的请求。其次,永远不要使用 GET 请求来改变状态。如果一个请求可能会创建或更改数据库或会话中的数据,它应该使用 POST 请求。
Rails 默认会处理包括一个秘密令牌并拒绝请求。打开 app/views/layouts/application.html.erb 文件,可以查看包括令牌的代码:
<%= csrf_meta_tags %>
在浏览器中加载你的网站,然后查看源代码,查看页面head中由该方法生成的meta标签。
<meta content="authenticity_token" name="csrf-param" />
<meta content="KA1Q/JoVfI+aV6/L4..." name="csrf-token" />
你还可以在应用程序中的每个表单里看到一个隐藏字段,里面包含authenticity_token。每次提交表单时,这个隐藏字段的值会与其他参数一起提交。authenticity_token也会自动包含在所有的 POST 请求中。
现在打开 app/controllers/application_controller.rb 文件,查看实际拒绝无效请求的代码:
protect_from_forgery with: :exception
在这里,Rails 更进一步,对那些没有包括 CSRF 令牌的请求抛出异常。这个异常会被记录下来,可以用来追踪攻击者。
第二步必须由你自己处理。每次你添加控制器操作时,确保如果该操作可能更改数据,就不要使用 GET 请求。在第十章中添加的 follow 操作会在数据库中创建一条记录,因此它应该使用 POST 请求。POST 请求会自动包含 authenticity_token,并且 Rails 会通过 ApplicationController 中的 protect_from_forgery 方法验证该令牌。
要修正应用中的这个漏洞,打开 config/routes.rb 并将 follow 操作改为使用 POST 而不是 GET:
--*snip*--
**post** 'follow/:id', to: 'users#follow', as: 'follow_user'
--*snip*--
现在,更新 app/views/users/show.html.erb 中的链接,改为使用 POST 方法,而不是默认的 GET:
<%= link_to "Follow", follow_user_path(@user),
**method: :post**, class: "btn btn-default" %>
通过这两个更改,follow 操作现在应该可以防止 CSRF 攻击。
总结
如今,恶意用户和网站在网络上不可避免。随着你的应用越来越受欢迎,遭遇攻击的风险也会增加。幸运的是,Rails 提供了你所需的工具来保护你的应用和用户免受攻击。
本章中涵盖的安全漏洞来自《开放网络应用程序安全项目(OWASP)》发布的前十名漏洞列表。访问 www.owasp.org/ 查找你所在地区的 OWASP 分会,并参加当地免费的应用安全会议。
现在你的应用已经功能完备且安全,接下来我们将在下一章讨论性能。没有人喜欢慢速的 Web 应用!在这一点上,Rails 同样提供了若干提升应用性能的工具,但你需要将它们付诸实践。
练习
| 问题: | 1. 用户应该能够编辑他们的图片帖子。在 app/controllers/image_posts_controller.rb 中添加 edit 和 update 方法。还要在 app/views/image_posts/edit.html.erb 中添加 ImagePost edit 视图。最后,在 app/views/image_posts/_image_post.html.erb 中的 ImagePost 部分添加指向 edit 操作的链接。这些方法和视图应该类似于你为文本帖子添加的内容。 |
|---|
| 问题: | 2. 用户应能够管理他们自己帖子的评论。首先,在 PostsController 中的 show 操作内添加 @can_moderate 实例变量,如下所示:
--*snip*--
def show
@post = Post.find(params[:id])
**@can_moderate = (current_user == @post.user)**
end
end
该变量为 true 时表示 current_user 是正在显示的帖子作者。现在,更新 app/views/comments/_comment.html.erb 中的评论部分,如果 @can_moderate 的值为 true,则包括指向 destroy 操作的链接。最后,将 destroy 操作添加到 app/controllers/comments_controller.rb 中的 CommentsController。该操作应使用 params 哈希中的 id 查找正确的评论,调用该评论的 destroy 方法,然后重定向到 post_path,并显示成功或失败的消息。
| 问题: | 3. 你需要修正应用中的另一个 CSRF 漏洞。打开 config/routes.rb 文件,查看 logout 路由:
--*snip*--
get 'login', to: 'sessions#new', as: 'login'
➊ get 'logout', to: 'sessions#destroy', as: 'logout'
root 'posts#index'
end
该路由指向SessionsController中的destroy操作,并且你正在使用 GET 请求 ➊来访问它。将该路由从get改为delete,这样就需要使用 DELETE 请求。此外,在应用程序布局的app/views/layouts/application.html.erb中的登出链接上添加method: :delete。
第十二章 性能
Ruby on Rails 相对于其他语言和 Web 框架的性能仍然是一个争议话题。通过快速 Google 搜索可以发现,许多人认为 Ruby on Rails 很慢。
Ruby 解释器的更新版本在性能方面取得了显著进展。Ruby 2.0 包括了垃圾回收优化和其他改进,使其比旧版本更快。Ruby 2.1 引入了一个代际垃圾收集器,性能更为出色。
随着 Ruby 语言的进步,Ruby on Rails 也在不断改进。Rails 框架现在包括了几项专门用于提升应用程序性能的功能。本章首先讨论其中的两项内建功能,然后介绍一些你可以做的事情来提高性能。最后,我将讨论 Rails 支持的缓存技术。
内建优化功能
资源管道和 Turbolinks 是你在创建原始博客时就已经使用的两个内建的 Rails 性能优化功能。这两项功能在新的 Rails 应用程序中默认启用,我们将在这里探索它们是如何工作的。
资源管道
资源管道 是 Rails 的一项功能,它将你应用程序中使用的所有单独的 JavaScript 和 CSS 文件合并成一个 JavaScript 文件和一个 CSS 文件,从而减少浏览器为了渲染网页所发出的请求数量,因为你的应用程序使用了多个同类型的文件。浏览器并行请求的数量是有限的,因此较少的请求应该能使页面加载速度更快。
资源管道还会 压缩,或者说是压缩 JavaScript 和 CSS 文件,通过移除空白字符和注释。文件更小,加载更快,因此你的网页加载速度也更快。
最后,资源管道预处理器还使你能够使用更高级的语言,如 CoffeeScript 代替 JavaScript,以及 Sass 代替普通的 CSS。这些高级语言的文件会在被服务之前,通过各自的编译器预编译成普通的 JavaScript 和 CSS,以便浏览器能够理解。
清单
当你为应用程序生成控制器时,Rails 还会在 app/assets/javascripts 和 app/assets/stylesheets 目录中生成相应的 JavaScript 和 CSS 文件。Rails 使用清单文件,而不是单独链接到这些文件。正如在 第五章中提到的,清单文件是应用程序所需的其他文件的列表。
清单文件使用 指令,这些指令指定要包含的其他文件,以便生成一个用于生产环境的单一文件。require 指令包含清单中的单一文件。require_tree 指令包含一个目录中的所有文件。require_self 指令包含清单文件的内容。
要查看示例,请打开默认的 CSS 清单文件 app/assets/stylesheets/application.css:
/*
* This is a manifest file that'll be compiled into application.css,
* which will include all the files listed below.
*
--*snip*--
*
➊ *= require_tree .
➋ *= require bootstrap
➌ *= require_self
*/
这个文件首先使用require_tree .指令 ➊来包含当前目录下的所有 CSS 文件。接着,它使用你在第九章中添加的require bootstrap指令包含了 Bootstrap 样式表。最后,require_self指令 ➌将这个文件的内容包括在注释块下面。目前,注释块下方没有任何内容。
资源管道默认在三个不同的位置搜索资源。你已经知道其中一个位置:app/assets目录用于存放你应用程序的 CSS、JavaScript 和图片文件。
lib/assets目录用于存放你编写的库所需的资源。由于你还没有编写任何库,这个目录目前是空的。vendor/assets目录用于存放第三方创建的资源,如 JavaScript 插件和 CSS 框架的代码。
Ruby gems 可以将它们自己的目录添加到资源管道搜索的路径列表中。你可以在 JavaScript 清单文件app/assets/javascripts/application.js中看到这一点:
// This is a manifest file that'll be compiled into application.js,
// which will include all the files listed below.
//
--*snip*--
//
➊ //= require jquery
//= require jquery_ujs
//= require turbolinks
➋ //= require_tree .
➌ //= require bootstrap
这个文件使用require指令 ➊来引入 jQuery、jQuery UJS 和 Turbolinks 库,这些库是你应用程序的Gemfile中包含的 jquery-rails 和 turbolinks gems 的一部分。
它接着使用require_tree ➋来包含当前目录下的所有 JavaScript 文件。最后,它要求 Bootstrap CSS 框架所需的 JavaScript 文件 ➌。
你不会在vendor/assets/javascripts目录中找到jquery、jquery_ujs、turbolinks或bootstrap。相反,提供这些文件的 gems 已经更新了资源管道搜索路径,将它们自己的目录添加到路径中。
你可以通过在 Rails 控制台中输入Rails.application.config.assets.paths来查看资源管道搜索路径的完整列表。这条语句将返回一个路径数组。在这个列表中,你应该能找到像jquery-rails-3.1.0/vendor/assets/javascript、turbolinks-2.2.2/lib/assets/javascripts和bootstrap-sass-3.1.1.0/vendor/assets/javascripts这样的路径。
调试模式
如你所见,在开发环境中,CSS 和 JavaScript 文件作为单独的、未压缩的文件提供。根据服务器输出,你的社交媒体应用程序提供了 31 个单独的 CSS 和 JavaScript 文件。一个名为调试模式的资源管道配置控制着每个环境中资源的处理方式。
在开发环境中,调试模式已开启。这意味着 CSS 和 JavaScript 文件中引用的文件将单独提供,如果你需要使用浏览器的开发工具调试文件中的问题,这样做非常有用。
如果你想强制将资源合并并预处理,以便查看它们在生产环境中如何提供,你可以关闭调试模式。只需在开发环境配置文件config/environments/development.rb的底部修改config.assets.debug的值即可:
config.assets.debug = **false**
当调试模式关闭时,Rails 会将所有文件合并并运行预处理器(如 CoffeeScript 或 Sass 编译器),然后再提供它们。修改此文件后,重新启动 Rails 服务器,然后检查终端中的服务器输出,看看有什么不同:
Started GET "/login" for 127.0.0.1 at 2014-03-16 20:38:43 -0500
Processing by SessionsController#new as HTML
Rendered sessions/new.html.erb within layouts/application (1.5ms)
Completed 200 OK in 5ms (Views: 4.5ms | ActiveRecord: 0.0ms)
➊ Started GET "/assets/application.css" for 127.0.0.1 at ...
➋ Started GET "/assets/application.js" for 127.0.0.1 at ...
现在只会提供两个文件(➊和➋)——CSS 和 JavaScript 清单文件。这个设置在开发模式下可能会减慢页面加载速度,因为每个请求都会将文件合并,因此在继续之前,请将config.assets.debug设置为true。
资源预编译
在生产环境中,你应该预编译应用程序的资源,并通过你的 Web 服务器将它们作为静态文件提供。你可以通过几种方式预编译资源。当你在第六章中将博客部署到 Heroku 时,你在部署过程中预编译了资源。Rails 还包括一个rake任务来预编译资源。
rake任务会从你的 CSS 和 JavaScript 清单中编译所有文件,并将它们写入public/assets目录。你可以使用以下命令为生产环境预编译资源:
$ **RAILS_ENV=production bin/rake assets:precompile**
在预编译过程中,会根据编译文件的内容生成一个 MD5 哈希,并将其插入到文件名中。当文件被保存时,文件名会基于文件内容,因此,如果你更新了文件,你可以确保提供的是正确版本的文件。
例如,预编译后,文件app/assets/stylesheets/application.css可能会被命名为public/assets/application-d5ac076c28e38393c3059d7167501838.css。Rails 视图助手会在生产环境中自动使用正确的文件名。开发环境中不需要编译过的资源,因此,当你看完它们后,可以使用assets:clobber rake任务将其删除:
$ **RAILS_ENV=production bin/rake assets:clobber**
这个命令会删除public/assets目录及其所有内容。
在第十五章中,你将学习如何使用一个名为 Capistrano 的程序将你的应用程序部署到自己的服务器。你可以配置 Capistrano,在部署过程中自动预编译资源,就像你在将博客部署到 Heroku 时所做的那样。
Turbolinks
资源管道减少了浏览器请求资源的次数,但浏览器仍然需要为每个页面解析并重新编译 CSS 和 JavaScript。根据你的应用程序中包含的 CSS 和 JavaScript 的数量,这可能会消耗相当多的时间。
Turbolinks是 Rails 的一个功能,它通过替换当前页面body和title的内容为新页面的数据,而不是加载整个新页面,从而加速了在你的应用程序中跟随链接的过程。使用 Turbolinks 时,点击链接时甚至不会下载 CSS 和 JavaScript 文件。
Turbolinks 的实际应用
在新的 Rails 应用程序中,Turbolinks 默认启用。从你在第二章构建第一个应用程序开始,你就一直在使用它。你可以通过查看 Rails 服务器的输出看到它在工作。打开浏览器,访问http://localhost:3000/,并检查终端中的输出:
Started GET "/" for 127.0.0.1 at ...
Processing by PostsController#index as HTML
--*snip*--
Started GET "/assets/bootstrap.js?body=1" for 127.0.0.1 at ...
Started GET "/assets/application.js?body=1" for 127.0.0.1 at ...
在对帖子索引页发出 GET 请求后,浏览器会获取应用程序所需的所有 CSS 和 JavaScript 文件。现在点击索引页上的一个链接,比如“新建文本帖子”,再次检查输出:
Started GET "/text_posts/new" for 127.0.0.1 at ...
Processing by TextPostsController#new as HTML
User Load (0.2ms) SELECT "users".* FROM "users"
WHERE "users"."id" = ? LIMIT 1 [["id", 7]]
Rendered text_posts/_form.html.erb (2.4ms)
Rendered text_posts/new.html.erb within layouts/application (3.3ms)
Completed 200 OK in 38ms (Views: 36.5ms | ActiveRecord: 0.2ms)
浏览器只会发出对新文本帖子的 GET 请求。它不会重新加载 CSS 和 JavaScript 文件,因为这些文件已经被加载到内存中。最后,点击浏览器中的返回按钮。
这次终端窗口中没有输出。索引页已在浏览器中缓存,没有请求发送到服务器。Turbolinks 默认会缓存十个页面。
JavaScript 事件
如果你的应用程序包含使用 jQuery 的 ready 函数来附加事件处理程序或触发其他代码的 JavaScript 代码,那么需要修改这些 JavaScript 代码,使其与 turbolinks 配合工作。因为 turbolinks 在点击链接时不会重新加载整个页面,所以 ready 函数不会被调用。
相反,page:load 事件会在加载过程结束时触发。你可以通过将以下 CoffeeScript 代码添加到 app/assets/javascripts/posts.js.coffee 来看到这个过程:
--*snip*--
**$(document).ready ->**
➊ **console.log 'Document Ready'**
**$(document).on 'page:load', ->**
➋ **console.log 'Page Load'**
不幸的是,CoffeeScript 超出了本书的范围,但如果你已经熟悉 JavaScript,可能会认识到这段代码的作用。它会在页面首次加载时在浏览器的 JavaScript 控制台打印“Document Ready” ➊,而当你点击一个使用 turbolinks 的链接时,打印“Page Load” ➋。
因为你当前没有使用 $(document).ready() 来触发任何 JavaScript 代码,所以你现在不需要担心这个问题。但如果你以后开始使用 ready 函数,应该重新查看这一部分内容。
代码优化
现在你已经看过了 Rails 提供的一些内建优化,我们来看看你可以做哪些额外的事情来提高性能。我将介绍一些你可以用来减少应用程序数据库查询次数并提升慢查询性能的技巧。
减少数据库查询
Rails 模型让访问数据变得如此简单,以至于你可能会忘记你实际上是在查询数据库。幸运的是,Rails 服务器会在终端显示 SQL 语句。当你浏览应用程序时,查看这些输出,找出可能的低效之处。
检查 SQL 输出
确保你的服务器正在运行,并且在我带你通过几个示例时密切关注终端输出。在开始之前,确保你已经登出了应用程序。首先,浏览到登录页面 http://localhost:3000/login,并检查服务器输出:
Started GET "/login" for 127.0.0.1 at 2014-03-18 18:58:39 -0500
Processing by SessionsController#new as HTML
Rendered sessions/new.html.erb within layouts/application (2.0ms)
Completed 200 OK in 12ms (Views: 11.8ms | ActiveRecord: 0.0ms)
这个页面不会生成任何 SQL 查询。
现在登录到应用程序:
Started POST "/sessions" for 127.0.0.1 at 2014-03-18 18:59:01 -0500
Processing by SessionsController#create as HTML
Parameters: ...
➊ User Load (0.2ms) SELECT "users".* FROM "users"
WHERE "users"."email" = 'alice@example.com' LIMIT 1
Redirected to http://localhost:3000/
Completed 302 Found in 70ms (ActiveRecord: 0.2ms)
该页面生成了一条 SQL 查询 ➊,因为 Rails 加载了与你在前一页面输入的电子邮件地址匹配的用户记录。SessionsController 中的 create 方法使用该记录来验证你输入的密码。
登录到应用后,你应该会被重定向到帖子索引页面。该页面的服务器输出应该类似于下面这样:
Started GET "/" for 127.0.0.1 at 2014-03-18 18:59:02 -0500
Processing by PostsController#index as HTML
➊ User Load (0.1ms) SELECT "users".* FROM "users"
WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
➋ (0.1ms) SELECT "users".id FROM "users" INNER JOIN
"subscriptions" ON "users"."id" = "subscriptions"."leader_id"
WHERE "subscriptions"."follower_id" = ? [["follower_id", 1]]
➌ Post Load (0.2ms) SELECT "posts".* FROM "posts"
WHERE "posts"."user_id" IN (2, 1)
ORDER BY created_at DESC
➍ User Load (0.1ms) SELECT "users".* FROM "users"
WHERE "users"."id" = ? LIMIT 1 [["id", 2]]
User Load (0.1ms) SELECT "users".* FROM "users"
WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
CACHE (0.0ms) SELECT "users".* FROM "users"
WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
Rendered collection (2.7ms)
Rendered posts/index.html.erb within layouts/application (3.8ms)
Completed 200 OK in 13ms (Views: 11.0ms | ActiveRecord: 0.6ms)
该页面生成了六条查询。首先,它查找了 id 为 1 的用户 ➊;该查询在 PostController 中的 authenticate_user! 调用中查找 current_user。接下来,页面通过 current_user.timeline_user_ids 查找当前用户所关注的用户的 id ➋。然后,它查找帖子 ➌,这些帖子的 user_id 与 current_user 或其关注者的 id 匹配。
最后,页面会进行三次连续的查询 ➍,通过 SELECT "users".* FROM "users" 查找与 id 匹配的用户。这看起来有点奇怪。我的索引页面上有三篇帖子,但多出了三条查询。我们来看一下 app/controllers/posts_controller.rb 中的 index 动作,看看发生了什么:
class PostsController < ApplicationController
➊ before_action :authenticate_user!
def index
➋ user_ids = current_user.timeline_user_ids
➌ @posts = Post.where(user_id: user_ids)
.order("created_at DESC")
end
--*snip*--
这段代码在每个动作之前调用了 authenticate_user! ➊。index 动作找到当前用户希望查看的 user_ids ➋,然后查找匹配这些用户的帖子 ➌。你已经在之前的服务器输出中考虑了这些查询。由于 index 动作没有创建三条用户查询,这些查询一定是来自视图。
index 视图呈现帖子集合。这意味着这些查询的源头必须在 app/views/text_posts/_text_post.html.erb 中的 TextPost 部分:
--*snip*-
<div class="panel-body">
➊ <p><em>By <%= text_post.user.name %></em></p>
--*snip*--
这里是问题所在。每篇帖子创建者的名字 ➊ 是通过调用 text_post.user.name 来显示的。如果你检查 ImagePost 部分,你可以验证它也做了同样的事情。每显示一篇帖子,就会生成一条额外的查询,这就解释了你在 SQL 输出中看到的三条额外查询。
N + 1 查询
创建每个记录的额外数据库查询的代码属于 N + 1 查询 问题类别。这些问题在 Rails 应用中很常见,当集合上的关联被引用时,如果没有先加载关联的模型,就会发生这种情况。
在这种情况下,我将一组帖子加载到 @posts 中。然后我引用了每个帖子创建者的 name。因为我没有提前加载这些用户,Rails 会在渲染页面时逐个从数据库中获取它们。这些额外的查询意味着索引页面上的三条帖子导致了四条查询。查询的数量总是比集合中的项数多一个。
幸运的是,这个问题很容易修复。在 Rails 中,你可以提前使用 includes 方法指定所有需要的关联。这个技巧被称为 预加载。
现在让我们更新 PostsController 中的 index 动作,使用预加载:
--*snip*--
def index
user_ids = current_user.timeline_user_ids
➊ @posts = Post**.includes(:user)**.where(user_id: user_ids)
.order("created_at DESC")
end
--*snip*--
在这里,我将includes(:user)方法 ➊ 链接到设置@posts的查询中。传递给includes的符号必须与模型中的关联名称匹配。在这个例子中,post属于:user。
使用includes方法,Rails 确保使用最少的查询加载指定的关联。在保存此文件后,刷新浏览器中的索引页面,并查看终端中的 SQL 输出:
--*snip*--
Post Load (0.3ms) SELECT "posts".* FROM "posts"
WHERE "posts"."user_id" IN (2, 1) ORDER BY created_at DESC
➊ User Load (0.3ms) SELECT "users".* FROM "users"
WHERE "users"."id" IN (2, 1)
--*snip*--
查找每个用户的三个查询已被 ➊ 替换为一个查询,一次性查找所有用户。
在构建应用程序时,要注意额外的查询。检查视图中类似text_post.user.name的调用。注意这个调用中的两个点。两个点意味着你正在访问一个关联模型中的数据,这可能会引入 N + 1 查询问题,因此你应该在视图渲染之前预加载该关联。
分页
你已经减少了加载索引页面帖子所需的数据库查询次数,但想想当你有成千上万条帖子时会发生什么。索引页面会尝试显示所有这些帖子,显著增加应用程序的加载时间。你可以使用分页,即将记录集合分割成多个页面的过程,来缓解这个问题。
will_paginate gem 可以为你处理所有分页。首先,将will_paginate添加到你的应用程序的Gemfile中:
--*snip*--
gem 'bootstrap-sass'
**gem 'will_paginate'**
--*snip*--
记得在更改Gemfile后始终更新已安装的 gem:
$ **bin/bundle install**
接下来,更新app/controllers/posts_controller.rb中的index操作,添加对paginate方法的调用:
--*snip*--
def index
user_ids = current_user.timeline_user_ids
@posts = Post.includes(:user).where(user_id: user_ids)
➊ **.paginate(page: params[:page], per_page: 5)**
.order("created_at DESC")
end
--*snip*--
paginate方法与其他设置实例变量@posts ➊ 的方法链式调用。will_paginate gem 会自动添加params[:page]。我指定了per_page: 5,这样你可以在数据库中只有 6 个帖子时看到分页效果。默认每页 30 条记录。
paginate方法自动将正确的limit和offset调用添加到数据库查询中,从而选择最少的记录。
最后,打开index视图(位于app/views/posts/index.html.erb),并在页面的末尾添加对will_paginate的调用:
--*snip*--
➊ **<%= will_paginate @posts %>**
will_paginate视图助手 ➊ 接受一个记录集合,这里是@posts,并渲染正确的链接以便在该集合的各页之间导航。
为了查看此功能,你需要重新启动你的 Rails 服务器,因为你添加了一个新的 gem。然后创建新的帖子,直到至少有六个,并浏览到用户页面。如果你点击第二页,如图 12-1 所示,你应该能看到新的链接。

图 12-1. 分页链接
will_paginate视图助手添加了上一页和1的链接,点击这些链接可以从第二页返回到第一页。
再次检查服务器输出,查看用于从数据库检索帖子时的查询:
Started GET "/posts?page=2" for 127.0.0.1 at 2014-03-26 11:52:27 -0500
Processing by PostsController#index as HTML
Parameters: {"page"=>"2"}
--*snip*--
➊ Post Load (0.4ms) SELECT "posts".* FROM "posts"
WHERE "posts"."user_id" IN (2, 1)
ORDER BY created_at DESC LIMIT 5 OFFSET 5
--*snip*--
第二页的查询 ➊ 现在如预期包含了 LIMIT 5 OFFSET 5。这个查询只会获取渲染该页面所需的帖子。
缓存
在编程中,缓存是存储频繁使用的数据的过程,以便对相同数据的后续请求能够更快地返回。Rails 将存储数据的地方称为 缓存存储。Rails 应用程序通常使用两种类型的缓存。
低级缓存将耗时计算的结果存储在缓存中——这对那些经常读取但很少变化的值非常有用。片段缓存将视图的一部分存储在缓存中,以加速页面渲染。渲染大量模型集合可能会很耗时。如果数据很少变化,片段缓存可以提高应用程序的页面加载速度。
缓存在开发环境中默认是禁用的,因此在开始学习之前,你需要启用它。在开发过程中保持缓存禁用是一个好主意,因为你总是希望在开发时使用最新版本的数据。例如,如果你将一个值存储在缓存中,然后修改计算该值的代码,那么应用程序可能会返回缓存中的值,而不是由新代码计算出来的值。
在本章节中,你将启用开发环境中的缓存,以便了解它是如何工作的,并学习 Rails 应用程序中使用的缓存类型。打开 config/environments/development.rb 并将 config.action_controller.perform_caching 的值更改为 true:
Social::Application.configure do
--*snip*--
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = **true**
--*snip*--
end
完成本章内容后,将此值更改回 false,以禁用开发环境中的缓存。
Rails 支持几种不同的缓存存储。默认的 ActiveSupport::Cache::FileStore 会将缓存数据存储在文件系统中。一个流行的生产环境缓存选择是 ActiveSupport::Cache::MemCacheStore,它使用 memcached 服务器来存储数据。memcached 服务器是一个高性能的缓存存储,支持在多台计算机之间进行分布式缓存。
现在你已经启用了缓存,让我们为你的应用程序指定一个缓存存储。你无需在电脑上安装 memcached,可以使用 ActiveSupport::Cache::MemoryStore 来演示缓存。这个选项也将缓存对象存储在计算机的内存中,但无需安装额外的软件。在你刚刚修改的行下面,向 config/environments/development.rb 添加这一行:
Social::Application.configure do
--*snip*--
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = true
**config.cache_store = :memory_store**
--*snip*--
end
将缓存存储在内存中比存储在磁盘上更快。内存存储默认分配 32MB 的内存。当缓存数据超过此量时,内存存储会执行清理过程,移除最少使用的对象,因此你无需担心手动从缓存中删除对象。
重启 Rails 服务器以使这些更改生效。
缓存键
缓存中的所有内容都通过缓存键进行引用。缓存键是一个唯一的字符串,用于标识特定的对象或其他数据。
Active Record 模型包含 cache_key 方法,用于自动生成一个缓存键。你可以通过在模型实例上调用 cache_key 在 Rails 控制台中尝试它:
2.1.0 :001 > **post = Post.first**
Post Load (0.2ms) SELECT "posts".* ...
=> #<TextPost id: 1, title: ...>
2.1.0 :002 > **post.cache_key**
➊ => "text_posts/1-20140317221533035072000"
该帖子的缓存键是类名的复数形式,后跟一个斜杠,然后是帖子的 id,再加上一个破折号,最后是 updated_at 日期作为字符串 ➊。
使用 updated_at 日期作为键的一部分解决了缓存失效的问题。当帖子被修改时,updated_at 日期会变化,因此其 cache_key 也会发生变化。这样你就不必担心从缓存中获取过时的数据。
低级缓存
低级缓存在你需要执行一个耗时的计算或数据库操作时非常有用。它常用于可能需要较长时间才能返回的 API 请求。Rails 中的低级缓存使用 Rails.cache.fetch 方法。
fetch 方法接受一个缓存键并尝试从缓存中读取匹配的值。fetch 方法还接受一个块。如果提供了 Ruby 代码块,当值不在缓存中时,该方法会执行块,计算结果并将其写入缓存,然后返回结果。
为了演示低级缓存,我们在索引页面上显示每个帖子的评论数。首先编辑 app/views/text_posts/_text_post.html.erb,并在 text_post.body 下方添加评论计数:
--*snip*--
<p><%= sanitize text_post.body %></p>
**<p><%= pluralize text_post.comments.count, "Comment" %></p>**
--*snip*--
这行新代码使用了 pluralize 辅助方法,根据评论的数量正确地将“Comment”这个词变为复数。例如,如果帖子没有评论,它会显示“0 Comments”。对 app/views/image_posts/_image_post.html.erb 进行类似的修改,将 text_post 替换为 image_post。
现在在浏览器中刷新帖子索引页面,查看服务器输出:
Started GET "/posts" for 127.0.0.1 at 2014-03-26 15:15:05 -0500
Processing by PostsController#index as HTML
--*snip*--
➊ (0.1ms) SELECT COUNT(*) FROM "comments"
WHERE "comments"."post_id" = ? [["post_id", 6]]
(0.1ms) SELECT COUNT(*) FROM "comments"
WHERE "comments"."post_id" = ? [["post_id", 5]]
(0.1ms) SELECT COUNT(*) FROM "comments"
WHERE "comments"."post_id" = ? [["post_id", 4]]
(0.1ms) SELECT COUNT(*) FROM "comments"
WHERE "comments"."post_id" = ? [["post_id", 3]]
(0.1ms) SELECT COUNT(*) FROM "comments"
WHERE "comments"."post_id" = ? [["post_id", 2]]
Rendered collection (5.4ms)
Rendered posts/index.html.erb within layouts/application (10.1ms)
Completed 200 OK in 22ms (Views: 16.8ms | ActiveRecord: 1.5ms)
这个更改添加了五个新的查询 ➊ 用来统计每个帖子的评论数。这些额外的查询占用了宝贵的加载时间,但你可以通过消除它们来提高性能。消除这些查询的一种方法是通过使用 Rails.cache.fetch 缓存你需要的值(在这种情况下是每个帖子的评论数)。
你可以通过向 Post 模型添加一个方法来执行缓存。编辑 app/models/post.rb,并添加 cached_comment_count 方法,如下所示:
class Post < ActiveRecord::Base
--*snip*--
**def cached_comment_count**
➊ **Rails.cache.fetch [self, "comment_count"] do**
**comments.size**
**end**
**end**
end
这个方法将数组 [self, "comment_count"] ➊ 传递给 Rails.cache.fetch 方法。在这里,self 代表当前的帖子。fetch 方法将这些值组合成一个单一的缓存键。块仍然像以前一样调用 comments.size。
现在更新 TextPost 和 ImagePost 视图,使用这个新方法:
--*snip*--
<p><%= pluralize **text_post.cached_comment_count**, "Comment" %></p>
--*snip*--
当你在浏览器中刷新索引页面时,六个评论计数查询将再次执行,并且这些值会被缓存。再次刷新页面,查看服务器输出,注意查询不再被执行。
这个缓存方案有一个小问题。Rails 的 cache_key 方法使用帖子的 id 和 updated_at 日期来创建缓存键,但向帖子添加评论不会改变帖子的 updated_at 日期。你需要的是在添加评论时更新帖子。
Rails 提供了 touch 选项用于关联关系,正是为了这个目的。当你在关联关系上指定 touch: true 时,Rails 会在关联的任何部分发生变化时,自动将父模型的 updated_at 值设置为当前时间。无论是当一个模型被添加或移除,还是当关联的某个模型发生更改时,这种情况都会发生。
打开 app/models/comment.rb 并向 belongs_to 关联中添加 touch: true,如下所示:
class Comment < ActiveRecord::Base
belongs_to :post**, touch: true**
belongs_to :user
validates :user_id, presence: true
end
现在,帖子上的 updated_at 值会在其评论被更新、删除或为其创建新评论时发生变化。如果你向一个帖子添加评论,然后重新加载索引页面,该帖子的评论计数查询将再次执行,并且新的计数会被缓存。
注意
你也可以使用 Rails 的计数器缓存来解决这个问题。通过计数器缓存,Rails 会自动跟踪每个帖子关联的评论数量。通过向 Post 模型添加名为 comments_count 的列,并在 Comment 模型中的 belongs_to :post 声明中添加 counter_cache: true 来启用此功能。
片段缓存
除了低级别的值缓存外,你还可以使用 Rails 的一个功能,叫做 片段缓存,来缓存视图的部分内容。缓存视图通过将渲染后的视图数据存储在缓存中,减少了应用程序的页面加载时间。片段缓存通常是在部分视图中进行的。
为了有效演示片段缓存,我需要一个慢速页面。使用慢速页面可以让片段缓存的影响更加明显。让我们使用 Ruby 的 sleep 方法让帖子渲染得更慢。显然,你永远不会在真实应用中这么做——这只是为了演示。
打开 app/views/text_posts/_text_post.html.erb 部分,在第一行添加 sleep 调用,如下所示:
➊ **<% sleep 1 %>**
<div class="panel panel-default">
--*snip*--
</div>
这个 sleep 调用 ➊ 告诉 Ruby 暂停 1 秒。对 app/views/image_posts/_image_post.html.erb 中的 ImagePost 部分视图做同样的修改。
现在,当你刷新索引页面时,显示的时间应该会更长。检查服务器输出以获取确切的时间:
Started GET "/posts" for 127.0.0.1 at 2014-03-26 16:03:32 -0500
Processing by PostsController#index as HTML
--*snip*--
➊ Rendered collection (5136.5ms)
Rendered posts/index.html.erb within layouts/application (5191.6ms)
Completed 200 OK in 5362ms (Views: 5263.1ms | ActiveRecord: 11.8ms)
渲染这五个帖子花费了超过五秒钟 ➊,考虑到这五个 sleep 调用,这是可以理解的。
现在让我们在部分视图中添加片段缓存。再次编辑 app/views/text_posts/_text_post.html.erb,并添加 cache 方法调用和块,如下所示:
➊ **<% cache text_post do %>**
<% sleep 1 %>
<div class="panel panel-default">
--*snip*--
</div>
**<% end %>**
cache 方法 ➊ 会自动调用 cache_key 来处理 text_post。我还将所有的代码都缩进到了块内。对 ImagePost 部分视图做同样的修改。
现在,当你在浏览器中刷新页面时,你应该能看到来自 Rails 服务器的一些新输出:
Started GET "/posts" for 127.0.0.1 at 2014-03-26 16:18:08 -0500
Processing by PostsController#index as HTML
--*snip*--
➊ Cache digest for text_posts/_text_post.html: 3e...
➋ Read fragment views/text_posts/5-2014... (0.0ms)
➌ Write fragment views/text_posts/5-2014... (0.1ms)
--*snip*--
Rendered collection (5021.2ms)
Rendered posts/index.html.erb within layouts/application (5026.5ms)
Completed 200 OK in 5041ms (Views: 5035.8ms | ActiveRecord: 1.1ms)
渲染索引页面现在会生成关于缓存的几行输出。首先,为部分生成一个摘要 ➊。这个摘要每次渲染该部分时都相同。接下来,Rails 读取缓存 ➋ 来查看这个部分是否已经存在。如果没有找到缓存中的部分,那么该部分会被渲染并写入缓存 ➌。
刷新页面应该会从缓存中读取所有部分,页面渲染速度也会更快。检查服务器输出以确保:
Started GET "/posts" for 127.0.0.1 at 2014-03-26 16:29:13 -0500
Processing by PostsController#index as HTML
--*snip*--
Cache digest for text_posts/_text_post.html: 3e...
➊ Read fragment views/text_posts/22-2014... (0.1ms)
--*snip*--
➋ Rendered collection (25.9ms)
Rendered posts/index.html.erb within layouts/application (31.5ms)
Completed 200 OK in 77ms (Views: 73.1ms | ActiveRecord: 1.0ms)
现在你只看到缓存读取 ➊,并且集合渲染非常快速 ➋,在你添加了 sleep 调用后,渲染所需的时间大大减少。显然,缓存可以显著提高性能。
你现在应该从 TextPost 和 ImagePost 部分中移除 sleep 调用,但保留视图中的缓存。
问题
缓存是加速应用程序的好方法,但也可能引发一些问题。除非代码块或视图片段的缓存键包含用户 ID,否则相同的缓存数据会被发送给每个用户。
例如,TextPost 和 ImagePost 部分都包含检查帖子是否属于当前用户的代码。如果是,它会显示一个链接到 edit 动作的按钮。
<% cache text_post do %>
<div class="panel panel-default">
--*snip*--
➊ <% if text_post.user == current_user %>
<p><%= link_to 'Edit', edit_text_post_path(text_post),
class: "btn btn-default" %></p>
<% end %>
</div>
</div>
<% end %>
TextPost 部分中的条件语句在 app/views/test_posts/_text_post.html.erb 文件中,如果帖子属于 current_user ➊,则显示编辑按钮。帖子的所有者可能是第一个查看该帖子的用户。在所有者查看帖子后,视图片段会被缓存,并且包含了编辑按钮。当其他用户查看同一个帖子时,视图片段会从缓存中读取,其他用户也能看到编辑按钮。
你可以通过几种方式来解决这个问题。你可以在缓存键中包含用户 ID,但那样会为每个用户在缓存中创建一个单独的帖子副本,从而失去缓存对多个用户的好处。一个更简单的解决方案是将按钮移动到被缓存的片段之外,如下所示:
<% cache text_post do %>
<div class="panel panel-default">
--*snip*--
</div>
<% end %>
**<% if text_post.user == current_user %>**
**<p><%= link_to 'Edit', edit_text_post_path(text_post),**
**class: "btn btn-default" %></p>**
**<% end %>**
一旦编辑按钮被移到缓存块之外,条件语句就会为每个查看帖子用户进行评估,只有当当前用户是帖子的所有者时,编辑按钮才会显示。对ImagePost部分做相同的更改,文件位置是 app/views/image_posts/_image_post.html.erb。
记得像本节开始时所示的那样编辑 config/environments/development.rb,并在完成本章最后的练习后,禁用开发环境中的缓存。
总结
没有人喜欢慢速的 web 应用程序!本章介绍了加速应用程序的技巧,从 Rails 内置特性,如资产管道和 Turbolinks,到数据库查询优化、分页和缓存。现在尝试以下练习,让你的应用程序更快。
完成练习后,请将config/environments/development.rb中的config.action_controller.perform_caching更改回false。在开发过程中保持缓存关闭。否则,每次修改缓存视图片段时,您都需要记得清除缓存。
下一章将介绍调试策略,帮助您追踪应用程序中的难以捉摸的问题。您将查看服务器输出和日志,寻找线索,最终深入正在运行的应用程序,看看到底发生了什么。
练习
| 问: | 1. 到目前为止,您的性能优化主要集中在文章的索引页面。打开某个文章的详细页面,例如http://localhost:3000/posts/1。确保该文章有若干评论,然后检查服务器输出。在app/controllers/posts_controller.rb中的PostsController里使用急切加载(eager loading),以减少该页面发出的查询次数。 |
|---|---|
| 问: | 2. 文章显示页面渲染了评论集合。在app/views/comments/_comment.html.erb中的comment部分添加片段缓存。您只希望在@can_moderate为true时显示删除按钮。在这种情况下,通过将数组[comment, @can_moderate]传递给缓存方法,将@can_moderate的值包括在缓存键中。 |
| 问: | 3. 您可以通过将render @post.comments调用包裹在cache块中来缓存整个评论集合。打开app/views/posts/show.html.erb中的显示页面,并添加cache块。将数组[@post, 'comments', @can_moderate]传递给cache方法,确保只有具有管理评论权限的用户才能看到删除按钮,这在练习 2 中已有提到。将缓存集合包装在另一个缓存块中的技术有时被称为俄罗斯套娃缓存,因为多个缓存片段互相嵌套。当一个对象添加到集合中时,只需重新创建外层缓存。其他对象的缓存数据可以重用,只有新对象需要重新渲染。 |
第十三章. 调试
我曾听说,并不是所有的开发者像你我一样完美。我们在代码中从不犯错,但有时其他开发者会犯错,我们需要清理这些错误。当这种情况发生时,Rails 中内建的调试功能就派上用场了。本章将介绍这些内建的调试功能,从 debug 辅助方法开始,它能帮助你更轻松地查看应用视图中的变量值。
在前几章中,我们花了一些时间查看了 Rails 日志。在本章中,你还将学习如何向该日志添加自己的消息。最后,使用调试器 gem,你可以在应用运行时进入应用内部,追踪那些非常棘手的 bug。
调试辅助方法
Rails 包含一个名为 debug 的视图辅助方法,你可以用它来显示 Rails 视图中可用的实例变量或方法调用的值。这个辅助方法会将输出包裹在 <pre> 标签中,使其更容易阅读。
比如,看看随着你在应用中移动,current_user 方法的输出如何变化。首先编辑 app/views/layouts/application.html.erb 文件,并在 yield 方法下方添加对 debug 辅助方法的调用,如下所示:
<!DOCTYPE html>
<html>
--*snip*--
<%= yield %>
**<%= debug current_user %>**
</div>
</body>
</html>
现在启动 Rails 服务器(如果尚未启动),并在浏览器中访问 http://localhost:3000/login。你应该能看到 debug 辅助方法的输出,显示在“登录”按钮下方,如 图 13-1 所示。

图 13-1. 调试 current_user
此时,输出只是三条破折号排成一行,接着是三点省略号排成另一行。debug 辅助方法正在使用 YAML 格式化其输出。YAML 是一种在 Rails 项目中常用的数据序列化语言。例如,Rails 数据库配置文件 (config/database.yml) 就是 YAML 格式的。你在 第十章 中也使用了 YAML 来定义为测试提供默认数据的夹具。
在 YAML 中,三条破折号表示文档的开始。三点省略号表示 YAML 文档的结束。换句话说,这是一个空的 YAML 文档。在登录页面,current_user 为 nil,空的 YAML 文档正是反映了这一点。
现在登录到你的应用程序,滚动到帖子索引页面的底部,看看 current_user 的输出是如何变化的。
➊ --- !ruby/object:User
➋ attributes:
id: 1
name: Alice
email: alice@example.com
created_at: 2014-02-26 ...
updated_at: 2014-02-26 ...
password_digest: "$2a$10$7..."
现在,YAML 输出稍微更完整了一些。第一行以三条破折号开始,接着是 !ruby/object:User ➊,它表示正在显示的对象类型。在这个例子中,对象是一个 Ruby User 类的对象。attributes ➋ 代表对象属性及其值的开始。在它下面,你会看到 User 模型的属性:id、name、email、created_at、updated_at 和 password_digest。
显示这些信息是监控应用程序运行状态的好方法。不幸的是,使用debug助手会限制你只能查看当前会话中的值,如果应用程序在浏览器窗口中没有渲染任何内容,你将无法看到任何值。在这种情况下,你可以依靠 Rails 日志来追踪错误。
Rails 日志记录器
在本书中,我谈到了 Rails 服务器的输出。当 Rails 服务器运行时,它会显示开发日志的副本。即使服务器没有运行,你也可以在编辑器中打开文件log/development.log来查看该日志。
这个文件可能会很大,具体取决于你使用应用程序的频率。你可以使用bin/rake log:clear命令来清除应用程序的日志文件。
日志级别
Rails 日志记录器使用名为:debug、:info、:warn、:error、:fatal和:unknown的级别。这些级别表示日志消息的严重性。级别是由开发者在记录消息时分配的。
如果日志级别等于或高于当前环境配置的日志级别,则该消息会添加到相应的日志文件中。开发和测试环境中的默认日志级别是:debug及以上,而生产环境中的默认日志级别是:info及以上。
因为生产环境中的默认日志级别不显示:debug级别的消息,你可以放心地将这些调试消息留在代码中,而不必担心在应用程序部署并运行时会使日志变得杂乱。
日志记录
每个日志级别都有一个相应的方法用于打印消息。例如,你可以调用logger.debug "Message"来将一个debug级别的消息添加到日志中。
你已经看过如何使用debug助手在视图中显示值。Rails 的日志消息通常用于模型和控制器中。
让我们将current_user的值添加到日志中,并将其与浏览器中显示的内容进行比较。打开你的编辑器中的文件app/controllers/posts_controller.rb,并将这里显示的日志语句添加到PostsController中:
class PostsController < ApplicationController
before_action :authenticate_user!
def index
➊ **logger.debug current_user**
user_ids = current_user.timeline_user_ids
--*snip*--
这一行 ➊每次调用posts的index操作时,都将current_user的输出添加到开发日志中。刷新浏览器中的页面,并检查终端中的日志输出:
Started GET "/" for 127.0.0.1 at 2014-04-05 19:34:03 -0500
Processing by PostsController#index as HTML
User Load (0.1ms) SELECT "users".* FROM "users"
WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
➊ #<User:0x007fd3c94d4e10>
(0.1ms) SELECT "users".id FROM "users" ...
--*snip*--
Rendered posts/index.html.erb within layouts/application (27.1ms)
Completed 200 OK in 61ms (Views: 35.9ms | ActiveRecord: 1.7ms)
logger.debug将current_user方法的值转换为字符串,并将其作为#<User:0x007fd3c94d4e10> ➊添加到日志中。不幸的是,当像current_user这样的 Ruby 对象被转换为字符串时,默认的表示方式是对象的class后跟其object_id。
你要做的是inspect这个对象。inspect方法在 Rails 模型上调用时会显示属性和值。将你刚才添加到PostsController中的current_user调用更改为current_user.inspect,然后再次刷新浏览器中的页面。
Started GET "/" for 127.0.0.1 at 2014-04-05 19:34:27 -0500
Processing by PostsController#index as HTML
User Load (0.1ms) SELECT "users".* FROM "users"
WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
➊ #<User id: 1, name: "User One", ...>
(0.1ms) SELECT "users".id FROM "users" ...
--*snip*--
Rendered posts/index.html.erb within layouts/application (27.1ms)
Completed 200 OK in 63ms (Views: 40.9ms | ActiveRecord: 1.7ms)
这个输出要好得多。current_user 的值显示 ➊,包含所有属性,就像在 Rails 控制台中看到的一样。Rails 日志记录器会显示你发送给它的任何字符串。有时候,我会为日志中的数据加上标签,并添加像星号这样的字符,以便数据更突出:
class PostsController < ApplicationController
before_action :authenticate_user!
def index
**logger.debug "** current_user = "**
logger.debug current_user.inspect
user_ids = current_user.timeline_user_ids
--*snip*--
你之前可能在输出中很难找到 current_user 的值,但现在有了人类可读的标签,找起来就容易多了。
调试器
有时候,单纯查看变量的值并不足以帮助调试问题。Ruby 调试器让你能够在应用程序运行时逐步进入。你可以在调试器中看到代码的执行过程,检查变量的值,甚至修改值。
首先,编辑你的应用程序的Gemfile,添加调试器 gem。对于 Ruby 版本 2.0 或更高版本,应该使用 byebug gem。对于旧版本的 Ruby,则应使用 debugger gem。
--*snip*--
# Use debugger
**gem 'byebug', group: [:development, :test]**
适用于你 Ruby 版本的正确 gem 已在 Gemfile 底部注释掉。去掉行首的 # 并保存文件。生产环境不需要调试器,所以这行代码只会将其添加到开发和测试组。
由于你修改了 Gemfile,记得使用 bin/bundle install 命令来更新已安装的 gems。你还需要重新启动 Rails 服务器:
$ **bin/rails server**
现在你已经安装了调试器,接下来让我们看看它能做什么。
进入调试器
如果你在代码中调用 debugger 方法,当应用程序执行到该调用时,它会暂停并启动调试器。例如,去掉你之前在 app/controllers/posts_controller.rb 中 posts index 动作里添加的日志语句,改用调试器:
class PostsController < ApplicationController
before_action :authenticate_user!
def index
user_ids = current_user.timeline_user_ids
**debugger**
@posts = Post.includes(:user).where(user_id: user_ids)
.paginate(page: params[:page], per_page: 5)
.order("created_at DESC")
end
--*snip*--
当调用 index 动作时,执行会在 debugger 语句处暂停,调试器也会启动。刷新浏览器中的 posts 索引页面。页面应该不会加载完成。检查终端中的服务器输出,你应该能看到调试器提示符:
➊ .../social/app/controllers/posts_controller.rb:9
@posts = Post.includes(:user).where(user_id: user_ids)
[4, 13] in .../social/app/controllers/posts_controller.rb
➋ 4 def index
5 user_ids = current_user.timeline_user_ids
6
7 debugger
8
=> 9 @posts = Post.includes(:user).where(user_id: user_ids)
10 .paginate(page: params[:page], per_page: 5)
11 .order("created_at DESC")
12 end
13
➌ (rdb:2)
在正常的服务器输出中,你应该能看到一行指示当前代码位置的提示 ➊。在这个例子中,执行暂停在 app/controllers/posts_controller.rb 文件的第 9 行。接着,输出 ➋ 会显示你在代码中的位置。你应该会看到以第 9 行为中心的 10 行代码。最后,调试器提示符 ➌ 等待你的输入。
调试器命令
调试器接受多种命令来操作你的应用程序代码。本节涵盖了最常用的命令。除非另有说明,每个命令都可以使用其名称的首字母缩写。
首先输入 help 命令:
(rdb:2) **help**
ruby-debug help v1.6.6
Type 'help <command-name>' for help on a specific command
Available commands:
backtrace break catch condition
continue delete disable display
down edit enable eval
exit finish frame help
info irb jump kill
list method next p
pp ps putl quit
reload restart save set
show skip source start
step thread tmate trace
undisplay up var where
(rdb:2)
help 命令会显示所有可用的调试器命令列表。你也可以在 help 后面跟上其他命令的名称,以获取关于特定命令的信息。
当你进入调试器时,系统会显示当前位置周围的 10 行代码。list 命令会在调试器中显示接下来的 10 行代码。
(rdb:2) **list**
[14, 18] in /Users/tony/code/social/app/controllers/posts_controller.rb
14 def show
15 @post = Post.find(params[:id])
16 @can_moderate = (current_user == @post.user)
17 end
18 end
(rdb:2)
每次你输入list命令时,都会显示另外 10 行代码。在这种情况下,当前文件只剩下五行代码,因此显示这五行。输入list-可以查看前 10 行代码,输入list=则可以显示当前行周围的代码:
(rdb:2) **list=**
[4, 13] in /Users/tony/code/social/app/controllers/posts_controller.rb
4 def index
5 user_ids = current_user.timeline_user_ids
6
7 debugger
8
=> 9 @posts = Post.includes(:user).where(user_id: user_ids)
10 .paginate(page: params[:page], per_page: 5)
11 .order("created_at DESC")
12 end
13
(rdb:2)
现在你知道了自己在代码中的位置,你可能想查看一些变量的值。var命令会显示当前已定义的变量及其内容。要查看局部变量,输入var local命令:
(rdb:2) **var local**
self = #<PostsController:0x007ffbfeb21018>
user_ids = [2, 1]
(rdb:2)
这里仅定义了两个局部变量。变量self表示你当前在PostsController内。变量user_ids在之前的代码的第 5 行接收了它的内容。
使用var instance命令列出实例变量及其值:
(rdb:2) **var instance**
@_action_has_layout = true
@_action_name = "index"
@_config = {}
@_env = {"GATEWAY_INTERFACE"=>"CGI/1.1", "P...
@_headers = {"Content-Type"=>"text/html"}
@_lookup_context = #<ActionView::LookupCont...
@_prefixes = ["posts", "application"]
@_request = #<ActionDispatch::Request:0x007...
@_response = #<ActionDispatch::Response:0x0...
@_response_body = nil
@_routes = nil
@_status = 200
@current_user = #<User id: 1, name: "User O...
@marked_for_same_origin_verification = true
(rdb:2)
到目前为止,已经定义了相当多的实例变量。这段代码唯一设置的实例变量是@current_user。这个实例变量是在ApplicationController的current_user方法中定义的。其他变量是由 Rails 定义的。请注意,@posts尚未定义。你当前的位置在第 9 行,这一行定义了@posts,但这一行代码尚未执行。
display命令将一个变量添加到调试器中的显示列表。如果你特别关心user_ids的值,可以输入display user_ids命令将其添加到显示列表,如下所示:
(rdb:2) **display user_ids**
1: user_ids = [2, 1]
(rdb:2)
你也可以使用display命令(简写为disp)显示显示列表的内容及其值:
(rdb:2) **disp**
1: user_ids = [2, 1]
(rdb:2)
要从显示列表中移除一个变量,使用undisplay命令后跟列表中对应变量的编号。例如,undisplay 1会将user_ids从显示列表中移除。
使用eval命令可以评估你喜欢的任何 Ruby 代码,并打印其值。这个命令的简写是p,就像 print。例如,你可能想打印user_ids数组的长度或current_user方法的输出。
(rdb:2) **eval user_ids.length**
2
(rdb:2) **p current_user**
#<User id: 1, name: "User One", email: "user...
(rdb:2)
调试器是一个 Ruby shell,因此你也可以通过在提示符下直接输入命令来评估 Ruby 代码。甚至不需要使用eval命令。例如,通过在调试器提示符下输入以下语句,将user_ids的值设置为空数组:
(rdb:2) **user_ids = []**
[]
(rdb:2)
这将打印表达式user_ids = []的返回值,就像你在 Rails 控制台中输入它一样。
调试器提供了几个命令,用于在调试过程中执行应用程序的代码。最常用的命令是next,它执行下一行代码。next命令会执行下一行代码中的方法,但不会进入方法内部。
step命令与之类似,但它还会显示每一行在方法调用内部执行的情况。step命令会逐行执行你的应用程序及其依赖项的代码。你可以用它来查找 Rails 框架或应用程序中使用的其他 gem 中的错误。
当你完成了代码中的导航后,使用 continue 命令恢复执行并完成当前请求。如果你跟随本节内容进行操作,你可能会记得你将 user_ids 的值设置为空数组。当你 continue 执行并且帖子索引页面最终渲染时,不会显示任何帖子。因为你将 user_ids 设置为空数组,@posts 实例变量也为空,index 视图中的 render @posts 语句不会渲染任何内容。
Ruby 调试器可能不是你每天都会使用的工具,某些开发者甚至从不使用它。但如果你遇到一个非常难以发现的 bug,调试器将是无价的。
摘要
本章描述了几种调试技术。使用 debug 辅助方法在应用程序视图中显示值,或通过 logger 语句将数据添加到日志文件中,可以帮助你追踪大多数 bug。交互式调试器提供了对应用程序的完全控制,允许你逐步执行代码并定位那些特别难以发现的 bug。
下一章将介绍 Web 应用程序编程接口(API)。我们将讨论如何使用其他应用程序的 API 并创建你自己的 API。
练习
| 问题: | 1. 使用 debug 辅助方法,在帖子索引页面渲染时显示每个帖子的内容。在每种类型的帖子部分内部添加一个 debug 调用。 |
|---|---|
| 问题: | 2. 使用 logger.debug 在 app/controllers/posts_controller.rb 的 index 动作中,将 @posts 实例变量中每个帖子的 id 和 type 添加到日志中。 |
| 问题: | 3. 练习使用调试器探索你应用程序的代码。使用调试器中的 next 命令查看用户登录应用程序时会发生什么。 |
第十四章 Web API
最终,你可能希望将应用程序的范围扩展到网站之外。流行的 Web 应用程序通常还拥有一个本地移动客户端,有时甚至还有桌面客户端。你可能还希望将应用程序中的数据与其他网站和应用程序集成。
一个网页应用程序编程接口(或API)使这一切成为可能。可以将 API 看作是应用程序之间进行通信的语言。在 Web 上,API 通常是使用 JavaScript 对象表示法(JSON)消息的 REST 协议。
在本章中,我们将探讨 GitHub API,了解如何访问有关用户和仓库的详细信息。在讨论 GitHub 的 API 后,你将构建自己的 API。在此过程中,我将讲解 JSON、超文本传输协议(HTTP)和基于令牌的身份验证等细节。
GitHub API
GitHub 代码托管服务拥有一个广泛的 API。它的许多功能甚至无需身份验证即可使用。如果你在本章的示例讲解后,想继续探索 GitHub API,完整的文档可以在线查阅,地址为developer.github.com/。
GitHub API 提供了对有关用户、组织、仓库和其他站点功能数据的便捷访问。例如,访问api.github.com/orgs/rails/,你可以在浏览器中查看 GitHub 上的 Rails 组织:
{
"login": "rails",
➊ "id": 4223,
➋ "url": "https://api.github.com/orgs/rails",
"repos_url": "https://api.github.com/orgs/rails/repos",
"events_url": "https://api.github.com/orgs/rails/events",
"members_url": "https://api.github.com/orgs/rails/me...",
"public_members_url": "https://api.github.com/orgs/r...",
"avatar_url": "https://avatars.githubusercontent.com...",
"name": "Ruby on Rails",
"company": null,
"blog": "http://weblog.rubyonrails.org/",
"location": null,
"email": null,
"public_repos": 73,
"public_gists": 3,
"followers": 2,
"following": 0,
"html_url": "https://github.com/rails",
➌ "created_at": "2008-04-02T01:59:25Z",
➍ "updated_at": "2014-04-13T20:24:49Z",
"type": "Organization"
}
返回的数据对于任何与 Rails 模型打过交道的人来说,应该至少部分是熟悉的。你将看到id ➊、created_at ➌和updated_at ➍等字段,这些字段在你迄今为止创建的所有模型中都有。GitHub API 还包括几个url字段 ➋,你可以使用它们来访问有关组织的更多数据。
例如,访问repos_url(api.github.com/orgs/rails/repos/)查看属于 Rails 组织的源代码仓库列表。从那里,你可以通过访问其url来查看单个仓库的详细信息,例如api.github.com/repos/rails/rails/*。
访问api.github.com/users/username/可以获取有关单个用户的信息。要查看我的 GitHub 账户,请在浏览器中访问api.github.com/users/anthonylewis/。
注意
这些请求返回的数据是 JavaScript 对象表示法(JSON)格式的,它基于 JavaScript 编程语言的一个子集。在 JSON 格式中,大括号之间的数据是一个包含多个命名属性的 JavaScript 对象。每个属性由名称、后跟冒号以及属性值组成。此格式与 Ruby 中的哈希结构非常相似。
除了你目前为止所做的简单数据请求外,GitHub API 还支持使用适当的请求创建和更新对象。当然,这些操作需要身份验证。但在我讲解 API 身份验证之前,我需要先给你多讲一点 HTTP 的内容。
HTTP
HTTP 是 Web 的语言。Web 服务器和浏览器使用该协议进行通信。我已经讨论了一些 HTTP 的方面,例如 HTTP 动词(GET、POST、PATCH 和 DELETE),同时在第四章中讲解了 REST 架构。
除了你目前看到的数据外,HTTP 响应还包含一个包含更多详细信息的头部。你可能熟悉 HTTP 响应头部中的部分数据。任何在 Web 上呆过一段时间的人可能都见过来自 Web 服务器的 404 或 500 响应。像这样的状态码会包含在 Web 服务器的每个响应中。
状态码
每个响应的第一行都会包含一个 HTTP 状态码。这个三位数字代码告诉客户端应该期待什么类型的响应。
状态码根据其首位数字分为五个类别:
-
1xx 信息性
-
2xx 成功
-
3xx 重定向
-
4xx 客户端错误
-
5xx 服务器错误
在处理 API 时,你不应该遇到 1xx 范围的状态码。原始的 HTTP 1.0 规范并没有定义此范围的任何状态码,在我的经验中,这些状态码很少使用。
2xx 范围内的状态码表示请求成功。希望你能遇到许多这样的状态码。常见的代码包括 200 OK,表示成功的响应,通常是针对 GET 请求的;201 Created,当对象在服务器上被创建以响应 POST 请求时返回;以及 204 No Content,表示请求成功,但响应中没有附加数据。
3xx 范围的状态码表示重定向到其他地址。每当你在应用程序中使用 redirect_to 时,Rails 会返回 302 Found 响应。要查看这个过程,登录到你的应用程序并查看重定向日志。
4xx 范围的状态码表示某种客户端错误。换句话说,用户犯了错误。401 Unauthorized 会在请求需要身份验证的 URL 时返回。403 Forbidden 状态码与 401 相似,只是即使客户端成功进行身份验证,服务器也不会完成请求。404 Not Found 会在客户端尝试访问一个不存在的 URL 时发送。当你与 API 打交道时,可能会遇到 406 Not Acceptable 状态码,这表示请求无效,或者遇到 422 Unprocessable Entity 状态码,这表示请求有效,但附带的数据无法处理。
5xx状态码范围表示服务器错误。500 内部服务器错误代码是最常用的。这是一个通用消息,不提供任何额外的数据。503 服务不可用状态码表示服务器的临时问题。
要查看这些代码,您需要检查随响应一起发送的 HTTP 头部。这些通常不会在网页浏览器中显示。幸运的是,有一些工具可以轻松检查 HTTP 头部。其中最受欢迎的工具之一是命令行程序 Curl。
Curl
Curl 是一个免费的命令行网络通信工具。Curl 包含在 Mac OS X 和 Linux 中,Windows 用户可以从curl.haxx.se/下载该工具。Curl 使用 URL 语法,因此是测试 Web API 的理想工具。
打开终端窗口并尝试一些curl命令。我们从刚才查看过的 GitHub API 开始。
$ **curl https://api.github.com/users/anthonylewis**
{
"login": "anthonylewis",
"id": 301,
*--snip--*
}
这个示例展示了如何从 GitHub 获取特定用户账户的信息。默认情况下,Curl 只显示响应数据;输入curl -i以将 HTTP 头部包含在响应中:
$ **curl -i https://api.github.com/users/anthonylewis**
➊ HTTP/1.1 200 OK
Server: GitHub.com
Date: Thu, 17 Apr 2014 00:36:29 GMT
Content-Type: application/json; charset=utf-8
Status: 200 OK
➋ X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
➌ X-RateLimit-Reset: 1397696651
*--snip--*
{
"login": "anthonylewis",
"id": 301,
*--snip--*
}
响应头部以200 OK状态码开始➊。还要注意,GitHub API 请求是有速率限制的。X-RateLimit-Limit: 60这一行➋表示您在一定时间内最多可以发送 60 次请求。下一行显示您还剩余 58 次请求。您的速率限制将在X-RateLimit-Reset: 1397696651这一行➌指定的时间自动重置。
注意
数字1397696651是一个 Unix 时间戳。您可以通过在 IRB 会话或 Rails 控制台中输入Time.at 1397696651来将其转换为正常时间。
身份验证
到目前为止,您只从 GitHub API 读取了公共数据。您还可以使用 GitHub API 读取有关用户和仓库的私人数据,以及创建或更新信息,但这些操作需要身份验证。
我在第九章中讲解了用户身份验证。用户期望一次登录后就能在一段时间内浏览网站。您通过会话保持用户的登录状态,会话存储在一个浏览器会自动随每个请求一起发送的 cookie 中。
API 请求不会保持会话状态。访问 API 的应用程序需要在每次请求中提供身份验证凭据。一个常见的 API 请求身份验证方法是基于令牌的身份验证。在基于令牌的身份验证中,用户在每次请求中都包含一个唯一的 API 令牌。
您可以使用curl命令测试 GitHub 上的基于令牌的身份验证。首先,您需要在 GitHub 的应用设置页面生成一个个人访问令牌。如果需要,请登录 GitHub,并访问 github.com/settings/applications/。在该页面,点击生成新令牌按钮。接下来,您为该令牌提供一个描述;类似于 API 测试的名称就可以。最后,确认“repo”和“user”旁边的复选框已选中,然后点击生成令牌按钮。
GitHub 应该将您带回应用设置页面,并展示一个新的 40 位十六进制令牌。复制您的新令牌并粘贴到文本文件中,以便随时查看。正如屏幕上的信息所说,您将无法再次看到它!
为了验证您的令牌是否有效,请在终端中输入以下curl命令。将token替换为您在所有请求中实际使用的令牌:
$ **curl -H "Authorization: Token *token*" https://api.github.com/user**
{
"login": "anthonylewis",
"id": 301,
*--snip--*
在这里,我使用了-H参数传递给curl,用于向服务器传递自定义头数据,在这种情况下,数据是Authorization: Token头后跟我的令牌。
即使您没有指定用户名,您也应该看到关于您自己账户的信息。GitHub 使用您的个人访问令牌来验证请求。
您现在可以使用该令牌访问私密信息,例如与您的账户关联的 Git 仓库列表。
$ **curl -H "Authorization: Token *token*" https://api.github.com/user/repos**
{
"id": 6289476,
"name": "blog",
"full_name": "anthonylewis/blog",
"owner": {
"login": "anthonylewis",
"id": 301,
*--snip--*
GitHub 应该返回一个包含您账户创建的仓库的数组。根据您创建的仓库数量,这可能会是大量数据。
既然您已经有了令牌,您还可以通过 POST 请求将另一个仓库添加到您的账户中。正如您在[第四章中所学到的,POST 在 REST 中意味着创建。
➊ $ **curl -i -d '{"name":"API Test"}' \**
**-H "Authorization: Token *token*" \**
**https://api.github.com/user/repos**
➋ HTTP/1.1 201 Created
Server: GitHub.com
Date: Mon, 21 Apr 2014 23:47:59 GMT
Content-Type: application/json; charset=utf-8
Status: 201 Created
*--snip---*
➌ {
"id": 18862420,
"name": "API-Test",
"full_name": "anthonylewis/API-Test",
"owner": {
"login": "anthonylewis",
"id": 301,
*--snip--*
-d选项用于curl指定请求中包含的数据。在这里,您发送一个 JSON 字符串,其中包含名称为"API Test"的新仓库 ➊。因为您正在发送数据,curl会自动使用 POST 请求。GitHub 会对请求作出响应,返回指示 HTTP 状态201 Created的头信息 ➋,随后返回关于新创建仓库的信息 ➌。
现在您对现有的 API 有了一些经验,我们来为我们的社交应用创建一个自己的 API。
您的个人 API
您可能还记得在第四章中,Rails 的脚手架生成器在PostsController中使用了respond_to方法,根据请求类型返回不同的数据。这种方法对于某些应用程序是可以的,但当您在应用中加入用户身份验证和会话管理时,就会出现问题。
现有的控制器通过在每个动作之前调用authenticate_user!方法来进行用户身份验证。您的 API 将使用不同的方法来支持基于令牌的身份验证。现有的控制器还根据current_user的值展示数据,如帖子。您的 API 将在请求时展示所有帖子。
与其使用相同的控制器来处理应用程序和 API,你可以为每个构建单独的控制器。由于你的应用程序主要是关于帖子的,所以你可以从构建 API 的帖子控制器开始。
API 路由
首先添加 API 请求的路由。GitHub API 使用子域来处理 API 请求。由于你尚未设置自己的域名,因此你将使用一个单独的路径来处理 API 请求。打开文件config/routes.rb,并在文件末尾添加以下代码块:
Social::Application.routes.draw do
*--snip--*
➊ **namespace :api do**
**resources :posts**
**end**
end
namespace :api块 ➊ 表示为其包含的资源创建的所有路由路径都以api/开头。此外,这些资源的控制器文件应该位于一个名为api的目录中,并且控制器类应该在一个名为Api的模块内。
你可以在终端中输入bin/rake routes命令来查看新创建的路由。
API 控制器
现在你已经定义了路由,接下来需要创建一个控制器来处理这些动作。首先,创建一个目录来存放 API 控制器,方法是输入以下命令:
$ **mkdir app/controllers/api**
然后创建一个名为app/controllers/api/posts_controller.rb的新文件,并添加 API PostsController的代码,如下所示:
**module Api**
**class PostsController < ApplicationController**
➊ **respond_to :json**
➋ **def index**
**@posts = Post.all**
➌ **respond_with @posts**
**end**
**end**
**end**
该文件以module Api开始,表示该类属于 API 命名空间。在PostsController类内部,有一个对respond_to类方法的调用。调用respond_to :json表示该控制器中的动作返回 JSON 数据 ➊。
该类接着定义了index动作 ➋。index动作检索所有帖子,然后使用respond_with方法将其发送到客户端 ➌。respond_with方法会根据请求中使用的格式和 HTTP 动词自动格式化数据。在这种情况下,它应该在响应 GET 请求时返回 JSON 数据,用于index动作。
保存该文件后,如果 Rails 服务器尚未启动,请启动它。然后,你可以使用curl命令通过输入以下命令来测试你的 API:
$ **curl http://localhost:3000/api/posts**
[{"id":1,"title":"First Post","body":"Hello, World!"...
API 将返回一个帖子的数组,以响应帖子index动作。
数据是紧凑的并且在一行中,这可能难以阅读,但有几个免费的工具可以帮你格式化 JSON 数据。例如,jq 是一个 JSON 处理器,可以格式化 JSON 数据并添加语法高亮。你可以从stedolan.github.io/jq/下载 jq。安装后,你可以通过在命令的末尾添加| jq '.'将输出通过 jq 的基本过滤器来格式化:
$ **curl http://localhost:3000/api/posts | jq '.'**
[
{
"id": 1,
"title": "First Post",
"body": "Hello, World!",
"url":null,
"user_id":1,
*--snip--*
本章剩余的示例采用了漂亮打印格式。为了简洁起见,我省略了 | jq '.',但如果你想让输出与书中所见相同,应该包括它。你也可以在浏览器中查看 JSON 输出。在浏览器中输入 http://localhost:3000/api/posts 会引发 ActionController::UnknownFormat 错误。如果你查看终端中的服务器输出,会看到这是一个 406 Not Acceptable 错误,正如本章之前讨论的那样。发生此错误是因为控制器仅响应 JSON 请求,而浏览器默认请求 HTML。
通过在地址栏的 URL 中添加扩展名来指定不同的内容类型。浏览 http://localhost:3000/api/posts.json 会按预期返回一个 JSON 数组。
自定义 JSON 输出
到目前为止,你的 API 返回了与每个帖子相关的所有数据。你可能希望为每条记录包含额外的数据,某些情况下,可能希望排除某些字段的数据。例如,包含每个帖子的作者数据是有帮助的,但你不想包含用户的 password_digest 或 api_token。
你可以通过几种方式自定义内置于 Rails 中的 API 输出。你使用哪种方法取决于你需要多少自定义和个人偏好。
as_json
因为这个 API 返回的是 JSON 数据,所以你可以通过更改 Rails 将模型转换为 JSON 的方式来轻松自定义输出。Rails 首先调用模型上的 as_json 方法将其转换为哈希,然后再将哈希转换为 JSON 字符串。
你可以在 Post 模型中重写 as_json 方法,以自定义每个帖子的返回数据。打开文件 app/models/post.rb 并添加如下所示的 as_json 方法,强制该方法仅显示每个帖子的 id 和 title:
class Post < ActiveRecord::Base
*--snip--*
➊ **def as_json(options={})**
➋ **super(only: [:id, :title])**
**end**
*--snip--*
end
确保包括默认值为 {} 的 options 参数 ➊,因为原始的 as_json 包括了它。你虽然没有使用 options 参数,但因为你正在重写现有方法,所以你的定义必须与原始方法匹配。你的 as_json 方法调用 super,这将调用 Active Record 中定义的原始 as_json 方法,并传递参数 only: [:id, :title] ➋。
使用此方法后,你的 API 应该仅返回每个帖子的 id 和 title。使用 curl 命令验证此更改:
$ **curl http://localhost:3000/api/posts**
[
{"id": 1, "title": "First Post"},
{"id": 2, "title": "Google Search"}
]
as_json 方法支持若干附加选项。你可以使用 :except 选项来排除字段,而不是像 :only 一样指定包含的字段。你还可以使用 :include 选项来包含关联的模型。例如,更新 as_json 方法,如下所示,排除 user_id 字段并包含帖子关联的 user 模型:
def as_json(options={})
**super(except: [:user_id], include: :user)**
end
:methods 选项调用方法列表,并将其返回值包括在输出中。例如,你可以使用此选项调用你在第十二章中添加的 cached_comment_count 方法:
def as_json(options={})
**super(except: [:user_id], include: :user,**
**methods: :cached_comment_count)**
end
这个选项将包括与该帖子关联的评论数(缓存)的信息。
重写as_json当然有效,但根据定制需求的不同,这可能会变得有些杂乱。幸运的是,Rails 提供了一种完全定制 API 返回 JSON 数据的方式。删除Post模型中的as_json方法,让我们来学习 jbuilder。
Jbuilder
Jbuilder 是一个专门用于生成 JSON 输出的领域特定语言。jbuilder gem 默认包含在rails new命令生成的Gemfile中。使用 jbuilder,你可以为每个 API 操作创建视图,就像使用 ERB 为 Web 操作创建视图一样。
和其他视图一样,你需要为 jbuilder 视图创建一个目录。视图目录必须与控制器名称匹配。输入以下命令来为 API 视图创建一个目录,并为PostsController视图创建子目录:
$ **mkdir app/views/api**
$ **mkdir app/views/api/posts**
在这些目录设置好后,你可以创建第一个 jbuilder 视图。创建一个新文件,命名为app/views/api/posts/index.json.jbuilder,并在编辑器中打开它。添加这一行代码并保存文件:
**json.array! @posts**
json.array!方法告诉 jbuilder 将@posts的值呈现为 JSON 数组。使用 Curl 检查索引操作的输出:
$ **curl http://localhost:3000/api/posts**
{
"id": 1,
"title": "First Post",
"body": "Hello, World!",
"url":null,
"user_id":1,
*--snip--*
输出与开始时相同。现在让我们看看如何定制这个输出。
json.array!方法也接受一个块。在块内,你可以访问数组中的每个记录。然后,你可以使用json.extract!方法仅包含帖子中的特定字段:
**json.array! @posts do |post|**
**json.extract! post, :id, :title, :body, :url**
**end**
这个示例将每篇文章的id、title、body和url字段呈现为 JSON 格式。
所有常见的视图辅助方法在 jbuilder 视图中也可以使用。例如,你可以使用api_post_url辅助方法为每篇文章包含一个 URL:
json.array! @posts do |post|
json.extract! post, :id, :title, :body, :url
➊ **json.post_url api_post_url(post)**
end
方法调用的输出,如api_post_url(post) ➊,会自动转换为 JSON 格式。下一个示例添加了每篇文章的作者数据:
json.array! @posts do |post|
json.extract! post, :id, :title, :body, :url
json.post_url api_post_url(post)
**json.user do**
**json.extract! post.user, :id, :name, :email**
**end**
end
在这里,我再次使用了json.extract!方法,只包含每个用户的特定字段。你不希望公开 API 中暴露password_digest字段。
基于令牌的认证
现在,让我们添加认证,以便你也可以通过 API 创建帖子。你将添加基于令牌的认证,就像你之前访问 GitHub API 时使用的那样。
生成令牌
首先,通过生成数据库迁移,为User模型添加一个api_token字符串字段:
$ **bin/rails g migration add_api_token_to_users api_token:string**
记得在生成此迁移后,输入bin/rake db:migrate命令来更新数据库。
现在通过在编辑器中打开app/models/user.rb文件,更新User模型,添加对api_token字段的验证,并添加before_validation回调来生成 API 令牌:
class User < ActiveRecord::Base
*--snip--*
➊ **validates :api_token, presence: true, uniqueness: true**
➋ **before_validation :generate_api_token**
*--snip--*
首先,你需要验证api_token是否存在且唯一 ➊。因为你将使用这个值进行认证,所以两个用户不能拥有相同的api_token。
接下来,使用before_validation回调调用一个方法,如果api_token不存在,则生成它➋。在User模型的底部添加generate_api_token方法,如下所示:
class User < ActiveRecord::Base
*--snip--*
**def generate_api_token**
➊ **return if api_token.present?**
**loop do**
➋ **self.api_token = SecureRandom.hex**
➌ **break unless User.exists? api_token: api_token**
**end**
**end**
end
如果api_token已经有值,generate_api_token方法会立即返回➊。如果api_token没有值,方法会在一个无尽的loop中调用SecureRandom.hex生成一个值➋。SecureRandom类使用计算机上最安全的随机数生成器来生成值。在 Unix 计算机上,它使用/dev/urandom设备;在 Windows 上,它使用 Win32 加密 API。SecureRandom类还包括几种格式化随机值的方法。hex方法返回一个随机的 32 字符十六进制值。最后,如果没有用户拥有该api_token,则跳出循环➌。
现在打开 Rails 控制台并更新现有用户:
➊ irb(main):001:0> **user = User.first**
User Load (0.2ms) SELECT "users".* ...
=> #<User id: 1, ... api_token: nil>
➋ irb(main):002:0> **user.save**
(0.1ms) begin transaction
User Exists (0.2ms) SELECT 1 AS one FROM ...
User Exists (0.1ms) SELECT 1 AS one FROM ...
User Exists (0.1ms) SELECT 1 AS one FROM ...
SQL (1.3ms) UPDATE "users" SET "api_token" ...
(1.7ms) commit transaction
=> true
由于generate_api_token方法是通过before_validation回调自动调用的,您只需要将用户加载到变量中➊,然后将其保存到数据库中➋进行更新。对每个用户执行此操作。如果有任何用户没有api_token值,它将被创建。
现在更新用户的show视图,以便在用户查看自己的账户时显示api_token。按照下面所示更新app/views/users/show.html.erb:
<div class="page-header">
<h1>User</h1>
</div>
<p class="lead"><%= @user.email %></p>
➊ **<% if @user == current_user %>**
**<p class="lead">API Token: <%= @user.api_token %></p>**
**<% end %>**
*--snip--*
由于 API 令牌本质上是密码,您需要通过仅在显示的用户等于current_user时才显示它们,从而保护它们➊。
身份验证请求
现在所有用户都有了 API 令牌,让我们开始使用这些令牌。使用令牌进行身份验证的过程类似于您已经创建的用户名和密码身份验证。因为您的 API 可能有多个控制器,您应该将身份验证方法包含在ApplicationController中,它是所有其他控制器的父类。
首先,您需要一个方法来使用api_token进行身份验证。幸运的是,Rails 提供了一个名为authenticate_or_request_with_http_token的内建方法,可以处理这些细节。打开文件app/controllers/application_controller.rb,并添加以下方法来查看它是如何工作的:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
private
**def authenticate_token!**
**authenticate_or_request_with_http_token do |token, options|**
➊ **@api_user = User.find_by(api_token: token)**
**end**
**end**
*--snip--*
该方法命名为authenticate_token!,与您在[第九章中添加的authenticate_user!方法匹配。authenticate_or_request_with_http_token从请求的 Authorization 头部获取令牌,并将其传递给一个代码块。在代码块中,您尝试使用给定的令牌在数据库中查找用户➊。find_by方法如果找到匹配的用户,则返回一个User对象,否则返回nil。此值将赋给@api_user实例变量,并从代码块中返回。如果代码块返回一个假值,如nil,则方法知道身份验证失败,并向客户端发送401 未授权响应。
你为访问经过身份验证的用户写了一个辅助方法 current_user,该方法出现在 第九章 中。对于 API 请求,经过身份验证的用户已经分配给 @api_user 实例变量,因此你可以使用这个变量。
你的基于令牌的身份验证解决方案已经准备好了。让我们尝试通过 API 添加创建文本帖子的功能。
使用基于令牌的身份验证
首先,你需要为文本帖子添加路由,因此打开 config/routes.rb 并在 :api 命名空间中添加 text_posts 资源:
Social::Application.routes.draw do
*--snip--*
namespace :api do u
resources :posts
**resources :text_posts**
end
end
现在你需要为文本帖子创建一个控制器。记住,它需要位于 api/ 目录中,因为路由位于 :api 命名空间中。创建一个名为 app/controllers/api/text_posts_controller.rb 的文件,并添加以下代码:
**module Api**
**class TextPostsController < ApplicationController**
**respond_to :json**
➊ **before_action :authenticate_token!**
**end**
**end**
这个控制器的起始方式与 API 帖子控制器相同。TextPostsController 类必须位于名为 Api 的模块内。它还包括 respond_to :json。第一个变化是添加了 before_action :authenticate_token! ➊。控制器在每个操作之前都会调用 authenticate_token! 方法。
你想创建文本帖子,因此添加 create 方法:
module Api
class TextPostsController < ApplicationController
respond_to :json
before_action :authenticate_token!
➊ **def create**
**@text_post = @api_user.text_posts.create(text_post_params)**
**respond_with @text_post**
**end**
end
end
create 方法使用在 authenticate_token! 中设置的 @api_user 实例变量来创建一个新的文本帖子 ➊。然后你使用 respond_with 将新的文本帖子发送回客户端。请注意,你没有检查文本帖子是否真正创建。respond_with 方法会自动在 @text_post 包含错误时发送适当的错误响应。
因为你还想指定允许的参数值,所以你的最终添加是一个 text_post_params 方法:
module Api
class TextPostsController < ApplicationController
before_action :authenticate_token!
respond_to :json
def create
@text_post = @api_user.text_posts.build(text_post_params)
respond_with @text_post
end
**private**
➊ **def text_post_params**
**params.require(:text_post).permit(:title, :body)**
**end**
end
end
text_post_params 方法允许在一个嵌套的哈希中使用 :title 和 :body 数据,哈希的键是 :text_post ➊。这与处理 Web 请求时控制器中的 text_post_params 方法相同。
输入 curl 命令以尝试新的 API。运行命令时,确保将 Content-Type 头设置为 application/json,这样 Rails 就会自动解析请求中包含的 JSON 数据。将 token 替换为你应用程序某个用户的实际 api_token。
**$ curl -i \**
**-d '{"text_post":{"title":"Test","body":"Hello"}}' \**
**-H "Content-Type: application/json" \**
**-H "Authorization: Token *token*" \**
**http://localhost:3000/api/text_posts**
1 HTTP/1.1 422 Unprocessable Entity
*--snip--*
出现了问题:状态码 422 Unprocessable Entity ➊ 表示客户端传递给服务器的数据无效。请检查终端中的服务器输出以获取更多信息。
Started POST "/api/text_posts" for 127.0.0.1 at 2014-04-23 19:39:09 -0500
Processing by Api::TextPostsController#create as */*
Parameters: {"text_post"=>{"title"=>"Test", "body"=>"Hello"}}
➊ Can't verify CSRF token authenticity
Completed 422 Unprocessable Entity in 1ms
--*snip*--
传递给服务器的数据有效,但未包含 CSRF 令牌 ➊。请记住,这个令牌与 API 令牌不同。CSRF 令牌是另一个唯一的令牌,当你在应用程序中提交表单数据时,它会自动发送。因为你没有提交表单,所以无法知道正确的 CSRF 令牌。
当你之前更新ApplicationController时,可能注意到了类顶部的一条有用的注释。Rails 通常通过引发异常来防止 CSRF 攻击。这对于 Web 应用程序来说很有用,但对 API 无效。你可以通过清除用户的会话数据来防止 CSRF 攻击,而不是引发异常。现在,每当应用程序收到一个不包含 CSRF 令牌的数据时,它会清除用户的会话,从而有效地将用户从应用程序中登出并防止攻击。
幸运的是,API 客户端在每次请求时都包括正确的 API 令牌,而不是将认证数据存储在会话中。因此,API 请求在空会话下应该可以正常工作。打开app/controllers/application_controller.rb文件并进行以下更新:
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
➊ **protect_from_forgery with: :null_session**
*--snip--*
在protect_from_forgery方法调用 ➊ 中,将:with选项的值更改为:null_session,然后使用curl再次尝试相同的请求:
**$ curl -i \**
**-d '{"text_post":{"title":"Test","body":"Hello"}}' \**
**-H "Content-Type: application/json" \**
**-H "Authorization: Token *token*" \**
**http://localhost:3000/api/text_posts**
➊ HTTP/1.1 201 Created
*--snip--*
➋ {
"id":5,
"title":"Test",
"body":"Hello",
"url":null,
"user_id":1,
"created_at":"2014-04-24T00:33:35.874Z",
"updated_at":"2014-04-24T00:33:35.874Z"
}
状态码现在是201 Created,表示成功 ➊。HTTP 头部后面是新文本帖子的 JSON 表示 ➋。因为你没有为这个动作创建 jbuilder 视图,所以使用了默认的 JSON 表示。
你也可以在浏览器中打开posts索引页面,或者使用命令curl http://localhost:3000/api/posts发出请求来验证文本帖子是否成功创建。
总结
Web API 可以让你的应用程序与客户和第三方应用程序进行协作。通过有效的 API,你还可以为你的应用程序构建本地移动或桌面客户端。你甚至可以使用另一个应用程序的 API 将其数据集成到你的应用程序中。
在本章中,我们讨论了 GitHub API,并使用它访问有关用户和仓库的详细数据。在介绍了超文本传输协议和基于令牌的认证后,你为你的社交网络应用程序构建了自己的 API。
在下一章,你将学习如何设置自己的服务器来托管 Rails 应用程序,并使用 Capistrano 远程服务器自动化工具来部署和维护你的应用程序。
练习
| 问: | 1. 通过发送一个带有假令牌的 POST 请求,验证你的基于令牌的认证是否真的有效。使用curl命令发送请求,并确保检查头部中的状态码和响应体。 |
|---|---|
| 问: | 2. 尝试使用无效数据创建文本帖子,看看会发生什么。你可以在app/models/text_post.rb中检查文本帖子的验证。再次使用curl命令发送请求,并确保检查头部和响应体中的状态码。 |
| 问: | 3. 通过在帖子控制器中添加一个show动作来扩展 API。这个动作应该使用params[:id]查找正确的帖子,然后使用respond_with方法将帖子返回给客户端。因为这是一个 GET 请求,你可以使用curl或在你的浏览器中检查它。 |
第十五章。定制部署
将你的完成的应用程序投入生产并使其可供用户访问,需要做出许多选择。你可以选择各种各样的网络托管服务提供商、Rails 应用程序服务器、数据库和自动化部署系统。在 第六章中,你学习了 Heroku,一种使用 Git 进行部署的托管服务。
大多数大型公司都有一个运营团队来配置服务器并部署应用程序。但作为一名初学者 Rails 程序员,你可能没有专门的运营团队来部署你的应用程序。
在本章中,你将设置一个服务器来托管你的应用程序,配置应用程序的生产环境,将应用程序推送到 GitHub,最后使用 Capistrano 部署到服务器。
虚拟私人服务器
虚拟私人服务器(VPS) 是由网站托管服务提供商销售的一种虚拟机。一个物理服务器可以运行多个虚拟私人服务器。每个 VPS 通常被称为 实例。
当你购买 VPS 时,你获得了一个更大物理服务器的一部分处理能力、内存和磁盘空间。你可以完全访问服务器的这部分,包括选择操作系统的能力。因此,你可以自由安装所需的软件,并按自己喜欢的方式配置服务器。不幸的是,你也需要对服务器上的任何安装和配置错误负责。
许多不同的托管服务提供商提供 VPS 服务。通过快速的 Google 搜索可以找到数百家竞争的提供商。亚马逊 Web 服务(AWS)是创业公司和成熟企业中常见的选择。
注意
本章其余部分将使用 AWS 设置服务器并部署应用程序,但说明并非特定于 AWS。如果你更愿意使用其他服务,可以创建一个运行 Ubuntu Linux 14.04 LTS 的实例,应该可以顺利跟随操作。Ubuntu Linux 14.04 LTS 是一个长期支持版本,保证支持至 2019 年 4 月。
亚马逊 AWS 设置
除了是一个流行的选择外,亚马逊还为新用户提供了 AWS 免费使用层。你可以通过 aws.amazon.com/free/ 阅读更多关于免费使用层的信息,看看自己是否符合条件。即使你不符合免费使用层的条件,你仍然可以以每小时几美分的价格获得一个 AWS 微型实例。
亚马逊将他们的 VPS 服务称为 亚马逊弹性计算云(Amazon EC2)。为了避免在这里详细介绍如何设置亚马逊账户,请参考亚马逊 EC2 文档,链接见 aws.amazon.com/documentation/ec2/。
点击 User Guide 链接,并按照从设置开始的说明进行操作。本节将指导你完成注册 AWS、在 AWS 身份与访问管理(IAM)系统中创建用户帐户、创建密钥对和创建安全组的过程。请务必存储你的 IAM 凭据和私钥 —— 你将在本章中需要它们。
然后继续进行“入门”部分。在本节中,你应该启动一个 EC2 实例,连接到你的实例,添加一个存储卷,最后清理你的实例和卷。EC2 用户指南使用了一个 Amazon Linux 机器镜像,我们不会再次使用,因此在完成本节后,请确保按照用户指南中的清理说明进行操作。
一旦你熟悉了 Amazon EC2,你就可以按照本节所述设置生产服务器。我推荐使用 Ubuntu Linux,因此以下的指令是针对 Ubuntu 的。从 EC2 管理控制台,点击 Launch Instance 按钮以创建一个新的服务器实例,并在“快速启动”部分选择 Ubuntu Server 14.04 LTS (PV) Amazon 机器镜像。因为这是一个 Web 服务器,你需要配置安全组以允许 HTTP 流量。点击 Next 按钮,直到到达步骤 6:配置安全组。现在点击 Add Rule 按钮,选择 HTTP 从类型下拉菜单中,然后点击 Review and Launch 按钮。最后,点击 Launch 按钮。
一旦实例启动,记下在 EC2 管理控制台中显示的公共 DNS 名称,然后通过终端窗口使用 SSH 连接到实例。使用以下命令,将 your_key_file 替换为在 EC2 用户指南的“设置”部分中创建的私钥文件的完整路径,将 your_instance_name 替换为实例的公共 DNS 名称:
$ **ssh -i** your_key_file **ubuntu@**your_instance_name
Welcome to Ubuntu 14.04 LTS...
*--snip--*
Ubuntu AMI 上的默认用户帐户名为 ubuntu。因此,这个命令会连接到你实例上的名为 ubuntu 的用户。
Ubuntu Linux 设置
一旦你连接成功,就可以配置实例以托管 Ruby on Rails 应用程序。在 SSH 连接上执行本节中的所有命令。
Ubuntu 使用一个名为 apt-get 的系统来从在线仓库安装软件。你需要的第一件事是 Ruby。不幸的是,默认的仓库通常包含较旧版本的 Ruby,但你有解决办法。
安装 Ruby
一家名为 Brightbox 的托管公司开发人员创建了自己的 Ubuntu 仓库,提供最新版本的 Ruby,并将其公开提供。这个仓库被称为 个人软件包存档(PPA)。你可以通过这些命令将该仓库添加到你的实例中,并获取最新版本的 Ruby:
$ **sudo apt-get install python-software-properties**
Reading package lists... Done
*--snip--*
Setting up python-software-properties (0.92.36) ...
$ **sudo apt-add-repository ppa:brightbox/ruby-ng**
Next generation Ubuntu packages for Ruby ...
*--snip--*
http://brightbox.com
More info: https://launchpad.net/~brightbox/+archive/ruby-ng
Press [ENTER] to continue or ctrl-c to cancel adding it
当提示时按 ENTER,然后等待 OK 显示出来。在添加 Brightbox 仓库后,更新 apt-get 包列表,以便它能够找到更新版本的 Ruby 包。
$ **sudo apt-get update**
Ign http://us-east-1.ec2.archive.ubuntu.com trusty ...
*--snip--*
Fetched 13.7 MB in 9s (1,471 kB/s)
Reading package lists... Done
现在安装 Ruby 2.1 版本。以下命令将同时安装 Ruby 解释器和编译额外 gem 所需的开发头文件:
$ **sudo apt-get install ruby2.1 ruby2.1-dev**
Reading package lists... Done
--*snip*-
Do you want to continue? [Y/n]
按下 ENTER 继续。安装完成后,检查 Ruby 版本。
$ **ruby -v**
ruby 2.1.1p76 (2014-02-24 revision 45161) [x86_64-linux-gnu]
由于 Ruby 经常更新,你可能会看到比此处显示的版本号更新的版本。现在 Ruby 已安装,你需要一个 web 服务器来支持 Ruby on Rails 应用。
安装 Apache 和 Passenger
目前有多种 web 服务器可供选择。最流行的 web 服务器是 Apache,我们将使用它。使用以下命令安装 Apache HTTP Server 2 版本:
$ **sudo apt-get install apache2**
Reading package lists... Done
*--snip--*
Do you want to continue? [Y/n]
按下 ENTER 继续。
完成后,打开浏览器,访问你实例的公共 DNS 名称,以查看默认的 Ubuntu 网站。虽然此时还看不到你的应用,但你已经在取得进展。
Apache 是用于提供网页的优秀选择,但你需要一个应用服务器来运行你的 Ruby on Rails 应用。与 Apache 集成的一个流行应用服务器是 Phusion Passenger。
Phusion 通过自己的 apt-get 仓库提供 Passenger 应用服务器。与之前使用的 Brightbox 仓库不同,它不是 PPA,因此设置过程会多一些步骤。
首先,输入 apt-key 命令,将 Phusion 的 RSA 密钥导入到 Ubuntu 密钥服务器:
$ **sudo apt-key adv --keyserver keyserver.ubuntu.com \**
**--recv-keys 561F9B9CAC40B2F7**
Executing: gpg --ignore-time-conflict ...
*--snip--*
gpg: imported: 1 (RSA: 1)
apt-get 程序使用这个密钥来确保你安装的软件包确实来自 Phusion。Phusion 的仓库使用加密的 HTTP 连接(HTTPS)与实例进行通信。
首先,你需要将 Phusion Passenger 仓库添加到你的实例中。输入以下命令,在你的实例上用 nano 编辑器打开一个新文件。(或者,如果你更喜欢使用其他命令行编辑器,可以使用其他编辑器。)
$ **sudo nano /etc/apt/sources.list.d/passenger.list**
在第一行输入 deb https://oss-binaries.phusionpassenger.com/apt/passenger trusty main,将 Phusion Passenger 仓库的地址添加到你的实例中。然后,如果你使用的是 nano,按 CTRL-O 然后按 ENTER 保存文件,按 CTRL-X 退出编辑器。
现在再次更新 apt-get 包列表:
$ **sudo apt-get update**
Ign http://us-east-1.ec2.archive.ubuntu.com trusty InRelease
*--snip--*
Reading package lists... Done
然后安装 Apache 2 Phusion Passenger 模块:
$ **sudo apt-get install libapache2-mod-passenger**
Reading package lists... Done
*--snip--*
Do you want to continue? [Y/n]
按下 ENTER 继续。安装完成后,你的实例应该已经配置好,可以提供标准网页和 Ruby on Rails 应用的服务。
安装好 web 服务器后,为你的应用创建一个目录。常规 HTML 网页的默认目录是 /var/www/html。因为你正在部署 Ruby on Rails 应用,所以需要使用以下命令创建一个单独的目录。
$ **sudo mkdir /var/www/social**
$ **sudo chown ubuntu /var/www/social**
$ **sudo chgrp ubuntu /var/www/social**
第一个命令创建一个名为 /var/www/social 的目录。接下来的两个命令将该目录的所有权分配给你的 ubuntu 用户和组,允许你根据需要向该目录写入文件。
现在你需要为你的应用安装并配置一个数据库。
安装 PostgreSQL
本章使用了 PostgreSQL 数据库,但你选择哪款数据库软件主要取决于你。MySQL 是另一个你可以考虑的流行开源选项。
使用以下命令安装 PostgreSQL:
$ **sudo apt-get install postgresql postgresql-contrib**
Reading package lists... Done
--*snip*-
Do you want to continue? [Y/n]
按 ENTER 键继续。现在数据库软件已安装,我们来添加一个用户账户并创建一些数据库。PostgreSQL 的默认用户账户名为postgres,所以你需要使用 sudo -u postgres 命令作为 postgres 用户执行 createuser 命令:
$ **sudo -u postgres createuser --superuser ubuntu**
这个命令创建了一个名为ubuntu的新用户,该用户具有对数据库的超级用户访问权限。该用户可以完全访问所有数据库命令。在 Ubuntu 中,PostgreSQL 配置了一个名为ident sameuser的身份验证系统,默认情况下,如果你的 Ubuntu 用户名与 PostgreSQL 用户名匹配,你可以无需密码直接连接。
既然你已经为自己创建了 PostgreSQL 账户,接下来添加一个数据库,看看能否成功连接:
$ **createdb ubuntu**
$ **psql**
psql (9.3.4)
Type "help" for help.
ubuntu=# help
You are using psql, the command-line interface to PostgreSQL.
Type: \copyright for distribution terms
\h for help with SQL commands
\? for help with psql commands
\g or terminate with semicolon to execute query
\q to quit
ubuntu=#
现在你的账户可以登录 PostgreSQL 并运行命令。输入\q退出。接下来,通过输入以下命令为你的社交应用程序添加一个生产数据库:
$ **createdb social_production**
你在实例上不需要输入其他 PostgreSQL 命令。既然你已经创建了生产数据库,应用程序中的迁移会创建应用程序所需的表。在部署到实例之前,你将配置应用程序以使用这个数据库。
安装构建工具
你的实例几乎准备就绪!不过,在你部署应用程序之前,你需要再安装一些工具。你的应用程序使用的一些 gems 需要被编译,为此你需要像 C 编译器这样的构建工具。你还需要 Git 来从代码仓库中获取代码,并且需要 PostgreSQL 的头文件来编译 PostgreSQL 数据库 gem。
幸运的是,这个单一命令应该会安装你所需的所有构建工具:
$ **sudo apt-get install build-essential git libpq-dev**
Reading package lists... Done
*--snip--*
Do you want to continue? [Y/n]
build-essential 包是一组常见的构建工具,许多不同类型的软件在编译时都需要它们。你已经在第六章中熟悉了 Git。libpq-dev 包是编译 PostgreSQL 客户端应用程序(如 pg gem)所必需的。
安装 Gems
最后一步设置是安装你的应用程序所需的 gems。正如你将在下一节中学习的,bundle 命令在你部署时会自动运行,但在连接到服务器时安装 gems 有助于验证一切是否正常工作。
Gems 在安装时通常会生成文档。在服务器上,这些文档只是占用空间并减慢安装速度。你可以通过在 .gemrc 文件中添加 gem: --no-document 来告诉 gem 命令不要生成文档:
$ **echo "gem: --no-document" >> ~/.gemrc**
既然你已经关闭了 gem 文档生成,现在可以安装 Rails:
$ **sudo gem install rails**
Fetching: thread_safe-0.3.3.gem (100%)
Successfully installed thread_safe-0.3.3
Fetching: minitest-5.3.3.gem (100%)
Successfully installed minitest-5.3.3
*--snip--*
因为你正在使用 PostgreSQL 数据库,所以还需要安装 pg gem。这个 gem 的部分内容是用 C 语言编写的,安装时会自动编译。
$ **sudo gem install pg**
Building native extensions. This could take a while...
Successfully installed pg-0.17.1
1 gem installed
最后,你需要一个叫做 therubyracer 的 gem。这个 gem 将 Google 的 V8 JavaScript 解释器嵌入到 Ruby 中。Rails 使用这个 gem 在服务器上编译资产。这个 gem 的部分内容也需要进行编译。
$ **sudo gem install therubyracer**
Building native extensions. This could take a while...
Successfully installed therubyracer-0.12.1
1 gem installed
配置好这些 gems 后,你的实例就可以运行 Rails 应用程序了。现在 VPS 设置已完成,让我们了解 Capistrano 以及你需要对应用程序进行的更改,以便将其部署并在生产环境中运行。
Capistrano
Capistrano 是一个开源工具,用于自动化通过 SSH 连接在远程服务器上运行脚本和部署应用程序的过程。Capistrano 扩展了你已经使用过的 rake 工具。就像 rake 一样,Capistrano 使用一个简单的 DSL 来定义 任务,这些任务会根据不同的 角色 应用到不同的服务器上。
任务包括从 Git 仓库拉取代码、运行 bundle install 或通过 rake 运行数据库迁移等。角色是不同类型的服务器,如 Web 服务器、应用服务器或数据库服务器。目前这些服务器都在同一台服务器上,但当应用程序变得过大而无法仅依赖一台服务器时,Capistrano 使得将工作分配到多台服务器上变得更加简单。
Capistrano 还支持将应用程序部署到不同的阶段。Capistrano 阶段是服务器的集合,例如预发布服务器和生产服务器。这两台服务器都在生产环境中运行你的 Rails 应用程序,但预发布服务器可能仅用于测试,而生产服务器则是用户可以访问的。
入门
退出 VPS 上的 SSH 会话,或者在你的本地计算机上打开另一个终端窗口来设置 Capistrano。由于 Capistrano 是一个 gem,你首先需要更新应用程序的 Gemfile。Capistrano 已经出现在文件中,但它被注释掉了。删除 capistrano-rails gem 前面的井号,以便安装 Capistrano 和你需要的 Rails 特定任务。
在编辑 Gemfile 时,还需要做出适应生产环境运行的更改:
*--snip--*
# Use sqlite3 as the database for Active Record
1 gem 'sqlite3'**, group: [:development, :test]**
*--snip--*
# See https://github.com/sstephenson/execjs#readme...
2 **gem 'therubyracer', platforms: :ruby, group: :production**
*--snip--*
# Use Capistrano for deployment
3 **gem 'capistrano-rails', group: :development**
4 **# Use PostgreSQL in production**
**gem 'pg', group: :production**
# Use debugger
gem 'byebug', group: [:development, :test]
这些更改首先指定了 SQLite gem 仅在 development 和测试环境中需要 ➊。接下来,therubyracer gem 在生产环境中需要用于编译资产 ➋,如上一节所述。capistrano-rails gem 仅在开发环境中需要 ➌。最后,在生产环境中,你还需要 PostgreSQL gem ➍。
现在更新你计算机上安装的 gems:
$ **bin/bundle install --binstubs --without production**
Fetching gem metadata from https://rubygems.org/........
Fetching additional metadata from https://rubygems.org/..
Resolving dependencies...
--*snip*--
--binstubs 选项告诉 bundler 还要将可执行文件安装到 bin/ 目录中。例如,Capistrano 包含你将用来部署应用程序的 cap 命令,你将从 bin/ 目录运行该命令。--without production 选项告诉 bundler 仅安装开发和测试环境所需的 gems。
接下来,你需要在应用程序中安装 Capistrano:
$ **bin/cap install**
mkdir -p config/deploy
create config/deploy.rb
create config/deploy/staging.rb
create config/deploy/production.rb
mkdir -p lib/capistrano/tasks
Capified
这个过程生成了你配置 Capistrano 部署应用程序所需的文件。接下来我们来详细了解这些内容。
配置
现在您的应用程序已经被 Capified,您可能会注意到一些新文件。第一个文件名为Capfile,位于应用程序的根目录。您需要对该文件进行一个小的修改:
# Load DSL and Setup Up Stages
require 'capistrano/setup'
# Includes default deployment tasks
require 'capistrano/deploy'
➊ **# Include all Rails tasks**
**require 'capistrano/rails'**
*--snip--*
正如注释所述,新的require行将 Capistrano 的 Rails 特定任务包含到您的应用程序中 ➊。保存该文件后,您可以通过在终端中输入bin/cap -T命令来查看 Capistrano 任务列表。
接下来,您需要编辑文件config/deploy.rb。该文件包含所有部署阶段共享的配置,例如您的应用程序名称和 Git 仓库地址。
# config valid only for Capistrano 3.1
lock '3.2.1'
➊ **set :application, 'social'**
**set :repo_url, 'https://github.com/**yourname**/social.git'**
# Default branch is :master
# ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp }.call
# Default deploy_to directory is /var/www/my_app
➋ **set :deploy_to, '/var/www/social'**
*--snip--*
namespace :deploy do
desc 'Restart application'
task :restart do
on roles(:app), in: :sequence, wait: 5 do
# Your restart mechanism here, for example:
➌ **execute :touch, release_path.join('tmp/restart.txt')**
end
end
after :publishing, :restart
*--snip--*
end
首先,将您的应用程序名称设置为social,并指定您的 Git 仓库的 URL ➊。将yourname替换为您的 GitHub 用户名。接下来,将deploy目录设置为您在实例上创建的/var/www/social目录 ➋。最后,在restart任务中取消注释execute行 ➌。此行会执行touch tmp/restart.txt命令。部署后,此命令用于重新启动 Passenger 应用服务器。
现在共享设置已经更新,请编辑config/deploy/production.rb文件。该文件包含 Capistrano production阶段特定的设置。将该文件中的现有代码替换为以下代码:
**server '**your_instance_name**',**
➊ **user: 'ubuntu', roles: %w{web app db}**
➋ **set :ssh_options, {**
**keys: '**your_key_file
**' }**
首先,Capistrano 需要您服务器的地址,以及每台服务器的用户名和角色 ➊。您的实例执行所有三个角色,用户名是ubuntu。将your_instance_name替换为服务器的公共 DNS 名称。接下来,指定连接到实例所需的 SSH 选项 ➋。Capistrano 需要私钥的路径来进行连接。将your_key_file替换为您的私钥文件的完整路径。
数据库设置
接下来,配置您的应用程序以使用您之前创建的 PostgreSQL 数据库。数据库配置位于文件config/database.yml中。更新production部分,如下所示:
*--snip--*
production:
**adapter: postgresql**
**encoding: unicode**
➊ **database: social_production**
**pool: 5**
➋ **username: ubuntu**
➌ **password:**
这段代码告诉 Rails 在production环境中使用名为social_production的 PostgreSQL 数据库 ➊。Rails 将使用用户名ubuntu ➋并且没有密码 ➌,这要归功于之前提到的 Ubuntu 的 ident sameuser 身份验证设置。
密钥设置
您需要设置的最后一件事是用于签署应用程序 cookie 的密钥。该值存储在文件config/secrets.yml中。此文件还可以用于存储其他秘密信息,如应用程序所需的密码或 API 密钥。
*--snip--*
development:
secret_key_base: 242ba1d...
test:
secret_key_base: 92d581d...
➊ # Do not keep production secrets in the repository,
# instead read values from the environment.
production:
➋ secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
如注释中所述,您不应将生产环境的密钥保存在此文件中 ➊。如果您的应用程序代码存储在公共 Git 仓库中,那么这些密钥将变得公开可用。相反,该文件使用 ERB 标签读取SECRET_KEY_BASE环境变量的值 ➋。
在您可以在服务器上设置此环境变量之前,请使用以下命令生成一个值:
$ **bin/rake secret**
a3467dbd655679241a41d44b8245...
复制此命令输出的值,并将其保存在安全的地方。稍后在本章设置应用的虚拟主机时你还会用到它。
添加到 Git
配置好 Capistrano 并配置好数据库后,你就可以为你的应用创建 Git 仓库并将代码推送到 GitHub 了。Capistrano 会在你的实例上运行 git 命令,在部署过程中从 GitHub 拉取你应用的更改。
首先在你的本地计算机上使用以下命令创建 Git 仓库。如果需要复习 Git,可以参考 第六章。
$ **git init**
Initialized empty Git repository in ...
$ **git add .**
$ **git commit -m "Initial commit"**
[master (root-commit) 1928798] Initial commit
123 files changed, 1826 insertions(+)
*--snip--*
现在登录到你的 GitHub 账户,并创建一个名为 social 的新公共仓库。创建仓库后,向你刚创建的本地仓库添加一个远程仓库,并将代码推送到 GitHub。
$ **git remote add origin https://github.com/**yourname**/social.git**
$ **git push -u origin master**
Counting objects: 141, done.
--*snip*--
Branch master set up to track remote branch master from origin.
一旦 Capistrano 配置完成且你的应用已上传至 GitHub,你就可以进行部署了。
部署
首先,测试与实例的连接,并检查实例是否准备好接收来自 Capistrano 的部署。deploy:check 任务确保实例上的所有设置正确:
$ **bin/cap production deploy:check**
INFO [722a06ac] Running /usr/bin/env ...
*--snip--*
INFO [5d3c6d3e] Finished ... exit status 0 (successful).
请注意,我在命令中指定了 production 阶段。每次执行 Capistrano 命令时都必须包括阶段。
如果 deploy:check 任务成功完成,你就可以第一次部署你的应用了:
$ **bin/cap production deploy**
INFO [e6d54911] Running /usr/bin/env ...
*--snip--*
INFO [3cb59e26] Finished ... exit status 0 (successful).
deploy 任务不仅会从 GitHub 检出最新的代码,还会运行 bundle install 更新已安装的 gems,编译应用的资产,并迁移数据库。然而,即使你的应用已经安装并在实例上运行,你仍然需要进行最后一次配置修改,才能在互联网上访问到你的应用。
添加虚拟主机
虚拟主机 是一种在同一服务器或实例上托管多个站点的方式。Apache Web 服务器允许你在同一物理服务器上设置多个不同的站点。它根据每个站点的 DNS 名称,来为每个传入请求提供正确的站点。你当前的实例上只运行了一个站点,但你仍然需要将其设置为虚拟主机。
这一步只需执行一次。除非你决定在同一服务器上添加另一个站点,否则以后无需再次执行此步骤。由于你接下来要指定的目录名称在之前并不存在,所以你需要等到应用部署完成后再进行。
首先,使用 SSH 连接到实例,然后在 /etc/apache2/sites-available 目录中为社交应用创建配置文件:
$ **sudo nano /etc/apache2/sites-available/social.conf**
上述命令会在 nano 编辑器中打开新文件。在新文件中输入以下 Apache 配置代码:
➊ **<VirtualHost *:80>**
➋ **ServerName** ***your_instance_name***
➌ **DocumentRoot /var/www/social/current/public**
➍ **SetEnv SECRET_KEY_BASE** ***a3467dbd65...***
➎ **<Directory /var/www/social/current/public>**
**Allow from all**
**Options -MultiViews**
**</Directory>**
**</VirtualHost>**
第一行表示此虚拟主机响应所有请求(通过星号表示)并监听 80 端口 ➊。接下来,指定此虚拟主机的服务器名称 ➋。将 your_instance_name 替换为你实例的公共 DNS 名称。
然后为这个虚拟主机设置文档根目录 ➌。文档根目录通常是网站的 HTML 文件所在的位置,但在这里,你将其设置为你的应用程序的公共目录。此配置特定于 Passenger 应用程序服务器。
下一行设置了SECRET_KEY_BASE环境变量 ➍。将此处显示的部分密钥替换为你之前输入的bin/rake secret命令生成的完整 128 位密钥。
最后,为文档根目录设置选项 ➎。Allow from all这一行意味着所有主机和 IP 地址都可以访问此目录中的文件。Options -MultiViews这一行关闭了 Apache 中的 MultiViews 功能。该功能使用自动内容协商,可能会导致 Apache 向客户端提供文件,即使文件扩展名未指定,这是你不希望发生的。
按 CTRL-O 然后按 ENTER 保存文件,再按 CTRL-X 退出编辑器。
现在,新的站点已经在 Apache 中配置完成,你需要禁用 Apache 自带的默认站点并启用社交网站:
$ **sudo a2dissite 000-default**
Site 000-default disabled.
To activate the new configuration, you need to run:
service apache2 reload
$ **sudo a2ensite social**
Enabling site social.
To activate the new configuration, you need to run:
service apache2 reload
完成此操作后,重新加载 Apache 以激活更改:
$ **sudo service apache2 reload**
* Reloading web server apache2
*
现在打开你的网络浏览器,访问你实例的公共 DNS 名称。你的应用程序应该可以在互联网上访问,并在你自己的虚拟私人服务器上以生产模式运行。
总结
在本章中,你学习了如何为托管 Rails 应用程序设置 Linux 服务器。你安装并配置了 Apache Web 服务器、Phusion Passenger 应用程序服务器和 PostgreSQL 数据库服务器。
你还学习了如何将远程服务器自动化工具 Capistrano 集成到你的 Rails 应用程序中。你为生产环境配置了 Rails 应用程序,并使用 Capistrano 将其部署到你的实例中。
完成这些后,你已经在成为一名专业的 Rails 开发者的道路上迈出了重要的一步!
练习
| Q: | 1. 对你的应用程序进行一些小改动,例如更新每个页面的标题。将更改提交到本地 Git 仓库,推送更改到 GitHub,然后将更改部署到你的实例。 |
|---|---|
| Q: | 2. 了解其他可以用来轻松为你的 Rails 应用程序添加功能的 gem。例如,你可能希望允许用户将图片上传到你的网站,而不是使用第三方图片托管服务。数百个开源项目可以为你的应用程序添加此类功能。找到一个你喜欢的并试试。如果你发现 bug,修复它并向开发者发送 GitHub 上的 pull request。 |
| Q: | 3. 了解 Ruby on Rails 社区并参与其中。在 GitHub 上关注 Rails 开发。查看官方 Ruby on Rails 网站和博客。了解 Ruby 和 Rails 的会议,并尝试参加;在你当地的 Ruby 或 Rails 用户小组中让自己出名。 |
附录 A. 解决方案
第一章
-
练习 1 是学习如何读取文件,并使用文件内容探索数组方法。我期望在完成练习后,控制台会显示类似以下内容:
irb(main):001:0> **file = File.read("test.txt")** => "Call me Ishmael..." irb(main):002:0> **puts file.split** Call me Ishmael --*snip*-- => nil irb(main):003:0> **puts file.split.length** => 198 irb(main):004:0> **puts file.split.uniq.length** => 140输出取决于你使用的文本。
-
第二个练习需要写一些代码。以下示例仅使用到目前为止介绍的方法来解决问题:
file = File.read("test.txt") counts = {} file.split.each do |word| if counts[word] counts[word] = counts[word] + 1 else counts[word] = 1 end end puts counts这个解决方案应该打印类似以下内容:
=> {"Call"=>1, "me"=>3, "Ishmael."=>1, ...词语 “Call” 在段落中出现了一次;词语 “me” 出现了三次;以此类推。
-
使用练习 3 中提供的示例代码,完整的解决方案如下所示:
class WordCounter def initialize(file_name) @file = File.read(file_name) end def count @file.split.length end def uniq_count @file.split.uniq.length end def frequency counts = {} @file.split.each do |w| if counts[w] counts[w] = counts[w] + 1 else counts[w] = 1 end end end end
这将前两个练习的解决方案结合起来,并将它们封装在一个 Ruby 类中。
第二章
-
第一个练习是熟悉一个简单的 Rails 应用程序以及默认提供的功能。主页的地址是http://localhost:3000/posts。随着你在应用中移动,这个地址会发生变化。新帖子的表单在 /posts/new;第一篇帖子在 /posts/1;编辑第一篇帖子的表单在 /posts/1/edit。这些路径及其含义在第四章中讲解。
-
如果你以前从未在大型应用程序上工作过,那么典型的 Rails 应用程序中的文件数量可能会让你感到畏惧。大多数编辑器都包含某种类型的项目列表用于打开文件,并且提供快捷键来快速通过文件名搜索文件。这些功能在处理较大项目时非常宝贵。
第三章
-
以下命令生成并运行迁移,以向评论添加电子邮件地址:
$ **bin/rails g migration add_email_to_comments email:string** invoke active_record create db/migrate/20140404225418_add_email_to_comments.rb $ **bin/rake db:migrate** == 20140404225418 AddEmailToComments: migrating... --*snip*--然后你可以启动一个 Rails 控制台,使用
bin/rails console并创建一个带有电子邮件地址的新评论。 -
打开app/models/comment.rb并添加如下所示的验证:
class Comment < ActiveRecord::Base belongs_to :post **validates :author, :body, presence: true** end注意,我将两个字段的验证合并在一行中。你也可以通过对
validates方法进行两次调用来完成此操作。 -
你不能写一个查询来确定每个帖子的评论数量,但你可以遍历所有帖子并计算评论数量。在 Rails 控制台输入类似以下内容:
2.1.0 :001 > **Post.all.each do |post|** 2.1.0 :002 * **puts post.comments.count** 2.1.0 :003 > **end**
这段代码首先找到所有的帖子,然后对每个帖子在评论表上执行一个计数查询。
第四章
-
打开文件app/controllers/comments_controller.rb,找到
create方法。class CommentsController < ApplicationController def create @post = Post.find(params[:post_id]) if @post.comments.create(comment_params) ➊ redirect_to @post, notice: 'Comment was successfully created.' else redirect_to @post, alert: 'Error creating comment.' end end --*snip*--注意,目前它使用 @post.comments.create(comment_params) ➊ 来初始化并保存新评论,作为 if 语句的一部分。你需要将新评论存储在一个变量中,这样当保存失败时,你可以使用 errors 方法获取错误列表。根据下面的示例更新 create 方法:
class CommentsController < ApplicationController def create @post = Post.find(params[:post_id]) **@comment = @post.comments.build(comment_params)** if **@comment.save** redirect_to @post, notice: 'Comment was successfully created.' else redirect_to @post, alert: 'Error creating comment. ' **+** **@comment.errors.full_messages.to_sentence** ➊ end end --*snip*--这段代码将错误添加到现有的警告中。注意,我使用了
to_sentence方法 ➊ 将错误消息的数组转换为类似这样的句子:“Author can’t be blank 和 Body can’t be blank”。 -
编辑app/controllers/comments_controller.rb,找到 comment_params 方法。将 :email 添加到对 permit 方法的调用中:
class CommentsController < ApplicationController --*snip*-- private def comment_params params.require(:comment).permit(:author, :body**, :email**) end end
现在,如果用户在添加新评论时输入电子邮件地址,地址应该被存储到数据库中。如果没有这个更改,email字段将被忽略。
第五章
-
从app/views/posts/index.html.erb中删除
h1元素,并更新app/views/layouts/application.html.erb,如这里所示:--*snip*-- <body> **<h1>Listing posts</h1>** <%= yield %> </body> </html>还需要将app/views/posts/new.html.erb和app/views/posts/edit.html.erb中的标题更改为
h2标题:**<h2>New post</h2>** <%= render 'form' %> <%= link_to 'Back', posts_path %> -
首先,在app/views/posts/_form.html.erb部分中添加
:author的标签和文本字段:--*snip*-- <div class="field"> <%= f.label :title %><br> <%= f.text_field :title %> </div> **<div class="field">** **<%= f.label :author %><br>** **<%= f.text_field :author %>** **</div>** <div class="field"> <%= f.label :body %><br> <%= f.text_area :body %> </div> --*snip*--然后,在app/controllers/posts_controller.rb底部的
post_params方法中,将:author添加到允许的参数列表中:--*snip*-- def post_params params.require(:post).permit(:title, **:author,** :body) end end -
按照问题中描述的内容,修改config/routes.rb和app/views/comments/_comment.html.erb。这是我在app/controllers/comments_controller.rb中编写
destroy操作的方法:--*snip*-- **def destroy** **@post = Post.find(params[:post_id])** **@comment = @post.comments.find(params[:id])** **@comment.destroy** **respond_to do |format|** **format.html { redirect_to @post }** **format.json { head :no_content }** **end** **end** --*snip*--
第六章
-
在应用程序中编辑文件后,使用
git add .暂存你的更改,然后使用git commit -m "提交信息"提交这些更改,最后使用git push heroku master将更改推送到 Heroku。 -
如果你还没有 GitHub 账号,访问*
github.com/*并填写注册表单。接下来,你需要选择一个计划。免费计划包括无限的公共仓库。一旦完成注册过程,你应该会看到 GitHub Bootcamp 屏幕。按照屏幕上的说明创建一个仓库并上传你的应用程序。 -
在第二章中创建你新的应用程序,而不是在blog目录内。使用
rails new命令,后面跟上你新应用程序的名称。例如,要创建一个跟踪你的唱片收藏的应用程序,输入以下命令:$ **rails new vinyl**接下来,考虑一下应用程序所需的模型。在这种情况下,你可能需要一个
Record或Album模型。模型需要如title、artist和release_date等字段。进入vinyl目录,并使用rails scaffold命令生成一些代码以开始:$ **cd vinyl** $ **bin/rails generate scaffold Album title artist release_date:datetime**
现在启动 Rails 服务器,并开始使用你的新应用程序。
第七章
-
在我的 Rails 版本中,
Post类有 58 个祖先。irb(main):001:0> Post.ancestors.count => 58使用 Ruby 的漂亮打印方法(
pp),你可以将每个祖先列出在单独的行中:irb(main):012:0> pp Post.ancestors [Post(id: integer, title: string, body: text, created_at: datetime, updated_at: datetime, author: string), Post::GeneratedFeatureMethods, #<Module:0x007fabc21bafd8>, ActiveRecord::Base, --*snip*-- ActiveRecord::Validations, --*snip*-- Kernel, BasicObject]当你浏览祖先列表时,应该会看到一些你熟悉的名字,比如
ActiveRecord::Associations和ActiveRecord::Validations。同时,注意到Post类继承自BasicObject,就像 Ruby 中的其他所有类一样。 -
cannot_feature!方法应该与can_feature!方法相同,唯一的区别是它将false赋值给@features[f],而不是true。class User FEATURES = ['create', 'update', 'delete'] FEATURES.each do |f| define_method "can_#{f}!" do @features[f] = true end **define_method "cannot_#{f}!" do** **@features[f] = false** **end** define_method "can_#{f}?" do !!@features[f] end end def initialize @features = {} end end添加这个方法后,创建另一个
User类的实例,并确保新方法按预期工作:irb(main):001:0> **user = User.new** => #<User:0x007fc01b95abe0 @features={}> irb(main):002:0> **user.can_create!** => true irb(main):003:0> **user.can_create?** => true irb(main):004:0> **user.cannot_create!** => false irb(main):005:0> **user.can_create?** => false -
首先,查看
Element类定义的实例方法:irb(main):001:0> **Element.instance_methods(false)** => [:name, :name=]name和name=方法如预期所定义。现在重新打开Element类并添加对accessor :symbol:的调用。irb(main):002:0> **class Element** irb(main):003:1> **accessor :symbol** irb(main):004:1> **end** => :symbol=这应该创建两个新方法,分别命名为
symbol和symbol=。你可以通过再次调用instance_methods来验证方法是否已创建:irb(main):005:0> **Element.instance_methods(false)** => [:name, :name=, :symbol, :symbol=]
你可以通过创建Element类的实例并使用e.symbol = "Au"来验证方法是否按预期工作。
第八章
-
在
belongs_to关联的一方指定dependent: :destroy会导致父模型在任何子模型被销毁时一并销毁。在这个例子中,销毁任何Post也会销毁关联的User。这个错误比较常见。 -
完整的
Comment模型应该是这样的:class Comment < ActiveRecord::Base belongs_to :post belongs_to :user **validates :post_id, presence: true** **validates :user_id, presence: true** endRails 生成器会自动添加
belongs_to关联,但不会添加验证。 -
使用
bin/rails console启动 Rails 控制台。创建一个新的User、TextPost和Comment。验证所有模型是否已创建。然后对新创建的User调用destroy,并验证关联的TextPost和Comment记录是否也被销毁。irb(main):001:0> **carol = User.create name: "Carol"** => #<User id: 3, name: "Carol", ...> irb(main):002:0> **post = TextPost.create user: carol, body: "Testing"** => #<TextPost id: 3, body: "Testing", ...> irb(main):003:0> **comment = Comment.create post: post, user: carol, \** **body: "Hello"** => #<Comment id: 1, body: "Hello", ...> irb(main):004:0> **carol.posts.count** => 1 irb(main):005:0> **carol.comments.count** => 1 irb(main):006:0> **carol.destroy** ➊ --*snip*-- => #<User id: 3, name: "Carol", ...> irb(main):007:0> **carol.posts.count** => 0 irb(main):008:0> **carol.comments.count** => 0 irb(main):009:0> **carol.reload** ➋ ActiveRecord::RecordNotFound: Couldn't find User with id=3 --*snip*--
注意,调用destroy方法并不会从内存中删除模型➊。即使模型已经从数据库中删除,变量carol仍然引用该模型。尝试从数据库重新加载模型时,会抛出ActiveRecord::RecordNotFound异常,因为 carol 的记录已被删除➋。
第九章
-
首先,编辑位于app/views/text_posts/_text_post.html.erb的文本帖子部分, 如下所示:
<div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title"> <%= text_post.title %> </h3> **<%= link_to(** **"#{time_ago_in_words text_post.created_at} ago",** **post_path(text_post)) %>** </div> --*snip*--这会创建一个指向 text_post 的链接,链接中显示类似“5 天前”之类的时间。按照类似的修改方式编辑位于app/views/image_posts/_image_post.html.erb的图片帖子部分。
--*snip*-- </h3> **<%= link_to "#{time_ago_in_words image_post.created_at} ago",** **post_path(image_post) %>** </div> --*snip*--唯一的区别在于单词 text_post 被替换为 image_post。现在加载帖子索引页面,确保链接正常工作。
-
这个练习最重要的部分是限制控制器访问仅限认证用户。在app/controllers/comments_controller.rb中添加
before_action :authenticate_user!,如下所示:class CommentsController < ApplicationController **before_action :authenticate_user!** --*snip*-- end位于app/views/comments/_comment.html.erb的评论部分展示了创建评论的用户的
name和评论的body。**<p><em><%= comment.user.name %> said:</em></p>** **<p><%= comment.body %></p>**这个部分模板在
post的show视图中通过render @post.comments为每个评论渲染一次。 -
首先,使用
bin/rails console启动 Rails 控制台,查看用户的password_digest。irb(main):001:0> **alice = User.find 1** User Load ... => #<User id: 1, name: "Alice", ...> irb(main):002:0> **alice.password_digest** => "$2a$10$NBjrpHtfLJN14c6kVjG7sety1N4ifyuto7GD5qX7xHdVmbtweL1Ny"
你看到的alice.password_digest的值会有所不同。Bcrypt 在生成哈希摘要之前会自动为密码添加盐。通过查看该值,我无法知道alice的密码。Bcrypt 看起来相当安全!
你可以通过查看浏览器开发者工具或页面信息中的资源来看到网站的 cookie。根据 Chrome 开发者工具,我当前的_social_session cookie 是 465 字节的字母数字字符串,类似于这个"M2xkVmNTaGpVaFd..."。不过,我无法解读这些信息。
第十章
-
打开app/views/text_posts/_text_post.html.erb中的
TextPost部分。它已经显示了用户的name。在text_post.user.name之前添加对link_to帮助方法的调用,并将text_post.user传递给该帮助方法:--*snip*-- <div class="panel-body"> <p><em>By **<%= link_to text_post.user.name, text_post.user %>**</em></p> <%= text_post.body %> </div> --*snip*--然后更新app/views/image_posts/_image_post.html.erb中的
ImagePost部分:--*snip*-- <div class="panel-body"> <p><em>By **<%= link_to image_post.user.name, image_post.user %>**</em></p> <%= image_tag image_post.url, class: "img-responsive" %> <%= image_post.body %> </div> --*snip*--最后,更新app/views/layouts/application.html.erb中的应用程序布局:
--*snip*-- <div class="pull-right"> <% if current_user %> **<%= link_to 'Profile', current_user %>** <%= link_to 'Log Out', logout_path %> <% else %> --*snip*--应用程序布局中已经有对
current_user的检查。将Profile链接放在这个条件语句内。 -
打开
UsersController位于app/controllers/users_controller.rb。在 follow 操作之前要求认证是通过使用你在第九章中编写的authenticate_user!方法进行的一行代码修改。class UsersController < ApplicationController **before_action :authenticate_user!, only: :follow** --*snip*--唯一的
:follow选项意味着匿名用户仍然可以访问show、new和create操作。现在更新app/views/users/show.html.erb中的用户show视图。我使用了两个 if 语句,首先验证current_user不是 nil,然后验证current_user不等于或尚未关注正在显示的用户。--*snip*-- <p class="lead"><%= @user.name %></p> **<% if current_user %>** **<% if current_user != @user && !current_user.following?(@user) %>** <%= link_to "Follow", follow_user_path(@user), class: "btn btn-default" %> **<% end %>** **<% end %>** <h3>Posts</h3> --*snip*--你也可以通过结合所有三个条件语句来使用单个 if 语句完成此操作。
-
首先,打开app/controllers/image_posts_controller.rb,并为新建和创建操作以及私有的 image_post_params 方法添加方法。这些方法类似于 TextPostsController 中的相应方法。
class ImagePostsController < ApplicationController **def new** **@image_post = ImagePost.new** **end** **def create** **@image_post = current_user.image_posts.build(image_post_params)** **if @image_post.save** **redirect_to post_path(@image_post),** **notice: "Post created!"** **else** **render :new, alert: "Error creating post."** **end** **end** **private** **def image_post_params** **params.require(:image_post).permit(:title, :url, :body)** **end** end接下来,在app/views/image_posts/new.html.erb中添加新的视图:
<div class="page-header"> <h1>New Image Post</h1> </div> <%= render 'form' %>然后在app/views/image_posts/_form.html.erb中添加表单部分:
<%= form_for @image_post do |f| %> <div class="form-group"> <%= f.label :title %> <%= f.text_field :title, class: "form-control" %> </div> <div class="form-group"> <%= f.label :url %> <%= f.text_field :url, class: "form-control" %> </div> <div class="form-group"> <%= f.label :body %> <%= f.text_area :body, class: "form-control" %> </div> <%= f.submit class: "btn btn-primary" %> <%= link_to 'Cancel', :back, class: "btn btn-default" %> <% end %>最后,在app/views/posts/index.html.erb的主页上添加一个按钮,链接到新的图片帖子表单:
--*snip*-- <p> <%= link_to "New Text Post", new_text_post_path, class: "btn btn-default" %> **<%= link_to "New Image Post", new_image_post_path,** **class: "btn btn-default" %>** </p> --*snip*--
如果你对这些操作或视图有任何问题,请回顾创建帖子。
第十一章
-
首先,在app/controllers/image_posts_controller.rb中为
edit和update操作添加方法,如下所示:--*snip*-- **def edit** **@image_post = current_user.image_posts.find(params[:id])** **end** **def update** **@image_post = current_user.image_posts.find(params[:id])** **if @image_post.update(image_post_params)** **redirect_to post_path(@image_post), notice: "Post updated!"** **else** **render :edit, alert: "Error updating post."** **end** **end** private def image_post_params params.require(:image_post).permit(:title, :body, :url) end end接下来,在app/views/image_posts/edit.html.erb中创建
edit视图:<div class="page-header"> <h1>Edit Image Post</h1> </div> <%= render 'form' %>这个视图使用你在第十章中创建的表单部分。最后,在app/views/image_posts/_image_post.html.erb中的
ImagePost部分添加指向edit操作的链接:--*snip*-- <%= image_post.body %> **<% if image_post.user == current_user %>** **<p>** **<%= link_to 'Edit', edit_image_post_path(image_post),** **class: "btn btn-default" %>** **</p>** **<% end %>** </div> </div>这个链接被包装在一个条件语句中,只有当该图片帖子是当前用户创建时才会显示。
-
更新app/controllers/posts_controller.rb中的
PostsController,如问题所示。--*snip*-- def show @post = Post.find(params[:id]) **@can_moderate = (current_user == @post.user)** end end现在编辑app/views/comments/_comment.html.erb中的评论部分,并在
@can_moderate实例变量为true时添加删除评论的链接:<p><em><%= comment.user.name %> said:</em></p> <p><%= comment.body %></p> <% if @can_moderate %> **<p>** **<%= link_to 'Destroy', comment_path(comment),** **method: :delete, class: "btn btn-default" %>** **</p>** **<% end %>**确保在链接中添加
method: :delete,以便调用destroy操作。最后,在app/controllers/comments_controller.rb中添加destroy操作:--*snip*-- **def destroy** **@comment = Comment.find(params[:id])** **if @comment.destroy** **redirect_to post_path(@comment.post_id),** **notice: 'Comment successfully destroyed.'** **else** **redirect_to post_path(@comment.post_id),** **alert: 'Error destroying comment.'** **end** **end** private def comment_params params.require(:comment).permit(:body, :post_id) end end这个方法查找评论,调用
destroy,并重定向回帖子,显示成功或失败的消息。 -
打开config/routes.rb中的路由文件,并编辑
logout路由:--*snip*-- get 'login', to: 'sessions#new', as: 'login' **delete** 'logout', to: 'sessions#destroy', as: 'logout' root 'posts#index' end编辑位于app/views/layouts/application.html.erb的应用布局,并向Log Out链接添加
method: :delete。--*snip*-- <div class="pull-right"> <% if current_user %> <%= link_to 'Profile', current_user %> <%= link_to 'Log Out', logout_path**, method: :delete** %> <% else %> --*snip*--
现在该链接会发出 DELETE 请求以注销应用。
第十二章
-
显示页面加载评论集合以进行渲染,然后在渲染评论时逐个加载每个评论的所有者。您可以通过在
PostsController中的show方法里添加includes(comments: [:user])来预加载一个帖子的评论和所有者,位置在app/controllers/posts_controller.rb:--*snip*-- def show @post = Post**.includes(comments: [:user])**.find(params[:id]) ➊ @can_moderate = (current_user == @post.user) end end添加
includes(comments: [:user])会告诉 Rails 预加载该帖子的所有评论及其关联的所有用户。 -
打开位于app/views/comments/_comment.html.erb的
Comment部分,并添加缓存块:<% cache [comment, @can_moderate] do %> ➊ <p><em><%= comment.user.name %> said:</em></p> <p><%= comment.body %></p> <% if @can_moderate %> <p> <%= link_to 'Destroy', comment_path(comment), method: :delete, class: "btn btn-default" %> </p> <% end %> **<% end %>**将一个数组传递给
cache方法会创建一个缓存键,该键结合了数组中的元素➊。在这种情况下,缓存键包含了评论的id和updated_at字段的值,以及@can_moderate的值,可能为 true 或 false。 -
打开位于app/views/posts/show.html.erb的显示页面,并添加
cache块。--*snip*-- <h3>Comments</h3> **<% cache [@post, 'comments', @can_moderate] do %>** ➊ <%= render @post.comments %> **<% end %>** *--snip--*这会创建一个缓存键,它是
@post的缓存键、单词“comments”和@can_moderate的值的组合➊。现在,评论集合在从缓存中读取一次后就会显示出来。
第十三章
-
您需要更新此练习中两种类型帖子的视图部分。首先,编辑文件app/views/text_posts/_text_post.html.erb并在底部附近添加一个
debug调用,如下所示:<div class="panel panel-default"> --*snip*-- <%= debug text_post %> </div> </div>然后编辑app/views/link_posts/_link_post.html.erb并在底部附近添加一个
debug调用:<div class="panel panel-default"> --*snip*-- <%= debug link_post %> </div> </div> -
将每个帖子的 id 和类型添加到日志的最简单方法是遍历
@posts实例变量的内容。编辑app/controllers/posts_controller.rb并更新index动作。class PostsController < ApplicationController before_action :authenticate_user! def index user_ids = current_user.timeline_user_ids @posts = Post.includes(:user).where(user_id: user_ids) .paginate(page: params[:page], per_page: 5) .order("created_at DESC") **@posts.each do |post|** **logger.debug "Post #{post.id} is a #{post.type}"** **end** end --*snip*--现在当您刷新帖子索引页面时,应该能在日志中看到类似于“Post 5 is a TextPost”的五行记录。
-
为了调试用户登录应用时发生的情况,您需要在app/controllers/sessions_controller.rb中的 create 动作里添加一个
debugger调用:class SessionsController < ApplicationController --*snip*-- def create **debugger** user = User.find_by(email: params[:email]) if user && user.authenticate(params[:password]) session[:user_id] = user.id redirect_to root_url, :notice => "Logged in!" else flash.now.alert = "Invalid email or password" render "new" end end --*snip*--
添加这行代码后,您可以检查发送到此动作的params,当前session的内容,以及在此动作中执行时user的值。
第十四章
-
这个
curl命令与您之前用来创建新帖子的命令相同,只是我将token替换成了fake。$ **curl -i \** **-d '{"text_post":{"title":"Test","body":"Hello"}}' \** **-H "Content-Type: application/json" \** **-H "Authorization: Token fake" \** **http://localhost:3000/api/text_posts** HTTP/1.1 401 Unauthorized --*snip*-- HTTP Token: Access denied.请注意,状态码是401 Unauthorized,且响应体包含文本
"HTTP Token: Access denied." -
文本帖子验证正文是否存在,因此使用
curl尝试创建一个没有指定正文的文本帖子。$ **curl -i \** **-d '{"text_post":{"title":"Test"}}' \** **-H "Content-Type: application/json" \** **-H "Authorization: Token *token"* \** **http://localhost:3000/api/text_posts** HTTP/1.1 422 Unprocessable Entity --*snip*-- {"errors":{"body":["can't be blank"]}}请注意,状态码是422 Unprocessable Entity,且响应体包含错误的 JSON 表示。
-
向app/controllers/api/posts_controller.rb添加
show方法:module Api class PostsController < ApplicationController respond_to :json --*snip*-- **def show** **@post = Post.find(params[:id])** **respond_with @post** **end** end end该方法查找请求的帖子,并将其分配给
@post实例变量,然后返回该帖子。以下curl命令验证此动作是否有效:$ **curl http://localhost:3000/api/posts/1** { "id":1, "title":"First Post", "body":"Hello, World!", "url":null, "user_id":1, "created_at":"2014-04-22T00:56:48.188Z", "updated_at":"2014-04-22T00:56:48.188Z" }
因为你没有为此操作创建 jbuilder 视图,所以返回的是帖子默认的 JSON 表示形式。
第十五章
-
编辑文件 app/views/layouts/application.html.erb 以更改每个页面的标题:
<!DOCTYPE html> <html> <head> <title>**My Awesome Site**</title> --*snip*--在保存此更改后,将其添加到本地 Git 仓库的暂存区,然后使用合适的
commit消息提交更改。$ **git add .** $ **git commit -m "Update title"**现在,通过在终端中输入
bin/cap production deploy来部署你的更改。 -
Ruby 工具箱在
www.ruby-toolbox.com/上列出了数百个宝石,你可以用来为你的应用添加功能。例如,你可以让用户向你的应用上传文件。查看 Rails 文件上传类别,找到多个选项,包括 Paperclip 和 CarrierWave。在这里,你可以访问网站,阅读文档,并查看每个项目的源代码。 -
访问
github.com/rails/rails/参与讨论开放问题和拉取请求,并查看以前的提交记录。Ruby on Rails 也有一个页面rubyonrails.org/community/,供那些希望在线参与的人。你可以在rubyconf.org/和railsconf.com,/分别了解即将举行的 Ruby 和 Rails 大会。希望在那里见到你!
第十六章:索引
关于数字索引的说明
索引条目的链接将显示该条目所在章节的标题。由于某些章节有多个索引标记,索引条目可能会有多个链接指向同一章节。点击任何链接都将直接跳转到文本中标记出现的位置。
符号
404.html 文件,配置目录
422.html 文件,配置目录
500.html 文件,配置目录
&&(与)运算符,哈希
@,用于实例变量,方法
\(反斜杠),用于特殊字符,字符串
:(冒号),用于符号,字符串
{ }(大括号),数组,迭代
用于块,迭代
用于哈希,数组
::(双冒号)运算符,模块
"(双引号),用于字符串,字符串
==(等于)运算符,哈希
!(感叹号),位于方法名末尾,字符串
=>(哈希火箭),数组
(大于)运算符,哈希
<(小于)运算符,哈希,类
以及继承,类
<< 运算符,数组
<=> 运算符,包含
<%= %> 标签,嵌入 Ruby
<% %> 标签,嵌入 Ruby
<%# %> 标签,用于注释,控制流
%(取余)运算符,数据类型
!=(不等于)运算符,哈希
||(或)运算符,哈希
||=(条件赋值)运算符,布尔值
- 运算符,用于添加数组,数组
?(问号),位于方法名末尾,字符串
[ ](方括号),用于数组,字符串
_(下划线),在部分名称中,CSRF Meta Tags Helper
|(竖线),迭代
A
访问器类的定义,class_eval
Active Record,帖子模型,读取,删除,查询条件,高级 Active Record,缓存
cache_key 方法,缓存
计算,查询条件
查询条件,删除
关联,读取
ActiveRecord::Base,继承自,has_many :through
ActiveRecord::RecordNotFound 异常,读取,has_many 方法
ActiveSupport::Cache::FileStore 缓存存储,缓存
ActiveSupport::Cache::MemoryStore 缓存存储, 缓存
ActiveSupport::TestCase 模块, 运行测试
Ada, Ruby 基础
添加命令(Git), 入门
alert 类, 注册
alert-danger 类, 注册
提示消息, 注册
alert-success 类, 注册
all 方法, 读取
亚马逊弹性计算云 (Amazon EC2), 虚拟私有服务器
亚马逊网络服务 (AWS), 虚拟私有服务器
ancestors 类方法, extend
和 (&&) 操作符, 哈希
匿名用户,限制页面访问, 身份验证系统, 当前用户
Apache, 安装 Ruby, 添加虚拟主机
禁用默认站点, 添加虚拟主机
安装, 安装 Ruby
API 控制器, API 控制器
api 模块, 验证请求
API 请求, Curl, 身份验证
添加路由, 身份验证
身份验证凭证, Curl
API(应用程序接口), 渲染或重定向, 身份验证, Jbuilder
创建, 身份验证
基于令牌的身份验证, Jbuilder
api_token 字符串, Jbuilder
app/assets 目录, 布局, 布局, stylesheet_link_tag, 身份验证, 身份验证, 内建优化功能, 内建优化功能, 内建优化功能, 清单
application.css 文件, 布局, 身份验证, 内建优化功能
application.js 文件, stylesheet_link_tag, 身份验证
javascripts 目录, 内建优化功能
stylesheets 目录, 内建优化功能
app/controllers 目录,路径和 URL,身份验证系统,注册,实现,显示用户,创建帖子,创建帖子,授权攻击,授权攻击,防止 CSRF,分页,日志级别,API 控制器,生成令牌,验证请求,第五章,第九章,第十章,第十章,第十章,第十一章,第十一章,第十三章,第十三章,第十三章
api/posts_controller.rb 文件,API 控制器,第十三章
api/text_posts_controller.rb 文件,验证请求
application_controller.rb 文件,实现,防止 CSRF,生成令牌
comments_controller.rb 文件,第五章,第九章,第十一章
destroy 操作,第五章
image_post_controller.rb 文件,授权攻击,第十章,第十章
ImagePostsController 方法,第十章
posts_controller.rb 文件,路径和 URL,身份验证系统,分页,日志级别,第十一章,第十三章
索引操作,分页
logger 语句,日志级别
PostsController,第十一章
sessions_controller.rb 文件,第十三章
text_posts_controller.rb 文件,创建帖子,创建帖子,授权攻击
users_controller.rb 文件,注册,显示用户,第十章
app/helpers 目录,数字助手,数字助手,数字助手
application_helper.rb 文件,数字助手
posts_helper.rb 模块,数字助手
应用程序视图,不要重复自己
ApplicationController 类,当前用户,生成令牌
身份验证方法,生成令牌
ApplicationHelper 模块,数字助手,模块
应用程序编程接口,渲染或重定向(参见 API(应用程序编程接口))
application.rb 文件,配置目录
应用程序服务器,适用于 Ruby on Rails,安装 Ruby
app/models 目录,模型,用户模型,用户模型,帖子模型,帖子模型,帖子模型,注册,运行测试,低级缓存,第二章
comment.rb 文件,低级缓存,第二章
添加验证,第二章
ext_post.rb 文件,检查正文,帖子模型
image_post.rb 文件,帖子模型
post.rb 文件,模型
subscription.rb 文件,用户模型
user.rb 文件,用户模型,帖子模型,注册,运行测试
has_many 关联,帖子模型
app/views 目录,内嵌 Ruby,内嵌 Ruby,内嵌 Ruby,帖子索引页面,布局,集合,认证系统,帖子索引与显示,帖子索引与显示,帖子索引与显示,注册,会话,当前用户,显示用户,显示用户,创建帖子,创建帖子,授权攻击,授权攻击,授权攻击,防止 CSRF,分页,缓存,低级缓存,问题,调试助手,第四章,第九章,第九章,第九章,第九章,第九章,第十章,第十章,第十章,第十一章,第十二章,第十二章,第十三章,第十五章
comments/_comment.html.erb 文件,第九章,第十一章,第十二章
image_posts/edit.html.erb 文件, 授权攻击
image_posts/_form.html.erb 文件, 第十章
image_posts/_image_post.html.erb 文件, 帖子索引与展示, 低级缓存, 问题, 第九章, 第九章
ImagePost 部件, 第九章
image_posts/new.html.erb 文件, 第十章
layouts/application.html.erb 文件, 布局, 当前用户, 防止 CSRF, 调试助手, 第四章, 第十章, 第十五章
更新, 第四章
link_posts/_link_post.html.erb 文件, 第十三章
posts/index.html.erb 文件, 嵌入式 Ruby, 帖子索引页面, 身份验证系统, 创建帖子, 分页
will_paginate 调用, 分页
posts/show.html.erb 文件, 嵌入式 Ruby, 集合, 帖子索引与展示
sessions/new.html.erb 文件,用于登录表单, 会话
text_posts/edit.html.erb 文件, 授权攻击
text_posts/_form.html.erb 文件, 创建帖子
text_posts/_text_post.html.erb 文件, 帖子索引与展示, 授权攻击, 缓存, 第九章, 第九章, 第十二章
评论计数, 缓存
TextPost 部件, 第九章
users/new.html.erb 文件, 注册
users/show.html.erb 文件, 展示用户, 展示用户
apt-get 系统,用于软件安装, Amazon AWS 设置
数组, 字符串, 哈希, 读取
对哈希中的所有键, 哈希
返回首个和最后一个条目, 读取
ASC 顺序,用于获取帖子, 删除
as_json 方法, API 控制器
断言, 运行测试, 帖子固定数据, 控制器测试
控制器测试, 控制器测试
在测试中, 运行测试
资产管道, 布局, 内置优化功能, 内置优化功能, 清单, 清单, 调试模式
资产预编译, 调试模式
调试模式,清单
清单,内建优化特性
查看搜索路径列表,清单
在 Rails 应用中的资产,布局
assets:clobber rake 任务,调试模式
资产目录,控制器,配置目录
assigns 哈希,控制器测试
关联,测试数据,测试数据,添加关联,添加关联,has_many 方法,高级 Active Record,自连接关联,has_and_belongs_to_many,单表继承,用户模型,用户模型,N + 1 查询,低级缓存
添加,添加关联
belongs_to 方法,has_many 方法
定义,用户模型
生成模型,测试数据
has_many 方法,添加关联
has_many :through,has_and_belongs_to_many
多对多,自连接关联
多态,单表继承
自连接,高级 Active Record
提前指定,N + 1 查询
测试,用户模型
touch 选项,低级缓存
attr_accessor 方法,类,模块,define_method
对象的属性,显示,日志级别
已认证用户,TextPostsController,用于创建帖子
authenticate_or_request_with_http_token 方法,生成令牌
authenticate_user! 方法,当前用户,第十章
通过 SSH(安全外壳)认证,Heroku
认证,认证,认证系统,注册,当前用户,安全,Curl,Jbuilder,生成令牌
与授权对比,安全
当前用户,认证系统
使用 GitHub API,Curl
登录,注册
请求的,生成令牌
基于令牌,Jbuilder
用户的,当前用户
authenticity_token 令牌,防止 CSRF
授权攻击,安全
author 迁移, 添加列
average 方法, 查询条件
AWS(亚马逊 Web 服务), 虚拟专用服务器
B
反斜杠 (),用于特殊字符, 字符串
BasicObject 类, 祖先
bcrypt gem, 帖子索引和展示, 第九章
BCrypt::Password.create 方法, 运行测试
before_action :authenticate_token! 方法, 认证请求
before_action :authenticate_user! 方法, 第九章
before_action 方法(Rails), 路径和 URL, 当前用户
belongs_to 关联, 高级 Active Record, 自连接关联, 单表继承, 用户模型, 第八章
belongs_to 方法, has_many 方法
belongs_to 语句, 添加关联
between? 方法, include
bin 目录, Rails 基础, 控制器, 视图, 认证, 帖子索引和展示, 注册, Rails 测试, 模型测试, 测试流程, 调试助手, 日志记录, 认证, Jbuilder, 第八章, 第九章, 第十五章
bundle install 命令, 认证, 帖子索引和展示, 日志记录
cap production deploy 命令, 第十五章
rails console 命令, 第八章, 第九章
rails generate 命令, 测试流程
rails server 命令, 视图
rake db:migrate 命令, 注册, Jbuilder
rake log:clear 命令, 调试助手
rake routes 命令, 认证
rake test 命令, Rails 测试
rake test:models 命令, 模型测试
--binstubs 选项, 用于 bundler, 入门
代码块, 迭代
博客帖子,添加评论, 测试数据
body 元素(HTML), 布局
布尔值, 哈希
Bootstrap, 认证, 帖子索引和展示, 注册, 显示用户
form-group 类, 注册
面板组件,帖子索引和显示
链接的样式,显示用户
bootstrap_sass gem,身份验证
branch 命令(Git),其他有用命令
分支,变量
BrightBox,Amazon AWS 设置
build-essential 包,安装 PostgreSQL
构建工具,安装,安装 PostgreSQL
bundle 命令,公共目录,更新你的 Gemfile,安装 PostgreSQL
bundle install 命令,Rails 基础知识,添加到 Git
Bundler 工具,公共目录,更新你的 Gemfile
标志,更新你的 Gemfile
业务逻辑,不要重复自己
byebug gem,日志记录
C
cached_comment_count 方法,低级缓存
cache_key 方法,缓存,低级缓存,片段缓存
缓存键,缓存,第十二章
缓存存储,分页,缓存
Rails 支持,缓存
缓存,分页,缓存,缓存,低级缓存,片段缓存
启用,缓存
片段,低级缓存
问题,片段缓存
低级,缓存
calc 方法,prepend
回调,消除,将断言付诸实践
取消当前操作,交互式 Ruby
cannot_feature! 方法,第六章
Capfile,入门
Capistrano,调试模式,安装 Gems,入门,入门,配置,数据库设置,添加到 Git,添加虚拟主机
配置,入门
数据库设置,配置
部署,添加到 Git
secrets 设置,数据库设置
设置,入门
虚拟主机,添加虚拟主机
capistrano-rails gem,入门
层叠样式表(CSS),布局,身份验证,内置优化功能
资源管道和,内置优化功能
在应用中包含, 认证
应用程序使用列表, 布局
情况, 布尔值, 变量
常量, 布尔值
变量, 变量
cd 命令, Rails 基础
集中式版本控制系统, 版本控制
change_column 方法, 添加列
change 方法, 迁移
更改,推送或拉取, 版本控制
checkout 命令 (Git), 其他有用的命令, 分支
子模型, 高级 Active Record
类, 方法, 类, 类, prepend, extend, 祖先, 祖先
添加新方法, 类
祖先, extend
将方法调用分配给另一个, prepend
创建实例, 类
作为其他类的实例, 祖先
方法, 祖先
class_eval 方法, define_method
类方法, 类
类声明, 方法
客户端错误, 交互式 Ruby, 状态码
云应用平台, 部署 (见 Heroku 云应用平台)
代码, 迭代, Turbolinks 实战, 调试命令
块, 迭代
在调试器内执行, 调试命令
优化, Turbolinks 实战
CoffeeScript, 内置优化功能, Turbolinks 实战
:collection 选项, CSRF Meta 标签助手
集合, CSRF Meta 标签助手
冒号 (😃,用于符号, 字符串
数据库中的列,添加, 添加列
组合字符串, 字符串
注释模型, 评论模型, 第八章
comment_params 方法, 添加评论, 第四章
注释, 测试数据, 添加关联, has_many 方法, 回到控制器操作, 集合, 表单控件, 第十一章
添加, 回到控制器操作
添加到博客文章, 测试数据
belongs_to 方法,has_many 方法
表单,表单控件
has_many 方法,添加关联
显示页面,第十一章
显示,集合
CommentsController#create 方法,表单控件
commit 命令(Git),入门指南
提交信息,入门指南
可比较模块,包含
并发版本控制系统 (CVS),版本控制
条件赋值(||=)操作符,布尔值
条件语句,变量,片段缓存
用于编辑按钮,片段缓存
config.action_controller.perform_caching 设置,缓存
config.assets.debug 设置,调试模式
config 目录,控制器,路由,受限资源,显示用户,防止 CSRF,调试模式,缓存,问题,入门指南,配置,数据库设置,第十一章
deploy/production.rb 文件,配置
deploy.rb 文件,入门指南
environments/development.rb 文件,调试模式,缓存,问题
routes.rb 文件,路由,受限资源,显示用户,防止 CSRF,第十一章
登出路由,第十一章
secrets.yml 文件,数据库设置
控制台命令,bundle 命令
常量,布尔值
continue 命令(调试器),调试器命令
控制流,变量,嵌入 Ruby
控制器,控制器,控制器,根路由,路径和 URL,控制器操作,渲染或重定向,控制器测试,API 控制器
操作,路径和 URL
API,API 控制器
帮助器,根路由
参数,控制器操作
响应格式,渲染或重定向
测试,控制器测试
表单上的控件,辅助方法,显示评论
约定优于配置,你的第一个 Rails 应用
cookies, 响应格式, 数据库设置
秘密密钥, 数据库设置
cookies 哈希, 控制器测试
count 方法, 查询条件
创建操作, 表现层状态转移, 控制器测试断言, 控制器测试断言
控制器测试, 控制器测试断言
POST 请求, 控制器测试断言
创建命令,在 Heroku 中, 更新你的 Gemfile
创建评论操作,控制器, 回到控制器操作
created_at 字段, Rails 脚手架
create_join_table 方法, has_and_belongs_to_many
创建方法, 创建、读取、更新和删除, 回到控制器操作, 实现, 检查 SQL 输出
登录表单, 实现
在 SessionsController 中, 检查 SQL 输出
createuser 命令, 用于 PostgreSQL, 安装 Apache 和 Passenger
跨站请求伪造(CSRF), 布局, 跨站请求伪造攻击, 使用基于令牌的身份验证
令牌, 使用基于令牌的身份验证
跨站脚本攻击(XSS), 绕过身份验证系统
CRUD 功能, 创建、读取、更新和删除, 表现层状态转移
CSRF(跨站请求伪造), 布局, 跨站请求伪造攻击, 使用基于令牌的身份验证
令牌, 使用基于令牌的身份验证
csrf_meta_tags 方法, 样式表链接标签
csrf-token 元标签, 样式表链接标签
CSS, 内建优化功能(见层叠样式表(CSS))
Curl, 状态码, Curl, API 控制器, Jbuilder, 使用基于令牌的身份验证, 第十三章
检查索引操作输出, Jbuilder
测试 API, API 控制器
测试基于令牌的身份验证, Curl
花括号 ({ }), 数组, 迭代
块, 迭代
哈希值, 数组
当前用户, 身份验证系统, 实现, 身份验证用户
身份验证, 身份验证系统
识别, 实现
使用, 验证用户
current_user 方法, 实现, 调试助手, 验证请求
输出, 调试助手
自定义路由, 受限资源
CVS(并发版本系统), 版本控制
D
数据,测试, 测试数据
数据库添加列, 创建、读取、更新与删除, 创建、读取、更新与删除, 数据库迁移, 添加列, 表征状态转移, 表征状态转移, 路径和 URL
添加记录, 创建、读取、更新与删除
CRUD 功能, 创建、读取、更新与删除, 表征状态转移
当前状态, 数据库迁移
HTTP 动词用于操作, 表征状态转移
从中获取帖子, 路径和 URL
数据库迁移, 配置目录, Rails 脚手架, 查询条件, 更新 Gemfile, 用户模型, 帖子模型, 在 Rails 中测试
以及应用部署, 更新 Gemfile
用于创建表, 用户模型
防止创建, 帖子模型
使用时更新 db/schema.rb, 在 Rails 中测试
数据库查询,减少, Turbolinks 使用
database.yml 文件, 配置目录
数据模型, 高级活动记录, 高级活动记录, 多态关联
高级, 高级活动记录
用于社交网络服务, 多态关联
数据类型, 数据类型, 数据类型, 字符串, 字符串, 字符串, 数组, 哈希
数组, 字符串
布尔值, 哈希
哈希, 数组
数字, 数据类型
字符串, 字符串
符号, 字符串
dbconsole 命令, bundle 命令
db 目录, 配置目录, 查询条件, 数据库迁移
迁移目录, 查询条件
schema.rb 文件, 数据库迁移
db:rollback 语句, 迁移
调试器方法, 进入调试器, 第十三章
调试, 调试, 调试助手, 进入调试器
命令, 进入调试器
Rails 日志器, 调试助手
调试助手, 调试助手
:debug 日志级别, 调试助手
调试模式, 清单
def_delegators 方法, extend
define_method 方法, 鸭子类型
def 语句, 方法
委托, prepend
delete 方法, 控制器测试
DELETE 请求, 表现层状态转移, 受限资源, URL 辅助工具
delete_via_redirect 方法, 控制器测试断言
删除记录, 读取
dependent: :destroy 选项, 用户模型
deploy:check 任务, 添加到 Git
部署, 部署, 自定义部署, 虚拟私有服务器, 安装 Gems
Capistrano, 安装 Gems
虚拟私有服务器, 虚拟私有服务器
DESC 顺序, 用于检索帖子, 删除
destroy 动作, 表现层状态转移, 第十一章
destroy 方法, 读取, 回到控制器动作, 实现, 第八章
对于 user_id, 实现
开发环境, 调试模式, 调试模式
development.log 文件, 配置目录
development.rb 文件, 控制器
diff 命令(Git), 基本用法
数字签名, Heroku
指令, 在清单文件中, 内建优化特性
直接操作, 帖子模型
目录, Rails 基础, 安装 Apache 和 Passenger
创建 Ruby on Rails, 安装 Apache 和 Passenger
对于 Rails 项目, Rails 基础
display 命令(调试器), 调试命令
分布式版本控制系统, 版本控制
div 元素, class="container", 认证
除法, 数学运算, 数据类型
doctype(HTML5), 布局
文档, 认证, 安装 PostgreSQL
用于 Bootstrap,身份验证
来自 gem,安装 PostgreSQL
虚拟主机的文档根目录,添加虚拟主机
do, end 配对,用于添加块,路由
不重复自己 (DRY),不重复自己
do 语句,迭代
双冒号 (:😃 运算符,模块
双引号 ("),用于字符串,字符串
DRY(不重复自己),不重复自己
鸭子类型,鸭子类型
消除重复,运用断言
E
each 方法,条件语句,嵌入式 Ruby
急切加载,N + 1 查询
编辑操作,表现层状态转移,控制器操作
编辑按钮,条件语句,碎片缓存
艾菲尔,Ruby 基础
元素类,实例方法,第七章
元素,数组,布局
添加到数组末尾,数组
网页,布局
else 语句,变量
elsif 语句,变量
电子邮件地址,身份验证系统,文章索引与展示
当前用户,身份验证系统
存储,文章索引与展示
嵌入式 Ruby (ERB),不重复自己,视图,嵌入式 Ruby,控制流
注释,控制流
员工模型,定义关联关系,自连接关联
empty? 方法,字符串
end 语句,迭代,方法,模块
环境目录,控制器
等于 (==) 运算符,哈希
ERB(嵌入式 Ruby),不重复自己,视图,嵌入式 Ruby,控制流
注释,控制流
error_explanation div 元素,表单
错误信息,配置目录,表单
显示代码,表单
文件,配置目录
错误数组,测试数据
错误方法,第四章
/etc/apache2/sites-available 目录,添加虚拟主机
eval 命令(调试器),调试命令
感叹号 (!) ,方法名末尾,字符串
:exclusion 验证,验证
exit 命令,交互式 Ruby,Post 模型
extend 语句,prepend
F
favcon.ico 文件,公共目录
功能,启用和检查,鸭子类型
fetch 方法,缓存
Fibonacci 数列,prepend
Fielding, Roy,控制器
field_with_errors 类,表单错误
file.open 方法,传递块给它,迭代
file.read 方法,继承
file.split 方法,继承
find_by 方法,验证请求
find 方法,读取,授权攻击
first 方法,读取
fixtures,运行测试,显示用户
标志,在 Bundler 中,更新你的 Gemfile
flash 哈希,控制器测试
flash 消息,响应格式,注册
显示,注册
浮动点数学,数据类型
页面间的流动,测试,控制器测试断言
follow 动作,防止 CSRF 攻击,防止 CSRF
following?方法,用户模型
following!方法,用户模型
follow_redirect!方法,控制器测试断言
外键,高级活动记录,has_and_belongs_to_many
在迁移文件中,has_and_belongs_to_many
for 循环,条件语句,表单错误
form 构建器对象,表单错误
form_for 方法,表单错误,表单控件
表单,显示评论
用于评论的,表单控件
form_tag,实现
Forwardable 模块,prepend
Fowler, Martin, 《企业应用架构模式》,Post 模型
片段缓存,分页,低级缓存
friendly_date 助手方法,数字助手
全栈 Web 框架,Rails 基础
功能性测试,控制器测试
G
垃圾回收,优化,性能
gem 命令,公共目录,安装 PostgreSQL
Gemfile,Heroku,日志记录
添加调试器 gem,日志记录
为 Heroku 更新,Heroku
gems, Rails 基础, 身份验证, 身份验证, 帖子索引和显示, 帖子索引和显示, 清单, 分页, 日志记录, 日志记录, as_json, 安装 PostgreSQL, 安装 PostgreSQL, 入门指南, 第十五章
bcrypt, 帖子索引和显示
bootstrap_sass, 身份验证
byebug, 日志记录
capistrano-rails, 入门指南
资产管道搜索中的目录, 清单
文档来自, 安装 PostgreSQL
安装, 安装 PostgreSQL
jbuilder, as_json
更新已安装的, 身份验证, 帖子索引和显示, 日志记录
will_paginate, 分页
generate_api_token 方法, 生成令牌
generate 命令, bundle 命令
get 方法, 控制器测试
GET 请求, 表征状态转移, 受限资源, URL 帮助程序, 控制器测试断言, 跨站请求伪造攻击
和状态变化, 跨站请求伪造攻击
测试发行, 控制器测试断言
getter 方法, 类
get_via_redirect 方法, 控制器测试断言
git add 命令, 入门指南
git branch 命令, 其他有用的命令
git checkout 命令, 其他有用的命令, 分支
git commit 命令, 入门指南
git diff 命令, 基本用法
git --help 命令, 基本用法
GitHub, 部署你的应用, 数据库设置, 第五章
账户, 第五章
推送代码到, 数据库设置
GitHub API, Web APIs, GitHub API, Curl, Curl
与身份验证, Curl
令牌生成, Curl
git log 命令, 入门指南
git pull 命令, 分支
git push 命令, 分支, 更新你的 Gemfile
git remote add 命令, 分支
git status 命令,基本用法,其他有用的命令
Git 版本控制系统,部署,版本控制,版本控制,入门指南,入门指南,其他有用的命令,分支,数据库设置
基本用法,入门指南
分支,其他有用的命令
入门,版本控制
远程,分支
仓库,创建,数据库设置
设置,版本控制
暂存区,入门指南
大于(>)操作符,哈希
greet 方法,方法
H
汉松,David Heinemeier,Rails 基础
哈希版本的密码,帖子索引与显示
哈希,数组,迭代,入门指南
提交,入门指南
迭代,迭代
哈希火箭符号(=>),数组
has_many 关联,高级 Active Record,自连接关联,多态关联,用户模型,运行测试
has_many :leaders 关联,用户认证
has_many 方法,添加关联
has_many :through 关联,has_and_belongs_to_many,用户模型
has_secure_password 方法,帖子索引与显示,运行测试
head 元素(HTML),布局
head 方法,控制器测试
帮助命令,调试器,调试命令
--help 命令(Git),基本用法
helpers,不要重复自己,根路由,控制流,数字助手,显示评论,控制器测试,控制器测试断言
添加方法,数字助手
对于控制器,根路由
控制器测试,控制器测试
集成,控制器测试断言
表单控件的方法,显示评论
helpers 目录,控制器
Heroku 云应用平台,部署,Heroku,Heroku,更新 Gemfile
部署应用, 更新您的 Gemfile
Gemfile 更新, Heroku
heroku run 命令, 更新您的 Gemfile
Heroku 工具包, 安装, 简介, Heroku
hex 方法, 生成令牌
主页, 根路由, 认证用户
应用的根路由设置, 根路由
时间轴, 认证用户
HTML,共享代码的部分, CSRF 元标签助手
HTML5 字段类型, 其助手方法, 表单控件
HTML 页面, 不要重复自己, 布局
Rails 布局, 布局
Ruby 代码和, 不要重复自己
HTTP, GitHub API, GitHub API
状态码, GitHub API
HTTP 动词, 表述性状态转移, URL 助手
数据库操作, 表述性状态转移
I
标识符, 作为符号, 字符串
id 字段, Rails Scaffold, 读取
按照, 读取
if 语句, 变量, 第十章
ImagePost, 用户样本, 模型测试, 授权攻击
编辑, 授权攻击
用于的样本文件, 用户样本
验证测试, 模型测试
image_post_params 方法, 第十章
image_tag 助手, 帖子索引和展示
img-responsive 类(Bootstrap), 帖子索引和展示
include 语句,用于方法, 模块
:inclusion 验证, 验证
索引操作, 表述性状态转移, 路径和 URL, 认证系统, API 控制器
数组的索引, 数组
索引页面, 数字助手, 认证系统
对于帖子模型, 认证系统
对于帖子, 数字助手
为外键创建索引, has_and_belongs_to_many
继承, 类, 模块, has_many :through
在 Ruby 中, 模块
单表, has_many :through
initialize 语句, 方法
注入攻击, 授权攻击
不安全的直接对象引用, 授权攻击
inspect 方法, 日志级别
安装, 简介, 简介, 简介, Heroku, Amazon AWS 设置, 安装 Ruby, 安装 Apache 和 Passenger, 安装 PostgreSQL, 安装 PostgreSQL, 安装 Gems
Apache, 安装 Ruby
构建工具, 安装 PostgreSQL
gems, 安装 PostgreSQL
Heroku 工具包, 简介, Heroku
PostgreSQL, 安装 Apache 和 Passenger
Rails, 简介, 安装 Gems
Ruby, 简介, Amazon AWS 设置
实例, 类, 虚拟私人服务器
类的, 创建, 类
实例方法, 类
instance_methods 方法, 祖先
instance_of? 方法, 类
实例变量, 方法, 类
访问, 类
赋值给, 方法
实例化对象, 创建、读取、更新和删除
整数除法, 数据类型
集成测试, 控制器测试断言
交互式 Ruby 解释器 (IRB), 交互式 Ruby
内部服务器错误代码, 状态码
反射, 类
IRB(交互式 Ruby 解释器), 交互式 Ruby
irb 命令, 交互式 Ruby
is_a? 方法, 类
是-a 关系, 类
:is 验证, 验证
迭代, 条件语句
J
JavaScript, 布局, 认证, 内置优化功能, Turbolinks 实战
资源管道和, 内置优化功能
事件, Turbolinks 实战
在应用中包含, 认证
使用中的文件列表, 布局
javascript_include_tag 方法, stylesheet_link_tag
JavaScript 对象表示法 (JSON), 不要重复自己, 渲染或重定向, Web API, GitHub API, API 控制器
自定义输出, API 控制器
消息, Web APIs
jbuilder gem, as_json
连接表, 自连接关联, has_and_belongs_to_many, 用户模型
创建, has_and_belongs_to_many
多对多关联, 自连接关联
jq(JSON 处理器), API 控制器
JSON(JavaScript 对象表示法), 不要重复自己, 渲染或重定向, Web APIs, GitHub API, API 控制器
自定义输出, API 控制器
消息, Web APIs
json.array! 方法, Jbuilder
json.extract! 方法, Jbuilder
K
Kernel 类, 祖先
keys 方法, 哈希
键值对, 数组
L
label 辅助方法, 表单错误
last 方法, 读取
视图布局, 布局
leader_ids 方法, 认证用户
leaders 方法, 用户模型
length 方法, 字符串
:长度验证, 验证
小于(<)运算符, 哈希
lib/assets 目录, 清单
lib 目录, 配置目录
libpq-dev 包, 安装 PostgreSQL
limit 方法, 删除
link_to 辅助方法, 控制流
Lisp, Ruby 基础
list 命令(调试器), 调试命令
局部变量, 调试命令
日志目录, 配置目录, 用户数据, 调试助手
development.log 文件, 调试助手
test.log 文件, 用户数据
日志记录器(Rails), 调试助手
登录, 受限资源, 注册, 会话
自定义路由, 受限资源
实现, 会话
注销, 自定义路由, 受限资源
低级缓存, 分页, 缓存
M
mailers 目录, 控制器
manifests, 布局, 内置优化特性
多对多关联, 自连接关联
页面边距, 认证
主分支, 其他有用的命令
数学模块, 模块
数学运算, 在 IRB 中, 数据类型
松本行弘, Ruby 基础
maximum 方法, 查询条件
:maximum 验证, 验证
memcached 服务器, 缓存
备忘录, include
Memoize 模块, calc 方法, prepend
Mercurial, 版本控制
merge 方法, 哈希
元编程, 高级 Ruby, 鸭子类型
method_missing 方法, class_eval
methods, 哈希, 方法, 模块, 祖先
include 声明, 模块
传递命名参数给, 哈希
迁移文件,rails 生成命令创建空文件, has_and_belongs_to_many
迁移, 查询条件
(另见数据库迁移)
最小方法, 查询条件
:minimum 验证, 验证
MiniTest 框架, 测试, 帖子固定数据
错误, 调试 (见调试)
混入,用作模块, 模块
mkdir 命令, Rails 基础
models, 模型, 高级 Active Record, 帖子模型, 通过回调消除重复代码
为模型添加验证, 帖子模型
同类型之间的关系, 高级 Active Record
测试, 通过回调消除重复代码
模型-视图-控制器(MVC), 不要重复自己
module Api 声明, API 控制器
module 关键字, 模块
模块, 类, 高级 Ruby, 模块, 模块, 模块
作为混入, 模块
作为命名空间, 模块
模数(%)运算符, 数据类型
MVC(模型-视图-控制器), 不要重复自己
MySQL, 安装 Apache 和 Passenger
N
N + 1 查询, 检查 SQL 输出
命名参数,传递给方法, 哈希
names, 嵌入式 Ruby, CSRF 元标签助手, 模块
用于模块, 模块
用于部分模板, CSRF 元标签助手
用于模板, 嵌入式 Ruby
命名空间,用作模块, 模块
namespace :api 块, 认证
nano 编辑器, 添加虚拟主机
嵌套资源, 路由
网络通信, Curl 用于, 状态码
新动作, 表征状态转移, 控制器动作, 控制器测试断言
控制器测试, 控制器测试断言
新图片发布表单, 链接到, 第十章
字符串中的换行, 字符串
新方法, 类, 实现
登录表单, 实现
新发布表单, 渲染或重定向, 返回控制器动作, 显示评论
参数来自, 返回控制器动作
来自 Rails 脚手架生成器, 显示评论
下一个命令(调试器), 调试命令
nil, 交互式 Ruby, 数组, 数组, 方法
来自 [] 方法, 数组
来自访问不存在的键, 数组
方法定义, 方法
NoMethodError 异常, 祖先, 鸭子类型, class_eval
不等于(!=)运算符, 哈希
“not” 形式的断言, 发布固定数据
提示消息, 注册
非运算符, 条件语句
NOT 运算符, define_method
数字助手, URL 助手
数字, 数据类型
number_to_currency 方法, URL 助手
number_to_human 方法, URL 助手
number_to_human_size 方法, URL 助手
number_to_percentage 方法, 数字助手
O
对象类, 祖先
object_id 方法, 字符串
对象, 方法, 创建、读取、更新和删除
实例化, 创建、读取、更新和删除
状态, 方法
奇数, 数据类型
offset 方法, 删除
一对多关系, 测试数据
单向哈希, 发布索引和显示
开源软件,协作, 部署你的应用
开放式 Web 应用安全项目(OWASP), 总结
操作, 取消当前, 交互式 Ruby
优化, 性能, 内置优化功能, 内置优化功能, Turbolinks 在行动中
资产管道, 内置优化功能
内置功能, 内置优化功能
代码, Turbolinks in Action
垃圾回收, 性能
或(||)运算符, 哈希
时间线的排序子句, 使用当前用户
排序方法, 删除
输出标签(<%= %>), 嵌入式 Ruby
输出到屏幕, 交互式 Ruby
OWASP(开放网络应用安全项目), 总结
P
PaaS(平台即服务), Heroku
页面渲染速度, 分页
paginate 方法, 分页
分页与优化, N + 1 查询
参数, 哈希, 方法, 控制器操作
方法, 方法
传递命名方法, 哈希
新帖子表单中的参数, 返回控制器操作
参数哈希, 控制器操作, 创建帖子
:text_post 键, 创建帖子
父模型, 高级活动记录
部分, CSRF 元标签助手
检查密码属性, 运行测试
检查 password_confirmation 属性, 运行测试
密码摘要, 帖子索引和显示
密码, 帖子索引和显示, 检查 SQL 输出
认证, 检查 SQL 输出
哈希版本, 帖子索引和显示
patch 方法, 控制器测试
PATCH 请求, 表现层状态转移, URL 辅助工具
patch_via_redirect 方法, 控制器测试断言
路径助手, 根路由
企业应用架构模式(Fowler), 帖子模型
PDF 格式, 渲染或重定向
百分比, 数字助手
性能, 性能, 内置优化功能, 分页
(另见优化)
缓存与, 分页
Ruby on Rails, 性能
Perl, Ruby 基础知识
个人软件包档案(PPA), 亚马逊 AWS 设置
pg(PostgreSQL gem), 更新 Gemfile, 安装 Gems
安装, 安装 Gems
Phusion Passenger, 安装 Ruby
管道字符(|), 迭代
纯文本文件, 在 Ruby 中读取, 继承
平台即服务 (PaaS), Heroku
复数化助手方法, 低级缓存
多态关联, 单表继承
@post, 表单控件
@post.comments.build, 表单控件
post.comments.find 方法, has_many 方法
PostController, 日志语句, 日志级别
帖子数据, 用户数据
PostgreSQL, 安装, 安装 Apache 和 Passenger
PostgreSQL 数据库服务器, 更新你的 Gemfile
PostgreSQL gem (pg), 更新你的 Gemfile
post_id 字段, 测试数据
Post 索引视图, 帖子索引和显示
post 方法, 控制器测试
帖子模型, 模型, 用户模型, 身份验证系统
索引和显示页面, 身份验证系统
post_params 方法, 参数
post:references 选项, 测试数据
POST 请求, 表现层状态转移, 受限资源, 渲染或重定向, 身份验证
添加存储库, 身份验证
帖子, 数字助手, 显示用户, 授权攻击, 低级缓存
编辑权限, 授权攻击
索引页面, 数字助手
添加评论时的更新, 低级缓存
用户添加的能力, 显示用户
PostsController, respond_to 方法, 身份验证
PostsHelper 模块, 数字助手
帖子表, 帖子模型, 添加列
向中添加字符串列, 添加列
posts_url, 响应格式
post_via_redirect 方法, 控制器测试断言, 测试流程
PPA(个人包存档), 亚马逊 AWS 设置
谓词方法, 用户模型
prepend 语句, include
:presence 验证, 验证
email 字段的 presence 验证, 注册
标签,用于调试助手输出, 调试助手 美化打印, API 控制器, 第六章 JSON 数据, API 控制器 在 Ruby 中, 第六章 打印 Ruby 代码,调试器命令 按日志级别打印消息,日志级别 用户权限,授权攻击 生产环境,调试模式,调试助手 资源预编译,调试模式 默认日志级别,调试助手 production.rb 文件,控制器 程序员,ERB 注释用于记录,控制流程 提示符,交互式 Ruby,交互式 Ruby 返回工作状态,交互式 Ruby protect_from_forgery 方法,防止 CSRF,使用基于令牌的身份验证 public/assets 目录,调试模式 public 目录,config 目录 公钥,Heroku pull 命令(Git),分支 拉取更改到服务器,版本控制 push 命令(Git),分支,更新 Gemfile 将更改推送到服务器,版本控制 push 方法,extend put 方法,控制器测试 puts 方法,交互式 Ruby put_via_redirect 方法,控制器测试断言 ### Q 查询,N + 1,检查 SQL 输出 query_by_attribute 方法,class_eval 方法名末尾的问号(?),字符串 退出命令(IRB),交互式 Ruby 字符串的引号,字符串 ### R Rails,介绍,Rails 基础,Rails 基础,你的第一个 Rails 应用,不要重复自己,public 目录,Rails Scaffold,Rails 测试,安装 Gems 架构,不要重复自己 命令,public 目录 确认安装,Rails 基础 安装,介绍,安装 Gems 原则,你的第一个 Rails 应用 脚手架,Rails Scaffold 测试,Rails 测试 rails_12factor gem,用于 Heroku,更新 Gemfile Rails 应用,Rails 基础,控制器,布局,高级 Ruby 资源文件,布局 第一个,Rails 基础 模块,高级 Ruby 结构, 控制器 Rails.application.config.assets.paths 设置, 清单 Rails.cache.fetch 方法, 缓存, 低级缓存 rails 命令, bundle 命令 Rails 控制台, 帖子模型, 第八章 启动, 第八章 启动, 帖子模型 Rails 计数缓存, 低级缓存 Rails 开发环境,准备工作, Rails 测试 rails generate 命令, Rails Scaffold Rails 生成器, 添加列, 返回控制器操作, 显示评论 用于评论的控制器, 返回控制器操作 从新帖子表单, 显示评论 Rails 日志, 调试辅助工具, 调试辅助工具 级别, 调试辅助工具 rails new 命令, 控制器, bundle 命令, 第五章 由此创建的目录结构, 控制器 rails scaffold 命令, 第六章 rails server 命令, Rails 基础, 表单控件 rake 命令, 配置目录, Rails Scaffold, 迁移, 调试模式 预编译资源, 调试模式 rake db:migrate 命令, 更新你的 Gemfile 随机数生成器, 生成令牌 读-评估-打印循环(REPL), 交互式 Ruby 读取操作, 读取 ready 函数(jQuery), Turbolinks 实践 记录, 创建、读取、更新和删除, 读取, 读取, 查询条件 向数据库添加, 创建、读取、更新和删除 计数, 查询条件 删除, 读取 更新, 读取 红-绿-重构, 测试流程 redirect? 辅助方法, 控制器测试断言 重定向, vs. 渲染视图, 参数 redirect_to 方法, 渲染或重定向 重构代码, 公共目录 反射, 类 正则表达式, method_missing 余数, 数据类型 远程添加命令(Git), 分支 远程仓库,创建本地副本, 其他有用的命令 remove_column 方法,添加列 rename_column 方法,添加列 渲染动作方法,渲染或重定向 渲染命令,CSRF 元标记助手,创建帖子 用于表单部分,创建帖子 部分和,CSRF 元标记助手 渲染视图与重定向,参数 避免重复,不要重复自己 REPL(读取-求值-打印循环),交互式 Ruby 仓库,部署 表现层状态转移(REST),控制器 require 指令,内置优化功能 require_self 指令,stylesheet_link_tag,内置优化功能 require_tree 指令,内置优化功能,清单 require_tree . 语句,stylesheet_link_tag 资源路由,路由 resources :user_sessions 语句,受限资源 respond_to :json 方法,认证请求 respond_to 方法,返回控制器动作,认证 respond_to? 方法,鸭子类型 respond_to_missing? 方法,method_missing respond_with 方法,API 控制器 REST(表现层状态转移),控制器 受限资源,受限资源 return 语句,方法 订阅的反向,用户模型 robots.txt 文件,公共目录 根路由,根路由 注册页面的路由,注册 routes.rb 文件,配置目录 路由,路由,受限资源,根路由 自定义路由,受限资源 根路由,根路由 Ruby,简介,Ruby 基础,交互式 Ruby,亚马逊 AWS 设置 安装,简介,亚马逊 AWS 设置 交互式,交互式 Ruby Ruby 对象模型,高级 Ruby,extend Ruby on Rails,Rails 基础,性能,安装 Ruby,安装 Apache 和 Passenger,第十五章 应用服务器,安装 Ruby 创建目录, 安装 Apache 和 Passenger 性能, 性能 资源, 第十五章 Ruby 工具箱, 第十五章 ### S Sass, 内置优化功能 架构, 迁移 schema_migrations 数据库表, 迁移 schema.rb 文件, 配置目录 屏幕,输出到, 交互式 Ruby 用于 cookie 的密钥, 数据库设置 SECRET_KEY_BASE 环境变量, 添加虚拟主机 SecureRandom 类, 生成令牌 安全外壳(SSH),认证, Heroku 安全性, 安全, 安全, 授权攻击, 绕过身份验证系统, 跨站请求伪造攻击 授权攻击, 安全 跨站请求伪造(CSRF), 跨站请求伪造攻击 跨站脚本(XSS), 绕过身份验证系统 注入攻击, 授权攻击 Seeds.rb 文件, 配置目录 自连接关联, 高级 Active Record 服务器, Rails 基础, 状态代码, 虚拟专用服务器 (参见虚拟专用服务器) 错误状态代码, 状态代码 启动, Rails 基础 会话哈希, 控制器测试 会话, 注册 set_post 方法, 路径和 URL setter 方法, 类 setup 方法, 使用断言, 模型测试 短路运算符, 布尔值 显示动作, 表现状态转移, 路径和 URL, 身份验证系统 显示方法, 控制器 显示页面, 身份验证系统, 测试流程, 第十一章 用于评论, 第十一章 针对 post 模型, 身份验证系统 针对用户, 测试流程 添加注册表单, 注册 注册页面,添加路由, 注册 用户的注册流程, 帖子索引和显示 单继承, 类 单表继承, has_many :through size 方法, extend sleep 方法, 片段缓存 切片,在数组中,数组 Smalltalk,Ruby 基础 蛇形命名法,变量 Git 快照,入门指南 社交网络应用,多态关联,多态关联,多态关联,用户模型,评论模型 评论模型,评论模型 数据模型,多态关联 发布模型,用户模型 用户模型,多态关联 软件安装,apt-get 系统,Amazon AWS 设置 单词之间的空格,字符串 字符串中的特殊字符,字符串 SQL,创建、读取、更新和删除,注入攻击,Turbolinks 实践,检查 SQL 输出 命令,创建、读取、更新和删除 检查输出,检查 SQL 输出 注入攻击,注入攻击 程序优化,Turbolinks 实践 SQLite 数据库,配置目录 方括号 ([ ]),用于数组,字符串 SSH(安全外壳),通过认证,Heroku Capistrano 部署阶段,安装 Gems Git 的暂存区,入门指南 应用程序的状态,不要重复自己 HTTP 状态码,GitHub API 状态命令(Git),基本用法,其他有用命令 步进命令(调试器),调试命令 进入应用程序,日志记录 字符串,字符串 强参数,参数,返回控制器操作,添加注释 stylesheet_link_tag 方法,布局 提交助手,表单控件 订阅模型表示,用户模型 订阅表,用户模型 Subversion,版本控制 求和方法,查询条件 符号,字符串 ### T 字符串中的制表符,字符串 任务目录,配置目录 TDD(测试驱动开发),公共目录,测试流程 添加功能,测试流程 teardown 方法,将断言付诸实践 ERB 中的模板,嵌入式 Ruby 测试用例, 运行测试 测试目录, 公共目录, 公共目录, 在 Rails 中测试, 运行测试, 用户数据, 用户数据, 控制器测试断言, 控制器测试断言, 测试流程, 测试流程, 显示用户, 创建帖子 controllers/posts_controller_test.rb 文件, 控制器测试断言 controllers/text_posts_controller_test.rb 文件, 显示用户, 创建帖子 controllers/users_controller_test.rb 文件, 控制器测试断言, 测试流程 fixtures/posts.yml 文件, 用户数据 fixtures/users.yml 文件, 运行测试 integration/user_flow_test.rb 文件, 测试流程 models/user_test.rb 文件, 在 Rails 中测试, 用户数据 test_helper.rb 脚本, 公共目录 测试驱动开发 (TDD), 公共目录, 测试流程 添加功能, 测试流程 测试, 测试数据, 根路由, 用户模型, 测试, 在 Rails 中测试, 运行测试, 通过回调消除重复, 控制器测试, 控制器测试断言, API 控制器 使用 Curl 的 API, API 控制器 关联, 用户模型 控制器, 控制器测试 数据, 测试数据 辅助函数, 根路由 集成, 控制器测试断言 模型, 通过回调消除重复 在 Rails 中, 在 Rails 中测试 使用数据, 运行测试 测试日志, 用户数据 test.rb 文件, 控制器 文本框, 创建辅助函数, 表单错误 文本帖子, 创建帖子, 授权攻击, 授权攻击 创建新帖子的按钮, 创建帖子 编辑视图, 授权攻击 更新方法, 授权攻击 TextPost, 用户数据, 模型测试, 显示用户 创建帖子, 显示用户 测试数据文件, 用户测试数据 验证测试, 模型测试 TextPost 部分, 授权攻击, 碎片缓存 Edit 按钮的条件语句, 碎片缓存 链接到编辑部分, 授权攻击 文本帖子部分,编辑, 第九章 text_post_params 方法, 创建帖子, 使用基于令牌的认证 therubyracer gem, 安装 Gems 时间线,主页, 认证用户 timeline_user_ids 方法, 认证用户 tmp 目录, 公共目录 基于令牌的认证, Curl, Jbuilder 令牌, stylesheet_link_tag, Jbuilder, 生成令牌 当前会话, stylesheet_link_tag 生成, Jbuilder 请求认证, 生成令牌 Torvalds, Linus, 版本控制 touch 选项,用于关联, 低级缓存 touch tmp/restart.txt 命令, 配置 t.references :author 语句, has_and_belongs_to_many Tumblr, 多态关联 turbolinks, 资产预编译 ### U Ubuntu Linux 14.04 LTS, 虚拟专用服务器 Ubuntu Linux 设置, Amazon AWS 设置 下划线 (_),在部分名称中, CSRF 元标签助手 undisplay 命令(调试器), 调试命令 :uniqueness 验证, 验证 唯一性验证,用于电子邮件字段, 注册 唯一单词, 继承 单元测试, 通过回调消除重复 unless 语句, 条件语句 更新操作, 表述性状态转移 更新方法, 读取, 渲染或重定向, 返回控制器操作, 授权攻击 用于文本帖子的, 授权攻击 updated_at 字段, Rails 脚手架 更新记录, 读取 URL 帮助器, 根路由, 控制流 URLs, 路由, 帖子模型 映射操作到动词, 路由 验证, 帖子模型 用户关联, 用户模型 User.authenticate 方法,SQL 注入漏洞, 注入攻击 创建 User 类实例,Chapter 7 用户 fixtures,Running Tests 存储 session 中的 user_id,Sign Up 用户界面,Don’t Repeat Yourself(参见视图) 用户模型,Polymorphic Associations,Jbuilder api_token 字符串,Jbuilder 社交网络应用,Polymorphic Associations user:references 选项,Post Models 用户,Post Index and Show,Sign Up,Current User,Authorization Attacks 创建新项的动作,Sign Up 认证,Current User 权限,Authorization Attacks 注册流程,Post Index and Show ### V 验证,Validations,Post Models,Chapter 2 添加到 app/models/comment.rb 文件,Chapter 2 添加到模型,Post Models valid? 方法,Testing Data 有效用户,Running Tests values 方法,Hashes 显示对象的值,Log Levels var 命令(调试器),Debugger Commands 变量,Booleans,Booleans,Debugger Commands 检查值,Debugger Commands 仅在为 nil 时初始化,Booleans var 实例命令,Debugger Commands VCS(版本控制系统),Deployment vendor/assets 目录,Manifests vendor 目录,The public Directory 版本控制系统(VCS),Deployment 垂直管道符号(|),Iteration 视图,Don’t Repeat Yourself,Views,Layouts,Low-Level Caching 部分缓存,Low-Level Caching 布局,Layouts 创建视图模板,The Authentication System 虚拟主机,Adding a Virtual Host 虚拟专用服务器,Virtual Private Servers,Virtual Private Servers,Amazon AWS Setup Amazon AWS 设置,Virtual Private Servers Ubuntu Linux 设置,Amazon AWS Setup ### W web API,Web APIs web 浏览器上的 JSON 输出,API Controllers 创建 weblog,Rails Fundamentals 网页,Layouts,Chapter 15 元素,布局 标题,第十五章 WEBrick 服务器,Rails 基础 网络服务器,安装 Ruby where 方法,阅读,method_missing will_paginate gem,分页 没有生产选项,对于 bundler,入门指南 单词,文件中的计数,继承 write_with_time 方法,鸭子类型 ### X XML,不要重复自己,渲染或重定向 X-RateLimit-Limit,用于 GitHub API 请求,Curl XSS(跨站脚本攻击),绕过认证系统 ### Y YAML,运行测试,调试助手,调试助手 用破折号和点号表示开始和结束,调试助手 yield 语句,CSRF 元标签助手,认证 ### Z 数组的零索引,数组 # 第十七章:关于作者 Anthony Lewis 自 1994 年起一直从事网站开发工作。他是 Mass Relevance 的高级工程师,每天编写 Ruby on Rails 和 JavaScript 代码。他在 Lone Star Ruby Conference 主持研讨会,提供深入的 Rails 培训,并且是 Austin on Rails 的活跃成员。 # 第十八章:《Rails Crash Course: Rails 开发速成》 版权所有 © 2014 Anthony Lewis **Rails Crash Course** 保留所有权利。未经版权持有者和出版商事先书面许可,不得以任何形式或通过任何手段(包括复印、录音或任何信息存储或检索系统)复制或传播本作品的任何部分。 18 17 16 15 14 1 2 3 4 5 6 7 8 9 ISBN-10: 1-59327-572-2 出版社:William Pollock 制作编辑:Serena Yang 封面插图:W. Sullivan 内部设计:Octopod Studios 发展编辑:Jennifer Griffith-Delgado 技术审阅:Xavier Noria 校对:LeeAnn Pickrell 排版:Susan Glinert Stevens 校对:James Fraleigh 索引员:Nancy Guenther 如需有关发行、翻译或批量销售的信息,请直接联系 No Starch Press, Inc.: No Starch Press, Inc. 地址:245 8th Street, San Francisco, CA 94103 电话:415.863.9900;info@nostarch.com [www.nostarch.com](http://www.nostarch.com) *美国国会图书馆图书目录数据* Lewis, Anthony, 1975- 作者。 《Rails Crash Course:Rails 开发速成》/ 作者:Anthony Lewis。 页数 cm ISBN 978-1-59327-572-3 -- ISBN 1-59327-572-2 1. Ruby(计算机程序语言) 2. Ruby on rails(电子资源) I. 标题。 TK5105.8885.R83L49 2015 006.7’54--dc23 2014034816 No Starch Press 和 No Starch Press 标志是 No Starch Press, Inc. 的注册商标。文中提及的其他产品和公司名称可能是其各自所有者的商标。为了不在每次出现商标名称时使用商标符号,我们仅以编辑方式使用这些名称,并且有利于商标所有者,并无侵犯商标的意图。 本书中的信息以“按原样”方式分发,不提供任何担保。尽管在本书的准备过程中已采取一切预防措施,但作者和 No Starch Press, Inc. 对任何因本书中包含的信息而直接或间接导致的任何损失或损害,均不承担任何责任。 No Starch Press 2014-12-09T09:58:33-08:00


浙公网安备 33010602011771号