从-PHP-到-RubyonRails-全-
从 PHP 到 RubyonRails(全)
原文:
zh.annas-archive.org/md5/762bafe8f3f3653308e0583857e2ba6d
译者:飞龙
前言
嗨,大家好!从 PHP 到 Ruby 再到 Rails 是一本针对有 PHP 背景和理解的读者的指南,他们希望将自己的知识扩展到另一种面向对象的编程语言:Ruby。Ruby 是一种由 Yukihiro “Matz” Matsumoto 最初创建并在 1995 年公开发布的编程语言。自从其诞生以来,Ruby 一直是一种非常冗长的语言,开源社区也采纳了这一特性,使得 Ruby 程序在每行代码的意图上非常明确。本指南不仅将向您介绍 Ruby 的语言特性,还将帮助您进入 Ruby 应用程序开发的思维模式。在学习 Ruby 的这条路上,您还将学习 Ruby on Rails 的基础知识,这是一个由 David Heinemeier Hansson 开发并在 2004 年作为开源工具发布的用于创建 Web 应用的框架。随着 Ruby 和 Ruby on Rails 在技术公司中的日益流行和采用,全球对 Ruby 开发者的需求也在不断增加。无论您对 Ruby 和 Ruby on Rails 感兴趣,还是想了解 Ruby 是否适合您,这本指南都是为您准备的。
本书面向对象
本书面向那些已经有一定编程经验,并希望利用这些经验和知识来学习 Ruby 编程语言和思维模式的人。
从本书中受益最大的三种类型的人如下:
-
使用过其他编程语言的开发者。
-
有 PHP 工作经验的开发者,希望学习另一种编程语言。他们将通过示例和等效 PHP 代码的审查来获得 Ruby 语言的知识。
-
有任何 PHP 框架(Laravel、CodeIgniter、Symfony、CakePHP 等)工作经验的开发者。本书涵盖了 Ruby on Rails 框架。熟悉任何 Web 框架的人通过学习 Ruby on Rails 的模型-视图-控制器(MVC)实现将受益匪浅。
本书涵盖内容
第一章,理解 Ruby 思维方式和文化,介绍了 Ruby 开发者思考问题和编写代码的方式。
第二章,设置我们的本地环境,提供了一篇简短的指南,介绍如何安装 Ruby,以便您能够跟随本书中的练习。
第三章,比较基本的 Ruby 语法与 PHP,通过比较 PHP 和 Ruby 语法来帮助您轻松进入这门语言,了解它们的相似之处和不同之处。
第四章,Ruby 脚本与 PHP 脚本的比较,旨在利用 Ruby 的语法来编写更易读的代码。
第五章,库和类语法,介绍了 Ruby 的库(gem)及其安装和使用。本章还介绍了 Ruby 领域中的面向对象编程。
第六章,Ruby 调试,介绍了 Ruby 中可用于修复我们在运行时可能遇到的脚本中的错误和错误的工具。本章还提供了安装和使用这些工具的指南。
第七章,理解配置优于约定,介绍了 Ruby on Rails 网络框架,其安装以及使用它的最简单示例。
第八章,模型、数据库和 Active Record,介绍了通过模型使用 Ruby on Rails 处理数据库。它还涵盖了使用 ActiveRecord
的数据库操作的基本知识。
第九章,整合一切,提供了一篇指南,通过一个更实际的示例来生成一个简单的 Ruby on Rails 应用程序,使用我们在书中学到的一切。
第十章,托管 Rails 应用程序与 PHP 应用程序考虑因素,提供了一篇简短的指南,介绍了在现实场景,即生产环境中发布 Rails 应用程序时必须考虑的因素。
要充分利用这本书
您需要安装 Ruby 3.1.1(如有需要,请遵循书中的说明来设置 Ruby)。您还需要在您的计算机上安装 Git,因为 Ruby 库(gem)依赖于 git 来获取其源代码。如果您使用 macOS,您将需要安装 Xcode 命令行工具或 Xcode 本身,然后才能使用 Ruby。虽然不是强制性的,但您也应该安装 rbenv
以允许您安装不同的 Ruby 版本。
本书涵盖的软件/硬件 | 操作系统要求 |
---|---|
Ruby | Windows、macOS 或 Linux |
Ruby on Rails | Windows、macOS 或 Linux |
对于 Linux 用户,您需要使用以下命令(或根据您使用的发行版适当的等效命令)安装一系列库:
sudo yum install git-core zlib zlib-devel gcc-c++ patch readline readline-devel libyaml-devel libffi-devel openssl-devel make bzip2 autoconf automake libtool bison curl sqlite-devel
如果您使用的是这本书的数字版,我们建议您亲自输入代码或从书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码的复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件:github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails
。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有来自我们丰富的书籍和视频目录的其他代码包,可在github.com/PacktPublishing/
找到。查看它们吧!
使用的约定
本书使用了多种文本约定。
文本中的代码
:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“在 Ruby 中,我们实际上没有var_dump()
函数,而是每个对象都有一个名为inspect()
的现有方法。”
代码块如下设置:
require 'oj'
json_text = '{"name":"Sarah Kerrigan", "age":23, "human":true}'
ruby_hash = Oj.load(json_text)
puts ruby_hash
puts ruby_hash["name"]
任何命令行输入或输出都应如下所示:
gem uninstall oj
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“当您点击注册按钮时,您应该立即看到您在重定向之前试图浏览的页面。”
小贴士或重要注意事项
看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告,我们将非常感谢。请访问www.packtpub.com/support/errata并填写表格。
盗版:如果您在互联网上以任何形式发现我们作品的非法副本,我们将不胜感激,如果您能提供位置地址或网站名称,我们将非常感谢。请通过 copyright@packtpub.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《从 PHP 到 Ruby on Rails》,我们很乐意听听您的想法!请点击此处直接访问此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都非常重要,它将帮助我们确保我们提供高质量的内容。
下载此书的免费 PDF 副本
感谢您购买此书!
您喜欢在移动中阅读,但无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
不要担心,现在,每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠不止于此,您还可以获得独家折扣、时事通讯和每日免费内容的专属访问权限
按照以下简单步骤获取好处:
- 扫描下面的二维码或访问以下链接
packt.link/free-ebook/9781804610091
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他优惠发送到您的电子邮件
第一部分:从 PHP 到 Ruby 基础
在本部分中,您将了解 Ruby 的全套做事方式,同时将其与您使用 PHP 做事的方式进行比较。此外,您还将学习如何使用 Ruby 在语法、库(称为 gem)和调试工具方面的优势,使故障排除尽可能无痛。
本部分包含以下章节:
-
第一章,理解 Ruby 思维方式和文化
-
第二章,设置我们的本地环境
-
第三章,比较基本的 Ruby 语法与 PHP
-
第四章,Ruby 脚本与 PHP 脚本比较
-
第五章,库和类语法
-
第六章,Ruby 调试
第一章:理解 Ruby 思维方式和文化
Ruby 自 Yukihiro Matsumoto 创立以来已经有了一段相当长的历史。社区对语言的采用当然影响了 Ruby 的关注方向。但,在本质上,Ruby 在你“可以”和“应该”使用它编写程序/脚本的方式上非常直接。每种语言都有其独特的特性,社区将这些特性用于定义什么是良好的实践,什么是被认为是“坏”代码。虽然这可能是完全主观的,但这种主观性为作者最初创建语言的原意铺平了道路,使其成为社区希望语言成为的样子。
Ruby 的设计理念是极其易于阅读、灵活且面向对象。同样,这也适用于由于 Ruby 而产生的技术。我指的是在 Ruby 中创建的框架,例如 Ruby on Rails (rubyonrails.org
) 和 Sinatra (sinatrarb.com/
)。但我还指的是那些具有相同思维方式的工具,例如 Chef (www.chef.io/
)。所有这些工具都有共同的特性,但最突出的特性是可读性。一旦你进入 Ruby 的“领域”,你就能阅读和理解为纯 Ruby、Ruby on Rails 应用程序 API 或甚至 Chef 脚本(用于管理和配置基础设施)编写的代码。Ruby 并不会自动使你的代码更易于理解或阅读,但它为你提供了使代码更容易阅读的工具。使代码易于理解是专注于手头业务(或爱好)并减少试图理解某些代码在做什么的关键。
但在我们到达那里之前,我们需要转换到 Ruby 思维方式。在本章中,我们将通过涵盖以下主题开始我们的思维之旅:
-
创建可读的 Ruby 代码
-
面向对象的 Ruby
-
编写 Ruby 风格的代码
你下定决心学习一门新的编程语言。恭喜!至少对我来说,我很想赞扬这个决定,并希望它没有花你像我第一次对另一种编程语言产生浓厚兴趣时那么多的时间。一开始,我有点固执和犹豫,看到了 Ruby 的每一个缺点。我最喜欢的说法是,“我可以用 PHP 更容易做到那件事。”但后来,有一天,它突然变得清晰,我再也没有回头。Ruby 已经是我很长时间以来的首选语言。而且我不会试图过分夸大这一点。我拒绝说 Ruby 是最好的编程语言,因为这会回答一个有争议的问题。没有一种编程语言在所有语言中都是普遍优于其他语言的。我能做的是尝试向你展示我为什么喜欢 Ruby。
技术要求
要跟随这本书,你需要:
-
Git 客户端
-
rbenv(Ruby 版本管理器,可启用多个 Ruby 版本)
-
Ruby(版本 2.6.10 和 3.0 或更高版本)
-
您可以选择安装任何 IDE 来编辑代码(Sublime, Visual Studio Code, vim, Emacs, Rubymine 等)
所有代码示例都已编写以与 Ruby 版本 2.6.10 和 3.0(或更高版本)兼容。其中一些示例可能需要以前的 Ruby 版本(即 2.6.10)才能与以前的 Ruby on Rails 版本(例如 Ruby on Rails 5)兼容,但为了确保它们与本章中的方式相同,您应该尝试安装最新的 Ruby 版本。您可以从这里获取不同操作系统的安装程序:www.ruby-lang.org/en/downloads/
.
此外,为了能够使用不同版本的 Ruby,我建议您从以下 GitHub 仓库安装 rbenv:github.com/rbenv/rbenv
.
本书中的代码可在github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails
找到。
Ruby 的设计意图是像句子一样阅读
说了这么多关于 Ruby 的话,让我们动手实践,从最基本的概念开始。您已经了解了 PHP 变量。变量存储信息,可以在我们的程序中使用和引用。此外,PHP 是一种动态类型语言,这意味着解释我们的 PHP 代码的“引擎”将自动推断该变量内的内容类型。也就是说以下两点:
-
我们不需要定义变量包含的内容类型
-
变量可以在不失败的情况下改变类型
来自 PHP 的您不需要费尽脑筋去学习 Ruby 的新用法或定义变量的新方式,因为 Ruby 的行为完全相同。然而,请注意,在其他强类型语言中,如 Java,变量必须用其将包含的类型定义,并且其类型不能随时间改变。
因此,让我们在 PHP 中玩一些变量:
<?php
$name = "bernard";
$age = 40;
$height_in_cms = 177.5;
$chocolate_allergy = true;
$travel_bucket_list = ["Turkey", "Japan", "Canada"];
在这个场景中,Ruby 并没有太大的不同:
name = "bernard";
age = 40;
height_in_cms = 177.5;
chocolate_allergy = true;
travel_bucket_list = ["Turkey", "Japan", "Canada"];
对于那些阅读此文档且可能因我没有使用$
符号而眼睛发红的经验丰富的 PHP 开发者。PHP 和 Ruby 之间的另一个区别是,我们不需要任何标签来表示 PHP 代码,而在 PHP 中,我们使用开头的 PHP 标签(<?php
)。因此,我们片段之间的主要区别(到目前为止)是我们调用 PHP 代码的方式和引用变量的方式。虽然这是一段有效的 Ruby 代码,但我故意将其写成 PHP 风格,以便让您也一窥 Ruby 的灵活性。Ruby 非常灵活,甚至可以弯曲 Ruby 自己的行为。这种灵活性的一个例子是,虽然我们可以在每行末尾添加分号(;
),但 Ruby 的最佳实践是省略它们。如果您对 Ruby 的灵活性感兴趣,您可能想查看 Ruby 的元编程。这是一份很好的起点指南:
www.rubyguides.com/2016/04/metaprogramming-in-the-wild/
但我们不要急于求成,因为这个主题真的很复杂——至少对于一个初学者 Ruby 程序员来说是这样。
基于前面的 PHP 代码,现在让我们确定名字是否为空。在 PHP 中,你会使用empty
内部函数。我们用另一个内部函数var_dump
包围它,以显示empty
函数的结果内容:
$name = "Bernard";
var_dump( empty($name) );
这将输出以下内容:
bool(false)
根据empty
函数的文档,这是false
,因为名字不是一个空字符串。现在,让我们在 Ruby 中尝试一下:
name = "bernard";
puts name.empty?;
在这里,有几个需要注意的地方。首先想到的是,这几乎就像一个句子一样被阅读。这是 Ruby 社区如何聚集在一起并使用 Ruby 编写人类可读代码的关键点之一。在大多数情况下,你应该避免在代码中添加注释,除非它是版权声明和/或确实需要解释。Ruby 甚至有一种奇怪的方式来编写多行注释。如果我要在我的代码上写多行注释,我必须查找语法,因为我从未使用过那种符号。这并不是说你不能或你不应该这样做。它有它的原因。这仅仅意味着 Ruby 社区很少使用那种符号。要在 Ruby 中添加注释,你只需在行首添加井号符号(#
):
# This is a comment
正如您从代码片段中的注释中知道的那样,这一行将被 Ruby 忽略。请记住,编程语言,就像 spoken language,会因使用而演变。最好的工具可能因为没有人使用而丢失。学习语言的一部分也涉及到学习工具的使用和最佳实践。这包括了解 Ruby 社区决定不利用什么以及使用什么。因此,尽管社区很少使用多行注释,但所有 Ruby 开发者都会利用其最强大的工具之一:对象。
一切都是对象
在阅读之前的代码时,我脑海中浮现的第二件事是我们正在对一个字符串调用一个方法。现在,让我们稍微退后一点,这就是我们开始用新手的眼光审视 Ruby 代码的地方。我们的变量名包含一个字符串。这意味着我们的名字是一个对象吗?嗯,简短的答案是是的。在 Ruby 中,几乎所有东西都是一个对象。我知道这可能会感觉像是我们跳过了几章,但请耐心等待。我们将在第五章中看到 Ruby 的面向对象语法。现在,让我们通过以下行进一步在我们的代码中探索我们的变量具有哪种类型的对象:
puts name.class();
这将返回我们对象所属的类类型(在这个特定案例中,String
)。我们能够用其他变量做同样的事情,我们会得到类似的价值(Integer
、Float
、TrueClass
或Array
)。为了进一步证明我的观点,即 Ruby 中几乎一切都是对象,让我们阅读以下示例:
puts "benjamin".class();
这也将返回一个String
类型。所以,当你编写 Ruby 代码时,请记住这一点。现在,让我们回到最初的带有empty
函数的例子:
name = "bernard";
puts name.empty?();
我们还注意到第三点是,我们实际上是在提问。这让我第一次看到它时感到困惑。你如何知道何时提问?Ruby 是否真的如此直观,以至于你可以提问?这是什么魔法?不幸的是,事实远没有代码本身那么神秘。在 Ruby 中,我们可以将函数或方法命名为包含问号符号的一部分,仅为了提高可读性。它对 Ruby 解释器没有特殊的执行或含义。我们只是能够以这种方式命名方法/函数。话虽如此,按照惯例,Ruby 开发者使用问号来暗示我们将返回一个布尔值。在这种情况下,它仅仅回答了关于变量名称是否为空的提问。简单来说,如果名称为空,则提问将返回true
值,反之亦然。这种命名技术是 Ruby 哲学的一部分,旨在使我们的整个代码可读。此外,这种代码风格贯穿于 Ruby 的许多内部类中。一些附加到数字对象和数组对象的方法就是这种风格的例子。以下是一些示例:
-
.``odd?
-
.``even?
-
.``include?
所有这些示例都是以这种命名方式命名的,仅为了提高可读性,没有其他原因。其中一些甚至在不同的类之间共享,但每个类型都有自己的实现。当我们目前正在查看问号符号时,让我们看一下一个类似的符号:感叹号(!
)。在 Ruby 开发者中,它有一个稍微不同的含义。让我们用一个例子来看看。
让我们展示名称的大写字母形式。在 PHP 中,我们会写出以下代码:
$name = "bernard";
echo strtoupper($name);
在 Ruby 中,可以通过以下代码实现相同的功能:
name = "bernard";
puts(name.upcase());
在这两种情况下,这将返回名称的大写形式(BERNARD
)。然而,如果我们对name
变量进行任何其他引用,变量将保持不变:
name = "bernard";
puts(name.upcase());
puts(name);
这将返回以下结果:
BERNARD
bernard
但如果我们添加感叹号(!
)会发生什么呢?
name = "bernard";
puts(name.upcase!());
puts(name);
这将返回名称的大写形式两次:
BERNARD
BERNARD
事实上,感叹号符号会永久地修改变量内容。使用感叹号命名的函数被称为String
和Array
类:
-
.``downcase!
-
.``reverse!
-
.``strip!
-
.``flatten!
我们可以通过阅读它们来推断它们的功能,但现在我们知道了在这个上下文中感叹号符号的含义。使用时要小心,但如果使用场景需要,也不要害羞地使用它们。现在,当你阅读 Ruby 代码时,你会意识到问号(?
)和感叹号(!
)符号的存在。
转向 Ruby
到目前为止,我们已经看到了一些例子,我们的代码看起来非常类似于 PHP。正如我之前提到的,我故意这样做是为了展示 Ruby 的灵活性。这使得将代码转换为 Ruby 比其他语法与 Ruby 差异很大的语言要容易得多。然而,这仅仅是我们成为 Ruby 开发者旅程的开始。如果我们想要能够像经验丰富的 Ruby 开发者一样阅读和编写 Ruby 代码和代码片段,我们需要了解社区是如何使 Ruby 代码的。简而言之,虽然我们可以将代码编写得类似于其他语言,但我们应该避免这种做法,在这个过程中,了解 Ruby 可以提供什么来使我们的代码越来越易于阅读。
我们将采取的第一个步骤是移除代码中的不必要的语法。为了做到这一点,我们也必须理解我们正在移除的内容的用途。
让我们以我们的原始代码为例:
name = "bernard";
age = 40;
height_in_cms = 177.5;
chocolate_allergy = true;
travel_bucket_list = ["Turkey", "Japan", "Canada"];
在 Ruby 中,分号可以用来将多行代码合并成一行,每行用分号分隔。如果我们把名字和例子转换成大写,我们会得到以下内容:
name = "bernard"; name.upcase!(); puts(name);
这工作得非常好。但请记住,我们正在努力使我们的代码更易于阅读。这并不更易于阅读。相反。而且,如果我们不打算将整个代码写在一行中,那么让我们保留原始代码片段(多行),并移除每个分号。这开始看起来更像 Ruby:
name = "bernard"
age = 40
height_in_cms = 177.5
chocolate_allergy = true
travel_bucket_list = ["Turkey", "Japan", "Canada"]
通过移除未使用的字符,这确实稍微提高了可读性,但我们还没有完成。让我们用另一个例子来实践。让我们编写一个示例,如果 $chocolate_allergy
变量的值为 true
,则将打印出字符串 This person is allergic to chocolate
。由于我们的 PHP 背景,我们可能会被促使编写类似于 PHP 的代码。在 PHP 中,我们会编写以下内容:
$chocolate_allergy = true;
if($chocolate_ allergy)
{
echo "This person is allergic to chocolate";
}
考虑到这一点,我们会在 Ruby 中编写以下代码:
chocolate_allergy = true
if(chocolate_allergy)
puts("This person is allergic to chocolate")
end
这工作得很好,但它仍然看起来很像 PHP。一个中级 Ruby 开发者可能会写类似以下的内容:
chocolate_allergy = true
puts "This person is allergic to chocolate"
if chocolate_allergy
这使得代码的可读性每秒都在提高。但它也带来了一些新的实践。首先,puts
语句没有被括号包围。这是因为,在 Ruby 中,对于函数和方法,括号的使用是可选的。这非常有用,因为它开始看起来像普通的英语。它也适用于具有多个参数的函数。例如,一个实现的函数可能看起来像这样:
add_locations "location 1", "location 2"
当然,如果我们需要调用嵌套函数,这会变得很繁琐。让我们看看以下两个函数的例子:
def concatenate( text1, text2 )
puts text1 + " " + text2
end
def to_upper( text )
return text.upcase()
end
concatenate
函数接受两个字符串,并将它们之间用空格连接的字符串打印出来。第二个函数只是将输入字符串转换成大写字符串并返回值。如果我们没有使用括号,这可能会成为问题。如果我们想将两个字符串连接起来,并将每个字符串转换成大写字符串,我们可以尝试以下操作:
concatenate to_upper "something", to_upper "else"
但我们会失败得很惨,因为 Ruby 解释器不知道 "something"
是 to_upper
函数的参数。我们可以通过括号轻松解决这个问题:
concatenate to_upper("something"), to_upper("else")
注意这个知识,就像其他所有事情一样,如果过度使用,可能会损害我们代码的可读性。在决定是否使用括号时,我们需要考虑两个额外的点。第一个是,这些规则也适用于函数的定义。因此,concatenate
函数可以这样定义:
def concatenate text1, text2
puts text1 + " " + text2
end
第二点是,这个规则也适用于没有参数的函数——也就是说,我们也可以从它们中移除括号。让我们以下面的例子作为参考:
return text.upcase()
现在将变成以下内容:
return text.upcase
更重要的是,使用带问号的方法和破坏性方法(!
)现在对可读性来说非常合理。
让我们看看以下内容:
name.empty?()
这变成如下所示:
name.empty?
作为另一个例子,让我们看看以下内容:
name.upcase!()
现在变成如下所示:
name.upcase!
我们现在要讨论的最后一点与 Ruby 在返回值方面的行为有关。虽然方法可以使用 return
关键字显式返回一个值,但 Ruby 不需要这个关键字。在函数和方法内部,Ruby 会自动返回最后引用的值。让我们用以下例子来说明这一点:
def to_upper text
return text.upcase
end
那个例子将变成这样:
def to_upper text
text.upcase
end
你会看到很多这样的 Ruby 代码。一开始可能会觉得令人畏惧和困惑,但一旦你理解了 Ruby 在做什么,它就会变得更有意义。
如你现在可能已经意识到的,Ruby 的创造者非常重视这些工具,以便更容易编写可读的代码。社区也采纳了这种理念并将其付诸实践。我们不仅看到使用这些约定和规则来提高可读性的代码,还看到 Ruby 程序员采用其他约定,虽然它们本身不是 Ruby 规则的一部分,但在特定上下文中使用时却非常合理。我指的是变量和方法命名。因为 Ruby 开发者会努力使他们的代码看起来像普通英语,所以他们会在如何命名方法和变量以使代码更易读上花费大量时间。因此,在 Ruby 中更常使用蛇形命名法,因为它有助于提高可读性。考虑到这一点,让我们看看这个例子:
chocolate_allergy = true
puts "This person is allergic to chocolate"
if chocolate_allergy
我们仍然可以提升其可读性。这不仅仅是一个语法上的改变;它还涉及到变量名甚至定义一个方法,只是为了提高可读性。因此,一位经验丰富的 Ruby 开发者可能会为这个例子编写以下最终的代码片段:
def say text
text
end
is_allergic = true
say "This person is allergic to chocolate" if is_allergic
正如你所见,Ruby 开发者会尽力使代码尽可能像普通英语一样易于阅读。当然,这并不总是可行的,有时它可能并不实用,因为它有时需要投入大量的努力来编写即使是简单的东西,但大部分情况下,只要它是可读的,遵循这些指南,其他开发者会感谢你,而不仅仅是 Ruby 开发者。
摘要
在本章中,我们介绍了 PHP 和 Ruby 之间的语法差异和相似之处,Ruby 的可读性工具,以及 Ruby 的语法灵活性。我们还学习了问号(?
)和感叹号或叹号符号(!
)。走到这一步意味着你确实在尝试重用你之前的编程技能,但使用的是一门新语言:Ruby。这是一个很好的开始,因为你可以跳过学习一门新语言中最困难的部分之一:逻辑部分。虽然我们只看到了 Ruby 的表面,更重要的是,我们清楚地看到了 Ruby 开发者编写代码时的思维方式。我们了解到,对于 Ruby 开发者来说,可读性是最重要的。我们不仅使用语法和语言结构来实现这一点,我们还使用对象来增加代码的可读性。它读起来越像句子,就越好。我们查看了一些简单的 Ruby 示例,虽然你可以跟随,但这并不是练习的目的。它更多的是为了激发你的兴趣。
为了继续学习这条学习路径,我们现在需要适当的工具来开始编写和运行 Ruby 代码。在下一章中,我们将探讨安装 Ruby 的不同方法以及设置我们的本地环境,这样我们就可以开始学习 Ruby 的真实示例,并最终跟随这个过程。
第二章:设置我们的本地环境
作为一名开发者,你可能已经知道,你需要掌握的关键技能之一是将编程语言本身安装到你的电脑上。我们需要一种方法来开始测试代码,除了我们的大脑,因为我们的大脑不是最好的语言编译器。但安装语言到底意味着什么呢?
来自 PHP,这可能意味着在我们的电脑上安装 PHP 的二进制解释器,以便我们可以运行 PHP,打开浏览器,然后就可以开始了。或者,这也可能意味着下载 PHP 源代码,编译它,并使用我们选择的编译选项生成自己的二进制文件。在 Ruby 中,我们不仅有与这些相似的选择,还有许多安装 Ruby 解释器到本地机器的方法。
在本章中,我们将探讨设置我们的开发环境的不同方法。我们将分析每种方法的优缺点,为您提供不同的选择,以便开发者在出现错误时能够保持一致性,从而避免所有开发者都听过并不幸使用过的讨厌短语:“在我的机器上它运行正常。”
因此,在本章中,我们将涵盖以下主题:
-
在本地安装 Ruby
-
使用虚拟机
-
使用 Docker
-
使用 rbenv
技术要求
要跟随本章内容,我们需要以下内容:
-
任何用于查看/编辑代码的 IDE(例如 SublimeText、Visual Studio Code、Notepad++ Vim、Emacs 等)
-
对于 macOS 用户,您还需要安装 Xcode 命令行工具
本章中展示的代码可在github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails
找到。
在本地安装 Ruby
我们已经准备好设置我们的 Ruby 环境。可能在我们机器上安装 Ruby 最方便的方式是使用包管理器或安装程序,这取决于您的操作系统。
macOS 用户
对于 macOS 用户,brew 包管理器是最佳选择。
要安装 brew,使用 Finder 窗口,导航到应用程序文件夹,然后到实用工具文件夹,然后滚动直到找到终端:
图 2.1:应用程序实用工具
双击终端图标,你应该会看到一个带有 shell 提示符的终端,等待输入命令:
图 2.2:终端
然后从 brew 主页([brew.sh/
](https://brew.sh/))复制以下命令:
图 2.3:Homebrew 安装说明
现在将命令粘贴到终端以安装 brew。
一旦安装了 brew,安装 Ruby(或其变体)就变得简单。如果 brew 已经安装,你可能只想通过运行 brew update
命令来更新它。你可能需要打开一个新的终端窗口,但一旦你做了,你只需运行以下命令即可安装 Ruby:
~ $ brew install ruby
这将产生以下输出:
Running `brew update –auto-update`…
.
.
.
==> Summary
🍺 /opt/homebrew/Cellar/ruby/3.1.2_1: 15,996 files, 42.8MB
==> Running `brew cleanup ruby`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
==> `brew cleanup` has not been run in the last 30 days, running now...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
.
.
.
Removing: /Users/bpineda/Library/Logs/Homebrew/gnutls... (64B)
Pruned 0 symbolic links and 6 directories from /opt/homebrew
==> Caveats
==> ruby
By default, binaries installed by gem will be placed into:
/opt/homebrew/lib/ruby/gems/3.1.0/bin
You may want to add this to your PATh.
ruby is keg-only, which means it was not symlinked into /opt/homebrew,
because macOS already provides this software and installing another version in
parallel can cause all kinds of trouble.
If you need to have ruby first in your PATH, run:
ec'o 'export PA"H="/opt/homebrew/opt/ruby/bin:$P"'H"' >> /Users/bpineda/.bash_profile
For compilers to find ruby you may need to set:
export LDFLA"S="-L/opt/homebrew/opt/ruby/"ib"
export CPPFLA"S="-I/opt/homebrew/opt/ruby/incl"de"
这里显示的代码已被简化以节省篇幅,并且可能与不同版本的 macOS 版本有所不同,但本质上,在安装了 brew 之后,你应该会看到相同(或非常相似)的输出。只要没有错误,你应该就可以正常使用了。作为旁注,我想强调 brew 是许多在 macOS 和一些 Linux 环境中工作的开发者使用的包管理器,但它并不是安装 Ruby 的唯一途径。
最后一步,我们应该打开一个新的终端窗口。这将重新加载 $PATH
变量,并使我们能够使用 Ruby。这是一种非常实用的安装 Ruby 的方法,因为我们可以直接开始使用 Ruby 解释器。
现在,让我们确认 Ruby 确实已经安装。在终端中,输入以下内容:
ruby -v
这应该会返回已安装的 Ruby 版本:
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [arm64-darwin21]
我们已成功安装 Ruby。如今,大多数 Mac 电脑出厂时已经预装了 Ruby。然而,这是一个过时的 Ruby 版本,因此自己安装它仍然是一个很好的实践练习。
Windows 用户
对于 Windows 用户,安装 Ruby 最简单的方法是使用来自 rubyinstaller.org/
的安装程序:
图 2.4:Ruby Windows 安装程序
我选择了最新版本,因为它是最推荐的安装版本,或者至少是最新稳定版本。务必根据您的机器架构选择正确的版本(32 位或 64 位)。下载完成后,双击安装程序,你将看到这个屏幕:
图 2.5:Windows 安装程序许可协议
接受许可协议并点击下一步。
然后选择默认的安装位置和将二进制文件添加到我们的 PATH 中的选项:
图 2.6:Windows 安装程序安装位置
之后,选择安装。一旦这个过程完成,我们会看到一个运行‘ridk install’的选项:
图 2.7:Windows 安装程序完成设置
当 sh
、make
和 gcc
可用。这些工具需要为一些我们可能在未来使用的库(或 gems)进行编译。通过设置此选项,将弹出一个新的提示:
图 2.8:MSYS2 安装提示
对于所有选项,按下 Enter 键直到安装完成。这些选项会安装和更新 MSYS2,它为与在 Windows 系统上构建的软件兼容提供了类似 Unix 的环境。
完成后,请确保 Ruby 已正确安装。为此,请打开 Windows PowerShell。然后使用 ruby -v
命令,你应该会看到以下内容:
图 2.9:Windows Ruby 版本确认
通过这样,我们已经确认 Ruby 已正确安装在我们的 Windows 系统上。
Linux 用户
对于 Linux 用户,我们有不同的发行版,尽管其中一些共享包管理器,但大多数情况下,每个系列都使用自己的包管理器。例如,Red Hat 系列的发行版(Red Hat 和 CentOS)使用 yum
,Ubuntu 使用 apt
,而 Debian 使用 dpk
,但也支持 apt
。我们将重点关注作为桌面操作系统最常用的发行版,即 Ubuntu。
在 Ubuntu 中,导航到 sudo apt install ruby
。然后 Ubuntu 将确认你确实正在尝试安装 Ruby。当出现此确认时,只需输入 Y,正如你所看到的那样:
图 2.10:Ruby 安装提示
我们使用 sudo
命令,因为我们需要在系统上安装应用程序需要 root 权限。根据我们的互联网速度和机器的配置,安装可能需要一段时间,但一旦过程完成,我们再次使用 ruby -v
验证安装:
图 2.11:Ubuntu Ruby 版本确认
因此,我们已经完成了,你现在知道如何在任何你使用的操作系统上安装 Ruby。现在让我们看看如何使用虚拟环境来与 Ruby 一起工作。
使用虚拟机
到目前为止,我们已经看到了如何在我们的本地机器上安装 Ruby,即我们每天用于工作的机器。这是开始安装 Ruby 的最佳方式,但一旦你开始处理更复杂的应用程序与其他开发者合作,你将希望确保每个人的本地环境行为的一致性。
为什么你会问?简单的答案是,我们想要避免所有开发者都曾听说过或在某些时候在我们的职业生涯中使用过的那个可怕的短语:“在我的机器上它运行正常。”重要的是要记住,每个本地环境都是不同的,从处理器到操作系统版本和 Ruby 版本,这可能会妨碍更重要的工作。
以我个人的经历为例,我们曾经在一次将 PHP 应用程序部署到在 Windows 本地机器上开发的 Linux 服务器时浪费了近一周的时间。问题是其中一位开发者忘记当时 Windows(当时)在文件夹名称中不区分大小写。虽然这听起来可能是个愚蠢的例子,但我们应尽可能避免这类问题,因为在处理这类问题时,时间会迅速被浪费。
虚拟化在创建团队中开发者的等效环境方面可以发挥重要作用,幸运的是,我们在虚拟领域有几种选项可以探索,以帮助我们实现这一点。
VMware
第一个选项是 VMware (vmware.com
)。VMware 允许你在机器内模拟完整的操作系统。当然,这种设置比其他方式花费的时间更长,因为你必须首先安装 VMware,创建虚拟机,然后在虚拟机中安装操作系统,最后安装 Ruby。这确实很复杂,也很耗时,但一旦设置好并运行起来,你就可以与其他团队成员共享环境。这意味着团队中的每个人都会拥有完全相同的环境。
VirtualBox
VirtualBox (www.virtualbox.org
) 是一个 Oracle 产品,其行为和工作流程与 VMware 类似。有些人喜欢 VirtualBox,有些人喜欢 VMware。根据我个人的经验,对于初学者来说,VirtualBox 是更好的选择,因为它是开源的,免费使用,有更好的用户界面,并且非常适合小型到中型项目。
Vagrant
Vagrant (www.vagrantup.com/
) 是一个帮助我们自动化和管理环境的工具。VMware 和 VirtualBox 都有机制可以与宿主机共享文件和其他资源;然而,它们的使用并不那么简单,有时配置它们会花费很多时间。Vagrant 出现就是为了解决这个问题:它允许我们指定本地机器和虚拟机之间的共享文件夹,允许我们通过配置文件复制配置,并且使连接到虚拟机变得更加容易。最酷的部分是 Vagrant 可以无缝地与 VMware 和 VirtualBox 一起工作。
Laravel 开发者可能对 Vagrant 很熟悉,因为 Homestead 可以与 VirtualBox 配合使用。即使你不想使用这个选项,因为它可能不太实用,我也建议至少尝试一次,以便在除你机器上安装的操作系统之外的环境中玩转 Ruby。这种虚拟化消耗大量资源,包括内存、处理器,甚至磁盘空间。为了方便而付出高昂的代价,但从长远来看是值得的。
注意
由于这超出了本书的范围,并且对于初学 Ruby 的开发者来说不是必需的;我们不会探讨如何执行此类安装。但对于一个新手 Ruby 开发者来说,至少应该熟悉这些工具,哪怕只是知道它们的名字。如果您对这些实现更感兴趣,尤其是 Vagrant 和 VirtualBox,您可能想访问以下网站:www.taniarascia.com/what-are-vagrant-and-virtualbox-and-how-do-i-use-them/
。
最后,Docker 提供了一种不同于 VMware 和 VirtualBox 的虚拟化类型,它比两者都要轻量,这是我们接下来要探讨的内容。
使用 Docker
Docker(www.docker.com/
)是另一种虚拟化技术,已成为许多中型和大型企业的首选选项。虽然它仍然是一个虚拟化环境,并带有它自己的缺点,但优点超过了它们。让我们看看一些优点:
-
Docker 不是完全虚拟化的——它创建了一个与运行它的主机共享资源的容器。正因为如此,它的运行速度比虚拟化环境要快得多。我在过度简化 Docker 技术,但本质上它是一个改进的(在我看来)虚拟化环境。
-
您可以将您的 Docker 环境打包成一个 Docker 镜像(类似于虚拟机),这样其他人就可以轻松地部署和使用它。
-
随着云计算的出现,Docker 变得越来越受欢迎。亚马逊(AWS)、微软(Azure)和谷歌(GCP)都支持 Docker,并且逐渐使其实现变得更加容易。
-
使用 Docker,您可以在本地复制另一台开发者的机器上发生的几乎所有错误,以及在生产环境中(在正确的情况下)。想象一个假设的开发者世界,您可以在生产服务器上进行测试,确保在向公众展示之前,过去看到的任何错误都得到了纠正。好吧,这几乎就是 Docker 所能实现的。当然,我在简化这个过程,但本质上,您在本地使用的 Docker 镜像有可能与生产环境中使用的镜像相同。
Docker 的主要缺点是,理解如何使用镜像和容器以及构建微服务更为复杂。如果您从未听说过微服务,这个概念可能会让您感到惊讶,因为它们与传统应用程序和部署不同。微服务是一种用于通过将应用程序分割成一系列服务来构建应用程序的架构风格。Docker Compose 可能会使在本地实现微服务变得更容易,但这仍然是一个复杂的话题。
注意
如果您对微服务主题更感兴趣,您应该查看microservices.io/
。
我真心认为探索 Docker 比其他虚拟化选项更有价值,因为它在开发者和基础设施社区中获得了如此多的流行。
虽然我们不会深入探讨 Docker,但我们可以迈出一些小步来学习如何在本地环境中使用它。第一步是通过安装程序或包管理器安装 Docker。根据您的操作系统和发行版,使用以下之一:
之后,我们可以运行以下命令来验证 Docker 是否已安装:
docker -v
这应该会返回您机器上找到的当前 Docker 版本:
Docker version 20.10.17, build 100c701
一旦 Docker 可用,我们可以下载一个 Ruby 镜像并使用这个单行命令运行 Ruby 命令:
docker run --entrypoint ruby ruby:latest -v
在这里,我们正在告诉 Docker 代理获取最新的 Ruby Docker 镜像,然后运行ruby -v
。命令应该输出类似以下内容:
Unable to find image 'ruby:latest' locally
latest: Pulling from library/ruby
cd84405c8b9e: Pull complete
a1d98e120b80: Pull complete
7cb6be5911b4: Pull complete
db608c3c3ce3: Pull complete
ef10f752bfb9: Pull complete
65032c8238ec: Pull complete
a6196a66f1a5: Pull complete
8f0e459675ce: Pull complete
Digest:
sha256:74f02cae856057841964d471f0a54a5957dec7079cfe18076c132ce5 c6b6ea37
Status: Downloaded newer image for ruby:latest
ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [aarch64-linux]
第一次运行此命令时,可能需要一段时间,因为它将不得不下载 Ruby Docker 镜像。一旦下载了 Docker 镜像,它将启动一个带有该镜像的容器并运行命令。一旦命令运行完毕,容器将被停止。
有了这些,我们已经成功使用 Docker 运行了一个 Ruby 命令。虽然实际上以更实用的方式使用 Docker 进行开发更复杂,但这是一个很好的起点。
现在我们已经看到了设置本地环境的一些选项,我们将转向 Ruby 开发的下一个基本工具,那就是 rbenv。
使用 rbenv
虽然强烈建议为团队的所有成员使用某种同质化环境(如之前提到的虚拟环境),但创建等效环境的一种更简单、更快捷的方法是使用某种 Ruby 版本管理工具。这类工具允许我们安装不同的 Ruby 版本,并且大部分情况下它们的行为相似,即使它们安装在不同的操作系统上。我们有几种选择,但为了简单起见,我们将使用 rbenv:github.com/rbenv/rbenv
。
rbenv 允许我们安装多个版本的 Ruby 并管理这些版本。通过“管理”我指的是我们可以定义整个系统使用的 Ruby 版本(全局),或者我们可以定义每个项目使用的特定版本(局部)。对于 macOS 和 Linux 用户,您应遵循之前提到的 GitHub 仓库中的说明,该仓库也作为官方网站。如果在尝试安装工具时遇到任何问题,您还可以遵循以下两个教程来安装 rbenv:
最后,对于 Windows 用户,我们有 rbenv-for-windows:github.com/ccmywish/rbenv-for-windows
。
注意,Windows 的版本可能有点有限,你可能会遇到某些 Ruby 版本的问题。
在我们安装 rbenv 之后,我们应该列出机器上安装的 Ruby 版本。让我们打开一个 shell 并运行以下命令:
rbenv versions
这应该会显示以下输出:
* system
上述输出意味着我们只安装了一个版本的 Ruby。让我们添加 Ruby 2.6.10 以用于本书的示例。我们将在 shell 中输入以下命令:
rbenv install 2.6.10
这应该会抛出以下输出:
To follow progress, use 'tail -f /var/folders/47/x761l7cd0419z6lzb_kt7yzc0000gn/T/ruby-build.20231019202743.61499.log' or pass –verbose
…
Downloading ruby-2.6.10.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.10.tar.bz2
Installing ruby-2.6.10...
…
NOTE: to activate this Ruby version as the new default, run: rbenv global 2.6.10
现在我们已经可以使用 Ruby 2.6.10,我们可以使用以下命令开始使用该版本:
rbenv local 2.6.10
在运行上述命令之后,请确保使用以下命令进行测试:
ruby --version
这应该会显示当前的 Ruby 版本为 2.6.10:
ruby 2.6.10p210 (2022-04-12 revision 67958) [arm64-darwin22]
输出可能会因系统而异,但版本应该相同。我不会过多地讨论 rbenv 的工作原理,但我会说,通过在文件夹上运行此命令,你在此文件夹(或此文件夹内的子文件夹)中进行的任何工作都将使用 Ruby 2.6.10。如果我们移动到不同的文件夹,可用的 Ruby 版本将不同。为每个项目设置 Ruby 版本而不是仅仅依赖计算机上安装的版本是一个重要的最佳实践。切换 Ruby 版本现在就像安装所需的版本并使用上述命令应用该版本一样简单。对于本书的练习,我强烈建议依赖 rbenv。
作为 rbenv 的替代方案,我们有 rvm:rvm.io/
。
然而,我强烈建议选择 rbenv 而不是 rvm,因为大多数开发者使用 rbenv。如果你愿意接受挑战,可以尝试使用 rvm。
摘要
到目前为止,我们已经学习了如何在 macOS、Windows 和 Linux 系统上安装 Ruby。虽然某些操作系统可能已经预装了 Ruby,但它几乎总是过时的。我们应该始终安装 Ruby 的最新版本,因为安装 Ruby 是我们作为 Ruby 开发者自身开发过程的一部分。
我们还学习了关于虚拟化的知识,包括 VMware、VirtualBox 和 Docker。我想指出,关于虚拟化技术的理论可能对初学者开发者来说有点令人难以承受——我本人刚开始学习 Ruby 时并没有使用过这些虚拟技术,主要是因为当时这并不是一个实用的选择,而且它们在那个时期还没有变得如此流行——但现在我可以自信地说,如果我当时有那些资源,将会节省我很多麻烦。话虽如此,我可以保证,它们将会非常有用(至少是 Docker),但开始用 Ruby 编程它们并不是必需的。最后,我们还学习了 rbenv 的基本命令以及为什么它已经成为 Ruby 社区中首选的 Ruby 版本管理器。
接下来,我们将进入下一章,现在我们已经准备好开始自己编写一些 Ruby 代码了。我们还将对比 PHP 和 Ruby 的语法,分析如何利用我们从 PHP 中已有的知识并将其迁移到 Ruby。最后,我们将了解 Ruby 语言中 PHP 所不具备但非常适合 Ruby 世界的增强功能。
第三章:将基本 Ruby 语法与 PHP 比较
在 PHP 和 Ruby 中运行脚本相似,尽管每种语言都有其独特之处。同样,Ruby 和 PHP 的语法有时也会出奇地相似,正如我们在第一章中看到的。然而,如果我们认真想要成为 Ruby 程序员,我们就需要学习 Ruby 带来的差异和增强。
让我们开始这段旅程,利用我们从知道 PHP 获得的资源来创建、执行和调试我们自己的 Ruby 脚本,并看看 Ruby 语言相对于其他语言的改进。我们不仅会以 Ruby 的思维方式思考,还会以“Ruby 方式”编程。
因此,在本章中,我们将涵盖以下主题:
-
从命令行运行 Ruby 代码
-
探索变量类型
-
使用条件语句
-
使用循环重复代码
-
使用 Ruby 语言增强功能
技术要求
要跟随本章内容,我们需要以下内容:
-
任何 IDE 来查看/编辑代码(例如,SublimeText,Visual Studio Code,Notepad++ Vim,Emacs 等)
-
对于 macOS 用户,您还需要安装 Xcode 命令行工具
-
Ruby 版本 2.6 或更高版本需要安装并准备好使用
本章中展示的代码可在github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails/
找到。
从命令行运行 Ruby 代码
在学习 Ruby 时,我们需要了解的第一件事之一是如何在我们的屏幕上直接运行我们的代码并查看输出。有不同方法可以实现这一点,但我们将以最简单的方式来做。虽然从命令行加载代码有多种方式,但我们将从一个文件开始。
运行一个简单的代码文件
正如我在引言中提到的,在 Ruby 中运行脚本简单且容易。与在 PHP 中运行脚本类似,我们可以创建一个文件,向其中添加 Ruby 代码,然后用 Ruby 执行它。运行或执行代码简单来说就是 Ruby 将读取(也称为解析)我们的源代码,然后将其翻译成计算机可以理解和处理的语言。
让我们从创建一个名为 ruby_syntax
的文件夹开始,这个文件夹位于我们的桌面上。在那个文件夹中,创建我们的源代码文件,命名为 running_ruby.rb
,使用您选择的 IDE。
现在,让我们向我们的文件中添加一些代码:
# running_ruby.rb
print('I am running a Ruby script');
现在,让我们打开一个 shell 并转到我们刚刚创建的相同文件夹:
cd path-to-our-desktop/ruby_syntax
一旦我们在 shell 中的这个文件夹里,我们可以用 Ruby 运行我们刚刚创建的脚本:
ruby running_ruby.rb
这应该会输出以下内容:
I am running a Ruby script
正如我在第一章中提到的,这种语法与 PHP 的语法非常相似。如果我们比较两者,我们会得到以下 PHP 等价代码:
<?php # running_php.php
print('I am running a PHP script');
我们将按照与 Ruby 一样的方式运行示例,但使用 PHP 可执行文件,如下所示:
php running_php.php
结果将与 Ruby 一样,但字符串为 PHP。
回到 Ruby 的例子,就像我们在 第一章 中的例子一样,让我们修改我们的 Ruby 代码,使其稍微易于阅读:
# running_ruby.rb
print 'I am running a Ruby script without parenthesis'
尽管语法略有相似,但结果相同,而且它的优点是读起来更自然。虽然 Ruby 中有 print
用于向用户输出文本,但你也可以使用 puts
或简单地 p
。你会在 Ruby 中经常看到这一点。
使用 load
方法加载源代码文件
到目前为止,你已经知道了如何在单个文件中执行 Ruby 代码。然而,随着源代码的增长,拥有一个单独的源代码文件将变得不切实际。这就是为什么 Ruby 允许我们从其他源文件加载源代码。
让我们看看如何做到这一点。首先,我们必须创建一个要加载的文件——在这个例子中,文件名为 my_library.rb
,并包含一些简单的内容:
# my_library.rb
print 'I am a library.'
虽然我们现在有了可运行的 Ruby 代码,但我们不会直接运行 my_library.rb
,而是让另一个脚本加载其代码。这就是 load
方法的作用。load
方法接受一个文件名及其代码,并将其包含在我们的执行中。所以,让我们创建一个名为 load_library.rb
的文件,并包含以下内容:
# load_library.rb
load 'my_library.rb'
现在,使用以下命令运行代码:
ruby load_library.rb
脚本的输出应该是这样的:
I am a library.
load_library.rb
文件注入(或者正如其名称所暗示的,加载)了 my_library.rb
文件中的代码并执行了它。这样,我们可以轻松地将大块代码分成更小、更易读的部分。
现在,如果我们多次在 Ruby 中调用 load
方法会发生什么?让我们试试看:
# load_library.rb
load 'my_library.rb'
load 'my_library.rb'
再次运行命令后,输出如下:
I am a library. I am a library.
从这种行为中,我们可以推断出每次我们加载一个文件,其代码都会被执行,这在我们的文件在执行期间多次更改时非常有用——在这些情况下,代码被认为是动态更改的。
作为动态更改代码的例子,假设我们需要一个脚本来包含代码的新部分,但又不能因此停止。在这种情况下,我们需要代码不断刷新,这就是 load
方法的作用。每次 load
方法遇到正在更改的文件时,它都会刷新代码——也就是说,每次 Ruby 引擎检测到更改时,它都会更新我们的代码。否则,每次我们需要代码中的新更改时,我们都需要停止执行。
Ruby 的 load
方法类似于 PHP 的 include
和 require
函数。然而,Ruby 的 require
方法与 PHP 中的略有不同,正如我们接下来将要看到的。
使用 require
方法加载源代码文件
与 load
方法相反,有时我们只需要代码执行一次,为此我们可以使用 require
方法。
让我们通过创建一个名为 require_library.rb
的文件并包含以下内容来实际看看:
# require_library.rb
require './my_library.rb'
现在,让我们以与运行 load_library.rb
脚本相同的方式运行它:
ruby require_library.rb
初始输出将与我们运行load_library.rb
脚本时的输出相同:
I am a library.
然而,请注意,在这个例子中,我们在文件名前包含了点斜杠前缀(./
)。require
函数需要文件的绝对路径或相对路径——因为my_library.rb
示例位于同一文件夹中,我们使用当前文件夹的相对路径,即./
。简单来说,require_library.rb
的源代码将读取为“注入位于同一文件夹中的my_library.rb
文件中的代码。”
现在,让我们尝试多次调用require
方法:
# require_library.rb
require './my_library.rb'
require './my_library.rb'
让我们再次用以下代码运行它:
ruby require_library.rb
对于经验丰富的 PHP 开发者来说,输出如下应该不会令人惊讶:
I am a library.
Ruby 的 require
方法几乎与 PHP 中的 include_once
和 require_once
一样工作。Ruby 解释器只加载代码一次,无论你何时尝试再次加载它,引擎都会注意到你已经加载了那段代码,因此不会再次加载,节省内存和资源。
使用require
方法时,我们可能还想考虑的一件事是,不强制包含文件扩展名,所以这也适用:
# require_library.rb
require './my_library'
通过不使用文件扩展名,我们的代码看起来稍微干净一些。现在,你可能想知道require
方法有什么用?好吧,无论何时你正在编写将在代码的多个部分中使用的库,你不必担心多次加载你的库,因为require
方法只会加载一次,从而节省内存资源。我们现在保持简单,但如果你对导入代码的更多选项感兴趣,还有require_relative
方法:apidock.com/ruby/Kernel/require_relative
。
Ruby 类和模块
在 Ruby 中,模块是一种将代码分组以便重用的方式。具体来说,模块是方法集合和常量的集合,可以注入到类中。模块本身并没有什么用,因为不能以独立的方式使用它们。我们使用模块向类添加功能。
类,正如你可能知道的,是从现实世界抽象到编程世界的蓝图。一个类可以有不同的方法来表示动作,以及属性来表示值。一个类可以通过包含来自模块的额外方法和常量来增强。所以,简单来说,一个类可以附加来自模块的方法和常量——这正是include
方法所做的事情。
让我们通过一个简化的例子来看看。创建一个名为include_module.rb
的文件,并包含以下代码:
# include_module.rb
class MyClass end
这里,我们创建了一个简单的空类MyClass
,没有方法。现在,让我们创建这个类的实例:
my_class_instance = MyClass.new
由于这个类是空的,我们无法用它做很多事情,所以让我们向它添加一个Utilities
模块:
module Utilities end
就像之前的类(MyClass
)一样,这只是一个空模块。让我们给我们的模块添加一个名为debug
的方法,这样我们的类和模块最终看起来就像这样:
# include_module.rb
class MyClass end
module Utilities
def debug
puts 'We are debugging'
end
end
my_class_instance = MyClass.new
使用这段代码,我们声明了一个名为debug
的方法,它会打印文本“我们正在调试。”我们能将这个方法添加到MyClass
中吗?嗯,这就是include
方法发挥作用的地方。
包含方法
到目前为止,我们应该将这个新构建的调试方法与include
方法结合起来,因为现在,debug
方法尚未被使用。它仅仅是被定义了。通过include
方法,我们现在可以通过在文件末尾添加以下内容来“附加”debug
方法到我们的类中:
MyClass.include(Utilities)
这仅仅意味着debug
方法现在对my_class_instance
可用。让我们从我们的类实例中调用新添加的debug
方法。您的最终代码应该看起来像这样:
# include_module.rb
class MyClass end
module Utilities
def debug
puts 'We are debugging'
end
end
my_class_instance = MyClass.new
MyClass.include(Utilities)
my_class_instance.debug
现在,到了关键时刻——让我们运行它:
ruby require_library.rb
这将输出以下内容:
We are debugging
这意味着我们已经成功地将调试方法附加到我们的空类中。每当我们需要在整个代码中重复使用的模块时,这会非常有用。
PHP 中与 Ruby 的include
方法等效的资源称为traits
。如果您对这个主题感兴趣,请查看www.php.net/manual/en/language.oop5.traits.php
。由于我们迄今为止还没有在 Ruby 中查看面向对象编程,这可能会一开始有点难以理解,但请不要担心您现在还没有完全理解它——我们将会达到那个水平。
交互式 Ruby Shell (IRB)
有时候,您可能想快速测试一小段代码,并且创建文件、添加代码然后运行它可能看起来很麻烦。也许我们只想测试一行代码的语法。好吧,我们不需要创建一个文件来测试一行代码。就像在其他语言中一样,例如 Python 或 PHP,Ruby 中有一个专门为此的工具:它就是交互式 Ruby Shell,也被称为IRB。让我们看看它。
在 shell 中运行以下命令:
irb
这将使您的 shell 看起来像这样:
irb(main):001:0>
这个 shell 作为一个实时 Ruby 解释器工作——也就是说,在>
符号之后,我们可以开始输入 Ruby 命令,这些命令将被立即解释和执行。作为一个简单的例子,让我们加 1 和 1:
irb(main):001:0> 1+1
这将返回以下结果:
irb(main):001:0> 1+1
=> 2
当您想快速测试语法和操作,或者甚至查看类的内容时,这非常有用。它的工作方式与我们在前面的例子中使用过的 Ruby 二进制文件相同。
所以,作为一个最后的例子,让我们加载我们创建的include_module
文件,但现在使用这个交互式 shell:
irb(main):001:0> require './include_module'
We are debugging
=> true
现在我们已经使用require
方法将代码包含到我们的 shell 中,我们可以在irb
会话中使用这个加载的代码。由于我们有MyClass
的定义可用,我们可以将其用作蓝图来创建MyClass
的一个实例:
irb(main):001:0> another_instance = MyClass.new
这将返回一个 Ruby 内部使用的唯一标识符,以确定实例在计算机内存中的位置:
=> #<MyClass:0x000000014a17ed50>
注意,我们也可以在我们的新创建的实例上调用debug
方法:
irb(main):001:0> another_instance.debug
就像之前一样,我们得到了相同的输出:
We are debugging
=> nil
要退出这个交互式 shell,只需输入exit
,使你的 shell 恢复正常。如果你想知道,PHP 中也有一个类似的 shell,称为交互式 shell。如果你对这个主题感兴趣,请务必查看这个页面:www.php.net/manual/en/features.commandline.interactive.php
。
到目前为止,我们可以自豪地说,我们现在知道如何以几种不同的方式运行 Ruby 代码。我们还知道如何使用单个源代码文件,或者从单独的源代码文件中加载代码。无论哪种方式,无论是使用 Ruby 二进制文件还是使用交互式 shell 运行你的代码,你都需要一种方式来存储和使用值,这把我们带到了下一个主题:变量。
探索变量类型
Ruby 中的变量与其他编程语言中的变量具有相同的效用:它们是用于存储值的可变容器。简单来说,变量用于保存值以供以后使用。这些值可能会随时间变化,甚至改变它们包含的数据类型。
就像 PHP 一样,Ruby 是动态类型的(或称为鸭子类型),这意味着解释器在运行时会推断我们正在处理的数据类型。我们不需要告诉 Ruby 或 PHP 一个变量是字符串、数字还是布尔值。然而,与 PHP 的一个区别是,在 PHP 的后续版本中,你可以指定要使用的数据类型,尤其是在面向对象的 PHP 中。然而,即使有这种“增强”,语言的大部分仍然是鸭子类型。
这对我们作为开发者有何影响?好吧,让我们看看一个简单的例子。首先,打开一个 IRS。输入以下命令:
irb
正如我们之前看到的,一旦我们输入这个命令,shell 将看起来像这样:
irb(main):001:0>
现在,输入以下声明:
name = "Oscar"
age = 35
is_married = true
books_read_this_week = 2.5
注意,尽管前一个代码块中的信息是你应该输入的内容,但在提示符中它看起来是这样的:
irb(main):001:0> name = "Oscar"
=> "Oscar"
irb(main):002:0> age = 35
=> 35
irb(main):003:0> is_married = true
=> true
irb(main):004:0>books_read_this_week = 2.5
=> 2.5
Ruby 语言的一个核心特性是,每行代码,Ruby 都会尝试返回一个值。如果你声明一个变量,Ruby 将返回分配的值。所以,当我们添加name
变量时,Ruby 返回了变量的值——即"Oscar"
。当我们使用这种行为来获取变量所持有的数据类型时,这很有用。Ruby“知道”这个name
变量包含的数据类型。
要做到这一点,我们可以使用内部的class
方法。只需输入name.class
;irb
应该返回类似以下的内容:
irb(main):005:0> name.class
=> String
Ruby 解释器确定name
变量是一个字符串,或者简单地说,是文本。同样的方法也可以用于我们刚刚声明的其他变量,例如age
变量:
irb(main):006:0> age.class
=> Integer
同样地,is_married
变量是一个布尔变量。我们可以通过获取该变量的类来确认这一点:
irb(main):006:0> is_married.class
=> TrueClass
这意味着is_married
变量有一个布尔值true
。我们可以用同样的方法处理books_read_this_week
变量:
irb(main):006:0> books_read_this_week.class
=> Float
我们没有明确告诉 Ruby 我们将使用什么类型的变量,但 Ruby“自动”知道了这一点。这意味着我们不必担心在运行时告诉 Ruby 我们将使用的数据类型。这在大多数情况下是实用的,但我必须承认,我确实遇到过在运行前知道数据类型会很有帮助的情况。
到目前为止,在我们的前几个例子中,我们已经查看了 Ruby 中的四种不同类型的变量:字符串、整数、布尔值(true 或 false)和浮点值。然而,还有三种其他类型的变量我们应该深入了解:数组、哈希和符号。我们现在将逐一介绍它们。
数组
数组是一种将常见变量分组的方法。从 PHP 背景来看,数组在概念和语法上都可以很容易理解。从概念上讲,我们将相似或相关的值组合在一起。一个例子可能是将一个人的电话号码组合在一个变量中。另一个例子可能是通过保存街道、门牌号、城市、邮编等来存储物理地址,所有这些都在同一个变量中。正如我们接下来将要看到的,Ruby 的语法与 PHP 非常相似。
数组允许我们将相关的值保存在一个单独的变量中。例如,假设我们想要保存我兄弟姐妹的年龄。我们可以创建一个表示我兄弟姐妹年龄的整数数组。让我们在 PHP 和 Ruby 中这样做,并比较两种语法。这将 PHP 语法:
$siblings_ages = [ 42, 31, 25 ];
而这将 Ruby 语法:
siblings_ages = [ 42, 31, 25 ]
除了$
和;
之外,两段代码是相同的;这表明,如果你有 PHP 背景,理解 Ruby 数组的概念不应该很难。虽然还有其他声明数组的方法,但我们现在将保持语法简单。
现在我们已经了解了如何声明数组,让我们看看它们的实际用途。
Ruby 中的数组可以包含其他类型的值(不仅仅是数字)。假设我想列出某人会演奏的乐器。我们可以创建一个字符串列表并命名为instruments_played
:
instruments_played = ["guitar", "drums", "bass", "ukulele"]
如您所见,我们将相关的值(在这种情况下是乐器)组合在一个单独的变量中。我们创建了一个列表,可以在我们的代码中使用。这种类型的数组,正如它所声明的,有一个内部计数器来引用每个值。这个内部计数器从 0 开始,所以第一个值(guitar
)将包含在instruments_played[0]
中,第二个值(drums
)将包含在instruments_played[1]
中,以此类推。
如果我们想在屏幕上打印出所有的乐器,我们可以逐个打印每个乐器:
puts instruments_played[0]
puts instruments_played[1]
puts instruments_played[2]
puts instruments_played[3]
然而,这样做既繁琐又不实用,正如你可能已经猜到的,我们有一个更好的编程方式来做这件事。我们不是逐个计数,而是可以使用 do
语句遍历数组的所有值:
for i in 0…4 do
puts instruments_played[i]
end
三点符号 (…
) 可能对来自 PHP 的人来说既新又奇怪,但如果你只是阅读代码,它几乎是有道理的。这种符号被称为范围,你会在 Ruby 中经常看到它的使用。这个符号创建了一个计数器,从 0 开始,到小于 4 的值(在这个例子中是 3),然后每次增加 1。然后这个计数器被分配给 i
变量。这个例子可以读作,“对于 i
变量,创建一个从 0 开始,到 3 结束,每次增加 1,并逐个打印数组值的循环。” 输出将看起来像这样:
guitar
drums
bass
ukulele
=> 0...4
你注意到 …
符号排除了数字 4,相当于数学符号中的 [0,4] 吗?那么,如果我们想使范围包含最后一个数字——例如,[0,3]——会发生什么呢?Ruby 也有一个两点符号 (..
),它确实包含其范围内的最后一个数字。因此,我们可以将示例重写如下:
for i in 0..3 do
puts instruments_played[i]
end
输出将与上一个例子相同:
guitar
drums
bass
ukulele
=> 0..3
你可以根据需要使用三点或两点符号。如果你对范围的主题更感兴趣,我建议你查看有关此主题的文档:ruby-doc.org/core-2.5.1/Range.html
。
现在,让我们回到数组。就像 PHP 一样,Ruby 也有一些内部方法来处理数组。作为一个并行设计的例子,PHP 有一个函数可以告诉我们数组的大小。这个函数叫做 count()
,在 Ruby 中有一个等效的函数叫做 size()
。只需记住,Ruby 中的所有东西都是对象,所以你不会像在 PHP 中那样使用 size(instruments_played)
。相反,为了打印我们数组中的元素数量,我们会调用 size()
方法作为 instruments_played
数组的方法:
puts instruments_played.size
由于数组有四个元素,我们会得到一个输出为 4
。此外,还有一个做同样事情的方法叫做 length
。
我发现另外两个内部方法极其有用,它们是 first
和 last
。这些方法(如我们从它们的名字中推断出的)允许我们分别获取数组的第一个和最后一个元素。让我们尝试使用一些变量插值来使用 first
方法:
puts "I learned how to play the #{instruments_played.first} first"
这将输出以下内容:
I learned how to play the guitar first
last
方法以相同的方式工作:
puts "I learned how to play the #{instruments_played.last} last"
这将按预期输出:
I learned how to play the ukulele last
如你所见,我们已经将字符串和变量的内容结合起来创建了一个新的字符串。这种组合被称为变量插值。
变量插值
变量插值(这是一个你在编程中会经常听到的术语)涉及用变量的值替换变量。当打印消息和/或向用户展示数据时,这非常实用。当正确使用时,变量插值允许我们在字符串中嵌入变量值。让我们看看我们之前的示例中的代码:
instruments_played = ["guitar", "drums", "bass", "ukulele"]
puts "I learned how to play the #{instruments_played.first} first"
字符串插值功能有几个层次,所以让我们分部分分析它。
首先,在代码的第二行,我们可以看到,在字符串内部,我们添加了一个特殊的块,它以#
符号开头,后跟一系列花括号(#{ }
)。当在字符串中使用时,这个块确定我们将使用插值。
其次,花括号内的所有内容都将被解释并返回。在这个例子中,花括号内的代码是instruments_played.last
,它包含数组的最后一个元素。这个数组的最后一个元素将作为字符串的一部分返回,从而完成插值。这仅当字符串用双引号定义时才有效。
组合数组类型
到目前为止,我们看到了包含相同类型数据的数组——我们有一个仅由字符串组成的数组,另一个仅由整数组成的数组。但关于 Ruby 数组,还有一个值得注意的最后一个特性,即它们可以在同一个数组中组合不同的变量类型。作为一个随机示例,让我们向一个数组添加一些无关的值:
random_values = [25, "drums", false, 3.8]
在这个数组中,我们正在将不同类型的数据(整数、字符串、布尔值和浮点数)组合成一个单一数组。并非所有语言都支持数组内的这种行为,但 Ruby 和 PHP 都支持。
正如我们在开始查看变量类型时提到的,Ruby 是动态类型的。动态类型语言的一个特性是数组可以组合它们所持有的数据类型。相比之下,在强类型语言(如 Java)中,数组被迫在每个元素上具有相同类型的数据——也就是说,你只能有一个整数数组或只能有一个字符串数组。这并不是说 Ruby 比 Java 好,或者一般来说,强类型语言比动态类型语言好。它们只是有不同的设计。
如果你对这个主题的数组感兴趣,请查看官方 Ruby 文档:ruby-doc.org/core-2.7.0/Array.html
。
Hashes
现在,让我们看看另一种 PHP 开发者也会很容易理解的变量类型:hashes。hash 是一个数组,但主要区别在于它有文本索引而不是数字索引。hashes 在行为上与数组非常相似,但不同之处在于我们使用字符串来引用某些值。在 PHP 中,这些被称为关联数组。
让我们看看一个实际例子。这里,我们有一个索引为英语、值为西班牙语的 hash:
numbers = { "one" => "uno", "two" => "dos", "three" => "tres" }
类似于我们可以用数组做的,要获取单个索引的值,我们可以输入以下内容:
puts numbers["one"]
我们会得到以下输出:
uno
如您所见,索引是一个字符串,对人类来说更易读。正是在这种可读性上,哈希表可以派上用场。让我们重写我们在探索变量类型部分开始时使用的第一个例子,使其使用哈希表:
person = { "name" => "Oscar", "age" => 35, "is_married" => true, "books_read_this_week" => 2.5 }
我们不需要为name
、age
、is_married
和books_read_this_week
分别设置单独的变量,我们可以用一个哈希表将这些值组合成一个名为person
的单个变量。现在,我们可以像以下这样引用每个索引:
person["name"]
person["age"]
person["is_married"]
person["books_read_this_week"]
此外,我们还可以使用以下方式打印一个非常易读的消息:
puts "#{person["name"]} is #{person["age"]} years old"
即使对于一个刚开始学习 Ruby 的开发者来说,这不仅是可读的,而且从代码的意图来看也是可理解的。正如预期的那样,它会输出以下内容:
Oscar is 35 years old
当处理需要由人类读取的映射数据时,哈希表非常有用。当你处理正在变化的数据时,它也很有用。
这把我们带到了我们将要看到的最后一种变量类型,我必须说它比我希望的要复杂。我们迄今为止看到的变量类型是可变的,这意味着它们可以被更改。然而,有时我们不需要某些值改变——我们需要一个存储这个值的位置。符号正是这样做的。
符号
符号是高度优化的标识符,它们将不可变的字符串映射到固定的内部值。它们本身也是不可变的字符串——也就是说,它们的值不会改变。
这个概念有点复杂,但我相信通过一个例子会更容易理解。让我们用一个简单的字符串来看看它的值指向什么。所以,运行以下代码:
"name".object_id
当你创建一个字符串时,Ruby 会将在内存中某个地方保存字符串对象。object_id
方法保存这个内部唯一标识符。注意当你多次调用同一行时会发生什么:
irb(main):126:0> "name".object_id
=> 2391267760
irb(main):127:0> "name".object_id
=> 2391332180
irb(main):128:0> "name".object_id
=> 2391359800
第一个字符串指向的地址与第二个和第三个字符串不同。所以,每次我们输入name
字符串时,Ruby 都会创建并存储一个新的字符串。即使它们的值相同,它们仍然是不同的。作为一个类比,这就像有相同名称但位于不同文件夹的文件。即使文件有相同的内容,它们仍然是不同的文件。
这与符号不同。符号指的是相同的内存位置。让我们用符号尝试相同的例子:
:name.object_id
这也应该返回一个唯一的标识符号码。然而,当我们多次调用相同的代码时会发生什么呢?
Irb(main):129:0> :name.object_id
=> 88028
irb(main):130:0> :name.object_id
=> 88028
irb(main):131:0> :name.object_id
=> 88028
与返回不同的随机数不同,这次我们得到的是相同的(尽管仍然是随机的)数字。这是因为每次我们调用 :name
时,Ruby 解释器都在查看内存中的同一位置。使用相同的类比,这就像创建一个唯一的文件,然后每次我们需要该文件时,我们都会创建指向原始文件的链接。所以,即使链接在不同的文件夹中,它们也会指向同一个文件。
我们现在不会深入探讨这个主题,因为只需要理解其基本知识就足够了,但我们在未来的章节中会看到更多示例,特别是 Ruby on Rails 的示例。目前,只需记住这个好的经验法则:当对象的身份很重要时使用符号。如果内容更重要,则使用字符串。
如果你现在想了解更多关于符号的信息,以下网站是一个很好的资源:medium.com/@lcriswell/ruby-symbols-vs-strings-248842529fd9
。
到目前为止,我们已经学习了所有关于变量的知识。但如果我们不能对变量做出决策,变量有什么用呢?这是我们下一个话题。
使用条件语句
现在我们已经知道了在 Ruby 中可以使用哪些类型的变量,让我们给这些变量一些更实用的用途。
if 语句
到现在为止,我们都应该熟悉 if
语句及其结构:如果句子为真,代码应该执行或返回某些内容。
让我们以前面章节中使用的 person 哈希作为基础:
person = { "name" => "Oscar", "age" => 35, "is_married" => true, "books_read_this_week" => 2.5 }
使用它,我们可以创建一个基本的 if
语句:
if person["is_married"] == true
puts "Person is married"
end
这基本上是自我解释的。这会读作:“如果 person["married"]
中的值等于 true,那么打印 Person is married
。” end
关键字限制了 if
语句何时完成——也就是说,end
关键字之后的内容不是代码块的一部分。你会在 Ruby 中经常看到 end
关键字——只需记住,它用于界定特定的代码块。
虽然前面的代码很有用,但还有更好的写法——即“Ruby 方式”。首先,我们移除 == true
,如果我们只打算执行一个动作,我们可以将其写在一行中:
puts "Person is married" if person["is_married"]
这读起来就像一个句子,你会在很多 Ruby 程序员中使用这个有用的单行代码。
if-else 语句
如果在值不为 true 的情况下需要不同的操作,那么你应该使用 if-else
结构:
if person["age"] < 31
puts "This person is under 30"
else
puts "This person is over 30"
end
这将输出以下内容:
This person is over 30
只需记住,if
语句评估条件。如果条件为真,Ruby 执行下一行的代码。然而,如果条件为假,Ruby 跳过第一个代码块,转到 else
语句,并执行 else
关键字之后的代码。
三元运算符
作为 if
语句的最后一个例子,我们还有三元运算符,它在其他编程语言中也有;尽管它不是那么易读,但仍然很有用。让我们看一个例子:
over_or_under = person["age"] > 31 ? "over" : "under"
puts "This person is #{over_or_under} 30"
使用三元运算符时,将在=
符号和?
符号之间的条件进行评估。如果条件被认为是真的,则返回:
符号左侧的值。如果条件被认为是假的,则返回:
符号右侧的值。在这种情况下,存储在person["age"]
中的值是 35。由于 35 大于 30,over
字符串将被存储在over_or_under
变量中。第二行代码将简单地插值这个值并应该返回以下内容:
This person is over 30
虽然这不如之前的if
语句可读,但代码仍然是有效的,并且在大多数编程语言中都是可用的。三元运算符在 PHP 中也是一样的,当你需要存储一个依赖于条件的值时很有用。
if
语句可能是编程中最常用的资源之一,因此了解其语法、用法以及它解决的不同用例是个好主意。现在,让我们看看另一个使用真/假值来运行代码的资源。
使用循环重复代码
我们来到了下一个主题,即循环。Ruby 与其他语言一样,有不同的方式来重复执行相同的代码。当我们讨论数组时,特别是包含乐器名称的数组,我们看到了一个for
循环的例子,它被用来打印数组中包含的每个乐器。但让我们看看另一种更常用的循环类型:while
循环。
while
循环让我们可以重复执行由真/假条件确定的代码。假设我们想要打印一个从一到三的数字。我们可以创建一个print
语句,并简单地重复它三次,同时增加值。但是,让我们尝试一种更简洁的方法。首先创建一个counter
变量:
counter = 1
现在,我们可以开始while
循环周期:
while counter <= 3
puts counter
counter++
end
这可能看起来像是有效的代码,但我们将从 Ruby 解释器得到一个错误:
Traceback (most recent call last):
3: from /usr/bin/irb:23:in `<main>'
2: from /usr/bin/irb:23:in `load'
1: from /Library/Ruby/Gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
SyntaxError ((irb):205: syntax error, unexpected end)
这是因为大多数 Ruby 新手都会犯的一个常见错误假设。如果你使用 PHP 或 JavaScript,你会习惯于++
运算符,它相当于给变量加 1。这就像写以下内容:
counter = counter + 1
然而,Ruby 中不存在++
运算符(这也适用于––
运算符,它将值减 1)。因此,我们不得不用重写我们的代码,使其看起来像这样:
while counter <= 3
puts counter
counter += 1
end
这将输出以下内容:
1
2
3
=> nil
while
语句的结构与if
语句非常相似,但它不是只执行一行代码,而是评估条件,如果条件仍然满足,则执行代码。
在这种情况下,在进入循环之前,counter
变量的值为 1
- 因为继续循环的条件是值小于或等于 3
,条件得到了满足。由于条件得到了满足,Ruby 将执行 end
关键字之前的代码,所以将打印数字 1
并将 1
添加到 counter
变量中。由于这是一个循环,Ruby 将再次读取条件,但这次 counter
的值为 2
,所以它将再次执行整个块。一旦 counter
的值达到 4
,Ruby 将确定 while
条件不再满足,并且将中断循环,而不再执行其内部的代码。
当我们与数组一起工作时,循环的一个更有用的例子是。我们已经看到了使用计数器遍历数组的一种方法,但我们还有一个名为 each
的方法来遍历数组的每个元素。让我们再次使用 instruments_played
数组:
instruments_played = ["guitar", "drums", "bass", "ukulele"]
我们可以使用 each
来遍历每个元素,如下所示:
instruments_played.each do |instrument|
puts instrument
end
这段代码将遍历数组,这样我们就不必为该数组的每个元素重复代码。这正是循环的作用:编写更少的代码。对于数组中的每个元素,Ruby 将打印该元素的值(我们为了可读性将其称为 instrument
,并且仅在这个示例中如此,但我们可以将其命名为任何我们想要的)。
此外,我们可以通过使用花括号将此代码压缩成一行,如下所示:
instruments_played.each { |instrument| puts instrument }
这将产生与上一个示例相同的输出,但如您所见,它更简洁且易于阅读。此外,each
循环帮助我们编写能够适应内容大小的代码。如果我们向数组中添加另一个元素,我们就不必修改循环中的任何内容来打印添加的乐器。循环将自动完成此操作。
最后,当我们想要访问数组的索引时会发生什么?我们有一个专门为此而存在的方法。each_with_index
方法将使数组的索引可用,正如您在这个示例中可以看到的那样:
instruments_played.each_with_index do |instrument, index|
puts "#{index}: #{instrument}"
end
此代码将输出以下内容:
0: guitar
1: drums
2: bass
3: ukulele
=> ["guitar", "drums", "bass", "ukulele"]
再次强调,instrument
和 index
都是别名 - 我们可以将其命名为任何我们想要的 - 但我们键入它们的顺序将决定哪个值将被存储在其中。数组元素的值将存储在第一个变量(instrument
)中,而数组计数器将存储在第二个变量(index
)中。
我们完全可以像这样重写示例,并且仍然得到相同的输出:
instruments_played.each_with_index do |array_element, array_index|
puts "#{array_index}: #{array_element}"
end
新的代码将产生与之前相同的输出,但这次我们将 instrument
和其 index
重命名为 array_element
和 array_index
,这仅仅是我个人为了使代码对我更有意义而做出的选择。这表明,作为程序员,我们决定如何命名变量以保护可读性(请相信我 - 随着你作为程序员的成长,你将花更多的时间尝试命名变量)。
到目前为止,我们知道如何通过使用循环和遍历数组来重复代码。我们不是多次编写相同的代码,而是利用 Ruby 的while
语句和each
方法来提高代码的效率和可读性。但我们还没有完成。Ruby 还有一些其他的技巧来进一步提高可读性。我们将在下一节中查看这些技巧。
使用 Ruby 语言增强功能
对于我们大多数开发者来说,我们应该始终努力提高代码的可读性,因为这将在长远上帮助到每个人。我遇到过一些情况,当我回顾自己的代码时,很难理解代码在做什么。这意味着我的代码写得不好。想象一下,这种写得不好的代码可能会对下一个使用它或,更糟糕的是,改进它的开发者或团队造成多大的负担。相比之下,如果我的代码写得很好,我们就不会遇到这个问题。我想说的是:请编写可读的代码,我无法强调得更多,Ruby 开发者为了使代码可读而付出的努力,超过了对代码的任何其他增强。Ruby 附带了一些额外的工具来实现这一点。
unless
语句
这些选项之一是一个名为unless
语句的语言增强功能。unless
语句是一个否定if
语句——也就是说,它只会在条件不满足时执行代码。让我们看看它在示例中的使用情况。
让我们假设以下场景:我们有一个针对未婚个人的产品。为了简单起见,如果这个人是未婚的,我们将只打印出“单身促销”的消息。让我们尝试编写这段代码。让我们以我们之前的人的详细信息哈希为例:
person = { "name" => "Oscar", "age" => 35, "is_married" => true, "books_read_this_week" => 2.5 }
现在,让我们将is_married
的值改为false
:
person = { "name" => "Oscar", "age" => 35, "is_married" => false, "books_read_this_week" => 2.5 }
一旦我们声明了这个哈希,我们就可以尝试打印一条消息,如果这个人是单身的话:
puts "Promo for singles" if person["is_married"] == false
因为这个人没有结婚,所以输出如下:
Promo for singles
虽然代码能正常工作,但看起来并不好。我们可以使用感叹号(!
)运算符将布尔值从true
反转到false
:
puts "Show promotion" if !person["is_married"]
虽然这段代码仍然能工作,但看起来仍然不好。让我们看看 Ruby 有哪些选项可以解决这个问题。
在大多数编程语言中,你会看到很多读起来像“if not”的句子。当然,这很难读,也违反了 Ruby 的可读性原则。为了解决这个问题,Ruby 的创造者添加了确切的句子,使其更具可读性:unless
。它的工作方式与if
语句类似,但会在条件被认为是false
时执行代码。
在这种情况下,当我们需要执行的代码只有当这个人没有结婚时,这很有帮助。所以,我们不是写一个否定条件语句(如果一个人是不结婚的),if !person["is_married"]
,我们可以将示例重写如下(除非一个人结婚):
unless person["is_married"]
puts "Show promotion"
end
看起来已经好多了,但就像if
语句一样,我们可以将其转换为单行代码:
puts "Show promotion" unless person["is_married"]
这是一个非常 Ruby 风格的句子,读起来就像它的行为一样:“除非这个人已婚,否则打印“Show promotion”。”这几乎是可读性的极致。
unless
语句非常有用,以至于今天最常用的 PHP 框架之一,名为 Laravel,已经以指令的形式借用了这个功能:laravel.com/docs/9.x/blade#if-statements
。
until
循环
就像unless
语句一样,until
循环解决了与负条件相同的问题。
与其写“while not”,这听起来很糟糕,until
语句接受一个假命题,并在条件变为真之前执行循环。
until
语句接受一个假命题,并在条件变为真之前执行循环。让我们再次看看我们之前的while
示例:
counter = 1
while counter <= 3
puts counter
counter += 1
end
使用until
,我们可以将其重写如下:
counter = 1
until counter > 3
puts counter
counter += 1
end
输出与while
语句相同:
1
2
3
=> nil
我们的代码将读作“打印计数器,直到计数器大于 3。”你选择使用while not
语句还是until
语句将取决于你,因为它们都看起来很易读,但 Ruby 提供了这两种语句的事实告诉我们,Ruby 的设计是为了可读性,而不仅仅是编程。
自动返回
当你在 IRB(交互式 Ruby 解释器)中工作时,你可能已经注意到,每当你输入变量时,IRB 都会输出你刚刚输入的值。即使在我们最后的until
语句示例中,shell 首先输出了三个数字,然后是一个最终的=> nil
值。如果你仔细观察其他示例,你会发现类似的行为。这是因为 Ruby 总是尝试返回一个值——无论是声明、方法还是字符串,Ruby 都会尝试自动返回一个值。
如果你还不信,让我们使用 IRB 来更明确地看到它。所以,输入以下内容:
"This is a string"
我们没有声明字符串或将其赋值给一个值;我们只是在 IRB shell 中输入了一个字符串。那么 shell 会做什么呢?它会返回我们刚刚输入的值:
=> "this is a string"
来自 PHP 背景(以及其他语言),理解“自动返回”功能对于理解更复杂的 Ruby 代码至关重要。重要的是要知道 PHP(以及大多数语言)不会这样做。在 PHP(以及其他语言)中,我们需要显式地返回值,而 Ruby 默认就会这样做。在 PHP 中,这是通过使用return
语句来实现的。话虽如此,有时你会在 Ruby 代码中遇到显式的return
语句,因为它有时会增加可读性。为了进一步理解这个特性,在接下来的几个示例中,我们将退出 IRB,继续通过创建源代码文件并运行 Ruby 来执行它们。你仍然可以在 IRB 中跟随下一个示例,但我强烈建议你用源代码文件而不是 IRB 来跟随它们。
假设我们想要创建一个在屏幕上打印消息的方法。我们可以通过创建一个名为 methods.rb
的文件来实现。这个文件将包含以下代码:
# methods.rb
def message()
return "This is a message"
end
现在,我们定义一个名为 message
的方法,它返回一个字符串。在 Ruby 中,我们使用 def
保留关键字来定义一个方法,并用 end
关键字来限制定义。
现在,让我们添加另一个名为 say()
的方法,并在该方法内部调用 message()
方法:
# methods.rb
...
def say()
message()
end
到目前为止,我们并没有做任何特别的事情——我们只是有一个方法调用了另一个方法。如果我们打开一个 shell 并执行这个脚本,它看起来就像什么都没做:
ruby methods.rb
这个脚本什么都没有输出,但在幕后,它现在有两个定义好的方法,并且可以随时使用。message
方法由于使用了 return
关键字而显式(不是自动)地返回一个字符串。这段代码看起来仍然很熟悉,但不会持续太久。
现在,让我们打印出 say()
方法的这个最后一条代码的内容:
# methods.rb
...
def say()
return message()
end
puts say()
如果我们再次运行它,我们将在屏幕上看到以下消息:
This is a message
这就是 Ruby 与 PHP 表现不同的地方。虽然你可以在 Ruby 中显式使用 return
函数,但 Ruby 不需要 return
语句,因为它已经自动作为其默认行为的一部分执行了。所以,让我们通过从 message()
和 say()
方法中移除 return
语句来试一试。你的最终代码应该看起来像这样:
# methods.rb
def message()
"This is a message"
end
def say()
message()
end
puts say()
诚然,这看起来很奇怪,尤其是对于有 PHP 背景的人来说。我的建议是,你只需尝试适应这种语法。你会在 Ruby 中非常频繁地看到它。为了更容易学习这个规则,我们可以概括地说,“Ruby 中的每一句话都会返回一个值。”有一些明显的例外,但对于所有 Ruby 语句来说,这是真的。
可选的括号
另一个奇怪但有用的语法增强是,Ruby 方法的括号完全是可选的——因此,你可以选择是否包含括号。并且就像我们迄今为止所学的每一个 Ruby 资源一样,我们应该尝试使用它来使我们的代码更容易阅读,但也应该尽量避免过度使用。过度使用这个特性可能会让我们把代码格式化成这样:
method1 method2 parmeter1, parameter2
这个片段的问题是我们不知道逗号是用来分隔 method1
或 method2
的参数的。在这种情况下,我们应该使用括号:
method1( method2( parameter1, parameter2 )
现在,很明显 method2
接收了两个参数,而 method1
只接收一个参数。让我们通过从之前的例子中移除括号来查看一个更简化的例子。我们的例子现在看起来是这样的:
# methods.rb
def message
"This is a message"
end
def say
puts message
end
say
输出与之前相同,但现在,代码看起来更像正确的句子而不是代码语法。你将看到很多类似的代码,尤其是在你开始使用 Ruby on Rails 时。由于缺少括号,Ruby 允许我们有一个方法和一个具有相同名称的变量。在这种情况下,我们有一个名为 message
的方法和一个名为 message
的变量。
这种情况,如果未进行解释,可能会在以后导致很多困惑。为了达到这个效果,让我们拿我们之前的例子稍作修改,以便更好地理解这种命名行为。首先,我们将向 say()
方法添加一个参数,使得打印的消息是动态的。这个参数将被命名为 message
:
# methods.rb
...
def say message
puts message
end
say "Now we can say anything"
在其他编程语言中,如果我们尝试运行这段代码,我们预期会出错,这就是为什么当你刚开始使用 Ruby 时,它有时会让人感到不知所措。我们故意将参数命名为 message
,这意味着我们现在有一个 message
方法和一个 message
本地变量。当我们到达 puts message
这一行时,我们不确定我们是调用带括号的 message
参数还是不带括号的 message
方法。不幸的是,这种混淆经常发生,至少在我开始更专业地使用 Ruby 时是这样的经验。
因此,我的建议是,在调用方法时尽量使用括号,即使语法不需要我们这样做。出于教学目的,我们在这个例子中不会这样做。所以,我们的最终源代码应该看起来像这样:
# methods.rb
def message
return "This is a message"
end
def say message
puts message
end
say "Now we can say anything"
正如你所预期(或不是),当在壳上执行脚本时,我们会得到以下输出:
Now we can say anything
为什么它打印了那个字符串而不是 "This is a message"
字符串呢?嗯,这是因为 message
变量优先于 message
方法。
虽然这个特性可能看起来不太美观(我并不太喜欢它),但我保证你时不时地会遇到它,你应该为此做好准备。
有疑问的感叹号方法名称
作为 Ruby 语言增强的甜点,其创造者也包含了一个命名特性,以增加我们方法的可读性:感叹号(!
)(也称为感叹号)和问号(?
)。它们在行为上没有任何改变,但允许一行代码读作一个问题或感叹。用感叹号命名的方法被称为危险方法,因为它们会修改它们被调用的对象。用问号命名的方法被称为谓词方法,并且按照惯例,返回一个布尔值。
要看到这个功能在实际中的运用,我们将使用带问号的创建一个方法。让我们创建一个名为 enhanced_naming.rb
的新文件,并添加以下代码:
# enhanced_naming.rb
$married_status = false
def is_married
$married_status
end
$married_status
变量是一个全局变量,这仅仅意味着我们可以在方法内或方法外修改或访问其内容。在这种情况下,我们定义了一个获取$married_status
值的方法。然而,知道我们可以将?
添加到这个方法的名称中,让我们将is_married
方法重命名为如下:
# enhanced_naming.rb
$married_status = false
def is_married?
$married_status
end
现在,让我们使用一个已经熟悉的单行代码来打印已婚人的消息:
puts "Promo for married people" if is_married?
虽然将?
添加到方法名称中不会影响其行为,但它确实将句子变成了一个明显的问题。我们将在 Ruby 中非常频繁地看到这种语法。
同样地,我们可以在方法名称中使用感叹号(!
)作为一部分。再次强调,仅仅将感叹号添加到名称中本身并不会影响行为,但它告诉阅读代码的人我们正在做不同于仅仅调用方法的事情。作为一个例子,让我们将我们的marry
方法重命名为marry!
并看看它看起来像什么:
def marry!
$married_status = true
end
作为 Ruby 社区采用的一种约定,感叹号(!
)将告诉代码的读者我们正在对象内部进行更改。没有感叹号的方法将简单地返回一个值,但不会影响对象本身。因此,在这种情况下,我们正在将$married_status
的值更改为true
。这就是代码现在的样子:
# enhanced_naming.rb
$married_status = false
def is_married?
$married_status
end
def marry!
$married_status = true
end
puts "Promo for married people" if is_married?
很遗憾,当我们运行这个例子时,我们没有看到任何输出。这是因为全局变量$married_status
的初始值是false
,并且我们的代码只有在值是true
时才会打印一条消息。现在,让我们调用marry!
方法,并在代码末尾再次复制单行代码:
puts "Promo for married people" if is_married?
marry!
puts "Promo for married people" if is_married?
现在,我们可以再次运行代码:
ruby enhanced.rb
输出将看起来像这样:
Promo for married people
这里发生了什么?我们有一个初始值为false
的全局变量$married_status
。然后,我们有两个方法——一个用于获取$married_status
的值,另一个将其更改为true
。最后,我们尝试打印消息,但由于初始值是false
,消息没有被打印出来。通过调用marry!
方法,我们将$married_status
更改为true
,这使得脚本的最后一条打印出消息。
Ruby 通过提高代码可读性来为编程带来语言增强。我见过写得如此漂亮的代码,这强化了不写代码注释而是让代码为自己说话的想法。一旦你开始经常使用它们,你会越来越欣赏它们,并希望所有语言都有这些增强。
摘要
在本章中,我们学习了如何使用 Ruby 二进制文件编写、执行和 require 脚本,以及如何使用 IRB 在命令行上直接执行 Ruby 代码,而无需编写源代码。
此外,我们还回顾了 Ruby 编写变量的语法、if
语句的语法以及如何遍历循环和数组。最后,我们学习了 Ruby 的一些语言增强,而 PHP 没有,这样我们就可以阅读和理解更复杂的 Ruby 代码。
现在,我们已经准备好编写 Ruby 代码来解决现实生活中的例子了。我们将在下一章开始这样做。
第四章:Ruby 脚本与 PHP 脚本
就像我们在查看 Ruby 和 PHP 语法时发现的相似之处一样,我们将更进一步,深入研究 Ruby 脚本和 PHP 脚本之间的相似性。脚本是一段代码,它将执行一个任务然后停止执行。这个任务可能是简单的,也可能是复杂的,但它不被视为一个应用程序,因为它在任务完成后就会停止,并且只执行该任务。让我们一起迈出这一步,开始编写简单的脚本,以便我们最终能够编写完整的应用程序。
考虑到这一点,在本章中,我们将涵盖以下主题:
-
有用的脚本
-
文本处理
-
文件处理
-
命令行参数
技术要求
为了跟随本章内容,我们需要以下内容:
-
任何 IDE 来查看/编辑代码(例如,SublimeText,Visual Studio Code,Notepad++ Vim,Emacs 等)
-
对于 macOS 用户,您还需要安装 XCode 命令行工具
-
必须安装并准备好使用 Ruby 版本 2.6 或更高版本
本章中展示的代码可在github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails/
找到。
超越 Hello World
在上一章中,我们学习了如何运行(或执行)Ruby 代码。然而,我们只关注了语法,而没有关注代码的有用性。我们在任何语言中编写的著名的 Hello World 脚本本身是毫无用处的,至少从实用角度来看是这样。因此,让我们开始学习如何使用一些工具来使我们的脚本变得更有用。
任何一种语言中,有一个验证当前使用编程语言版本的方法是非常有用的。一旦我们获取了版本信息,如果使用的版本不正确,我们可以停止执行。因此,我们的第一步是获取当前的 Ruby 版本。让我们创建一个名为version_verification.rb
的文件,并包含以下代码:
# version_verification.rb
puts "We are running Ruby version #{RUBY_VERSION}"
我们可以通过在 shell 中输入以下命令来运行此脚本:
ruby version_verifications.rb
它应该输出类似以下内容:
We are running Ruby version 2.6.8
在这个脚本中,我们使用RUBY_VERSION
常量来获取我们正在使用的 Ruby 当前版本,并将这个常量与一个字符串进行插值,以查看有关 Ruby 版本的整个消息。这个常量本身是没有用的,但让我们给它一些实际用途。假设我们想要与其他团队分享我们的脚本,该脚本将在不同的计算机和/或环境中使用。为了确保我们的脚本能够正常工作,我们必须为我们的脚本提供某些要求或条件。验证这些要求是否得到满足也会很有用。我们有几种方法可以实现这一点。我们可以简单地比较从RUBY_VERSION
常量获取的版本与另一个字符串,例如'2.6.8'
。这将是最直接的方法。然而,这种方法的问题是你必须在每个地方都有相同的 Ruby 版本,这很少见。我们几乎总是有版本的小幅变化。如果我们以'2.6.8'
为例,在其他系统中,我们可能会得到'2.6.5'
、'2.6.7'
,甚至'2.6.9'
。所有这些版本不仅等效,而且对于我们所要求的是有效的。所以,让我们说我们的要求是 2.6 及以上,这相当于任何高于'2.5.9'
的版本。我们可以将RUBY_VERSION
常量获取的版本分割,通过点分割其值,然后开始比较。然而,这太费事了;这正是 Ruby 发挥作用的地方。Ruby 附带一个名为stdlib
的库,其中包含一些在遇到这类问题时非常有用的实用工具。具体来说,Ruby 有一个Gem::Version
类,它将解决我们手头的这个问题。我们将在示例中包含它,但为了确保验证工作正常,我们将它与版本'3.0'
进行比较。一旦我们测试了验证,我们就可以添加正确的版本('2.6'
)。我们的代码现在看起来是这样的:
# version_verification.rb
puts "Incompatible Ruby version" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.0')
puts "We are running Ruby version #{RUBY_VERSION}"
如果我们在我们的 shell 上运行这个脚本,我们会得到以下输出:
Incompatible Ruby version
We are running Ruby version 2.6.8
我们的验证成功了,但现在的问题是,如果没有正确的 Ruby 版本,我们没有停止执行。显示 Ruby 版本的提示信息不应该显示。如果我们用 PHP 编写脚本,我们可以简单地使用die()
函数(它等同于exit()
语言结构)然后脚本就会立即停止。然而,由于我们正在编写脚本,某些实践可以使我们的脚本更加有用。如果我们的程序在网络上运行,我们会依赖 HTTP 响应状态码(developer.mozilla.org/en-US/docs/Web/HTTP/Status
)来告诉浏览器我们的页面已经渲染并且发生了错误。同样,在脚本中,我们依赖退出码(www.baeldung.com/linux/status-codes
)来告诉 shell 我们的程序失败了。考虑到这一点,我们将使用Kernel::exit()
方法来停止执行并向 shell 发送一个信号,表明我们的脚本失败了。此方法接收一个参数,然后将其发送到 shell。这个参数是一个错误码,操作系统可以使用它。我们将使用错误码1
,因为它表示一个一般错误。在做出这个调整后,我们的代码现在将如下所示:
# version_verification.rb
Kernel::exit(1) if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('3.0')
puts "We are running Ruby version #{RUBY_VERSION}"
如果我们在 shell 上运行此脚本,由于脚本在消息之前停止了执行,所以不会有输出。在基于 Unix 的系统上,在我们的脚本停止后,我们可以运行以下命令:
echo $?
这将返回1
,这与我们传递给exit()
方法的参数相同。$?
返回最后运行的命令的退出码。
Windows 用户注意事项
对于 Windows 用户,shell 的输出将根据您使用的 Windows shell 而有所不同。如果您使用的是 PowerShell,您可以通过在 PowerShell 中执行echo $LastExitCode
命令来获得相同的输出。
有关此变量的更多信息,请参阅 Windows 文档:learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-7.3
。
我们还需要对 Ruby 版本验证脚本进行最后一次调整才能使其完整。正如我之前提到的,我们只添加了版本'3.0'
以确保我们的代码能够工作,但实际上,我们希望验证安装的版本大于'2.6'
。因此,我们的最终验证将如下所示:
# version_verification.rb
Kernel::exit(1) if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('2.6')
puts "We are running Ruby version #{RUBY_VERSION}"
如果我们在 shell 上执行我们的脚本,我们会得到以下结果:
We are running Ruby version 2.6.8
通过这样,我们确保如果我们的脚本在低于 '2.6'
版本的 Ruby(例如,'2.5.7'
或 '2.2.1'
)上执行,则脚本将停止并发送错误信号。恭喜!我们已经创建了我们的第一个有用的代码片段。这种技术通常被经验丰富的 Ruby 开发者使用,他们对版本变化非常了解。现在,您可以根据需要改进这个片段,例如添加错误消息,并添加一个上限(例如,大于 '2.5.9'
但小于 '3.0'
)。现在我们已经创建了第一个真正有用的脚本,让我们看看一些其他有用的 Ruby 工具,用于处理文本。
文本处理
在您成为 Ruby 开发者的旅程中,您很可能会遇到字符串(文本),因此了解如何处理和操作这类数据非常重要。无论您需要将文本大写、获取部分字符串,甚至修剪字符串,Ruby 都提供了一套丰富的工具来按需操作文本。大多数编程语言都有这类工具,Ruby 也不例外。例如,假设我们想要获取之前输入的名字并确保所有字母都是大写或小写。Ruby 有两种方法可以做到这一点:upcase()
和 downcase()
。让我们通过创建一个名为 string_cases.rb
的文件并使用以下代码来尝试它们:
first_name = "benjamin"
last_name = "BECKER"
puts "My full name is #{first_name} #{last_name}"
到目前为止,我们已经声明了两个变量并使用插值输出了全名。假设我们在壳中运行此脚本,如下所示:
ruby string_cases.rb
输出结果如下:
My full name is benjamin BECKER.
由于我们声明名字时使用了小写字母,而姓氏使用了大写字母,所以输出结果并不令人意外。然而,将名字和姓氏的大小写分开是不合理的。因此,我们可以将它们都改为大写或小写。让我们尝试这两种解决方案。
大写方法
要将它们都转换为大写,我们可以使用 upcase()
方法。我们的代码将如下所示:
first_name = "benjamin"
last_name = "BECKER"
puts "My full name is #{first_name.upcase} #{last_name}"
如果我们在壳中再次运行此代码,我们会得到以下输出:
My full name is BENJAMIN BECKER.
小写方法
同样,我们可以使用 downcase()
方法将所有字符转换为小写。在这种情况下,我们的代码将如下所示:
first_name = "benjamin"
last_name = "BECKER"
puts "My full name is #{first_name} #{last_name.downcase}"
因此,通过这个最后的更改,如果我们运行脚本,我们会得到以下输出:
My full name is benjamin becker
如您所见,我们可以使用 upcase()
和 downcase()
方法来更改变量的大小写。然而,我们也可以直接对字符串执行相同的操作,而不仅仅是变量。为了展示这一点,让我们将代码更改为以下内容:
first_name = "benjamin"
last_name = "BECKER"
puts "My full name is #{first_name} #{last_name}".upcase
这次,我们将整个输出字符串转换为大写。输出结果如下:
MY FULL NAME IS BENJAMIN BECKER
虽然这样做很有趣,但它仅适用于学习目的。因此,让我们通过添加一个额外的方法来增加我们脚本的实用性。对于用户来说,无论是全部大写还是全部小写地读取全名都没有意义,当然也不够专业。所以,让我们只将 first_name
和 last_name
变量的首字母大写。
大写方法
我们可以使用 capitalize()
方法来实现这一点。现在,我们的代码将看起来像这样:
first_name = "benjamin"
last_name = "BECKER"
puts "My full name is #{first_name.capitalize} #{last_name.capitalize}"
如果我们运行这个示例,shell 上的输出将看起来像这样:
My full name is Benjamin Becker
注意,Ruby “知道”应该将哪些字符转换为大写,哪些字符转换为小写,以获得这个“大写”的输出。Ruby 有许多其他方法来处理和操作文本。我们本可以花掉本章的剩余时间来查看这些方法中的许多,但我更想专注于其他工具,并挑战你自己去检查这些文本方法。这些方法的文档相当清晰,而且由于这些方法是在 Ruby 哲学和最佳实践的基础上构建的,这也很有帮助。我建议你查看 strip
、lstrip
、rstrip
、start_with?
、end_with?
、rindex
、gsub
、chomp
和 chop
方法:
你可能对这组方法的名称更为熟悉,因为 PHP 有与我刚刚提到的类似的方法:trim
、ltrim
、rtrim
、str_starts_with
、str_ends_with
、strpos
和 str_replace
。chomp
和 chop
方法在 PHP 中非常不同,所以我建议你在 Ruby 中仔细查看它们,因为它们可以非常有用。
上述方法的易用性和实用性证明了为什么我们应该依赖 Ruby 的字符串方法来进行字符串操作。我们当然可以自己编写所有这些功能,但这只是重新发明轮子,我们会浪费时间和精力。如果你选择这样做,我当然不会阻止你,因为你在这个过程中可能会学到很多 Ruby。然而,在这个指导性的旅行中,我们将坚持学习 Ruby 为我们提供的更多工具。现在,让我们看看 Ruby 如何允许我们执行另一个强大的操作:处理文件。
文件操作
几十年前,保存信息的一个少数选项(如果不是唯一选项)是将信息存储在文件中。所有种类的数据都存储在这些文件中:密码、用户数据、配置数据等等。在当时,将信息保存在纯文本文件中是保存信息最可行的方法。但随着数据库(DBs)的出现和数据库的使用,这一切都结束了。数据库成为了一个更可行且更受欢迎的选项,并且现在有不同类型的数据库。尽管今天这仍然是事实,但使用数据库的成本相当高。我不仅是在谈论货币成本——我是在谈论内存、磁盘和处理器时间。因此,在某些用例中,使用纯文本文件来存储信息仍然是一个更好的选择。为此,大多数编程语言,包括 Ruby 和 PHP,都使这项任务变得简单直接。让我们看看我们如何利用 Ruby 伴随的文件操作工具。
假设我们想要从一个文件中获取用户的第一个名字。为此,我们必须创建一个文件。这个文件将被命名为 name.txt
。我们也可以不使用文件扩展名(.txt
)来命名它,但这不会对我们的脚本功能产生影响,但始终给我们的同事开发者一些提示,了解我们脚本的目的是一个好习惯。很容易假设一个名为 name.txt
的文件很可能包含文本,并且这些文本将是一个名字。所以,让我们创建这个文本文件并向其中添加一些文本:
mary
现在,让我们专注于打开这个文件。在 Ruby 中打开文件有不同的模式,但就目前而言,我们将专注于从文件中读取数据。让我们创建一个名为 reading_file.rb
的文件,并向其中添加以下代码:
# reading_file.rb
File.open("name.txt")
首先,我们必须让 Ruby 打开文件,以便它可以处理和操作它。File.open()
方法正是这样做的。但现在,我们需要获取文件内容以便在脚本中使用它。首先,我们将 File.open()
的结果分配给一个变量。我们的代码将如下所示:
# reading_file.rb
file_instance = File.open("name.txt")
通过这样,我们将 File.open()
方法的输出保存到了 file_instance
变量中,这反过来又让我们能够访问文件的内容。Ruby 有一个非常直观的方法来获取文件内容:read()
方法。read()
方法获取文件内容并将其转换为字符串。所以,让我们获取这个字符串并将其输出,以确保我们的脚本正在正常工作。现在,我们的脚本看起来是这样的:
# reading_file.rb
file_instance = File.open("name.txt")
user_name = file_instance.read
puts "The user's name is #{user_name}"
如果我们在 shell 中用 ruby reading_file.rb
运行我们的脚本,输出将如下所示:
The user's name is mary
And voilà – we have successfully read a value from a text file. In the code, we got Ruby to open the name.txt
file. Then, from the instance we got as a result, we obtained the original file’s contents as a string. Lastly, we used the value in a string to output something useful to the user. We can get fancy and capitalize the username with our already acquainted capitalize()
method. We can also test that our script is reading from the name.txt
file. Let’s open the name.txt
file and change the name contained in the file to something else:
nancy
Now, let’s run our script in the shell again with ruby reading_file.rb
. The output should be as follows:
full_name.txt with the following content:
paul smith
We’re going to manipulate this file with another Ruby script called `full_name.rb`. Initially, the script is going to be the same as the `reading_file.rb` script. We can even just copy the file, but we are going to make some tweaks to separate the full name into `name` and `last_name`. We’ll also change the `name.txt` parameter to `full_name.txt`. So, let’s look at the code in the `full_name.rb` file:
full_name.rb
file_instance = File.open("full_name.txt")
user_name = file_instance.read
puts "The user's name is #{user_name}"
If we execute this script on the shell with `ruby full_name.rb`, the output will be as follows:
The user's name is paul smith
There’s nothing unexpected here as the functionality is pretty much the same as the first script, `reading_file.rb`. But what if we wanted to have the name and the last name capitalized? We could try using the `capitalize()` method on the `user_name` variable. Let’s do that. The line where we output `user_name` will look like this:
puts "The user's name is #{user_name.capitalize}"
However, when we run the script again, the output will be as follows:
The user's name is Paul smith
Unfortunately for us, the `capitalize()` method only changes the first letter of the first word to uppercase. But do not despair, as we can accomplish the correct upper casing with just a single line of code. Before we do that, we will look at three additional methods: `split()`, `map()`, and `join()`.
The split() method
We can use `split()` to divide a word by spaces into an array. Simply put, `split()` would turn `paul smith` into an array of `[ "paul", "smith" ]`, which we can use in our current situation. So, let’s incorporate it into our code:
full_name.rb
file_instance = File.open("full_name.txt")
user_name = file_instance.read.split
puts "The user's name is #{user_name[0]} #{user_name[1]}"
In the preceding code, we took the string from the file and applied the `split()` method.
This method divided the `paul smith` into an array of two elements. In the end, we used the element on the `0` slot (`user_name[0]`) and the `1` slot (`user_name[0]`) and embedded them into the string. For now, the output is the same, but with the advantage that we have divided the name into two words. We could apply the `capitalize()` method to both elements and be done with our task at hand. But this is when we have to take a step back and think in more broad terms for our script. What would happen if someone had a middle name? Or how would our script behave if a user had two last names? Our script would truncate part of the name in both of these cases. It is our job, as developers, to create code that is generic and that will behave properly, even with some unexpected input. This is where the `map()` method proves useful.
The map() method
The `map()` method is equivalent to iterating through an array and applying a method to each element of the array in a single line. It receives a method that we want to apply to each element as a parameter. So, let’s have another rewrite of our script:
full_name.rb
file_instance = File.open("full_name.txt")
user_name = file_instance.read.split.map(&:capitalize)
puts "The user's name is #{user_name}"
Now, the output is something slightly strange, but closer to what we’re looking for. If we run this script again, we will get the following output:
The user's name is ["Paul", "Smith"]
We are almost there. Here, we are reading the name from the file, then dividing it by spaces into words, and finally applying each word to the `capitalize()` method. The problem with the output is that, yes, we’ve capitalized each element of the array, but then we are printing the whole array as a string, so the square brackets (`'[ ]'`) are included on the string. We are missing one last step, which is where the `join()` method comes in handy.
The join() method
The `join()` method does the opposite of `split()`. The `join()` method takes an array, converts it into a string, and glues each element with what we set as a parameter. So, the last step is to make the `user_name` array a string, each element separated by a white space. So, let’s add that last touch:
full_name.rb
file_instance = File.open("full_name.txt")
user_name = file_instance.read.split.map(&:capitalize).join(' ')
puts "The user's name is #{user_name}"
And with that, our generic script is done. Let’s take it out for a ride. If we were to run it on the shell, the output would be as follows:
The user's name is Paul Smith
Now, since we claim that our script is generic, it should not be an issue if we were to add a middle name. So, let’s change the name in the `full_name.txt` file:
paul isaac smith
If we were to run the script again, the output would be as follows:
The user's name is Paul Isaac Smith
We are still missing the other use case that I mentioned in which some people in some countries have two last names. So, let’s change the name one more time in our `full_name.txt` file:
benjamin eliseo pineda avendaño
As with the other examples, the script will run correctly and output the following:
The user's name is Benjamin Eliseo Pineda Avendaño
We’ve successfully made a truly generic piece of code. It will work whether we add a single name, a generic name (name and last name), or a special combination of first, middle, and two last names. While the code is not as readable as other snippets we’ve read, I can guarantee that you will encounter a combination of the `split()`, `map()`, and `join()` methods whenever you move into more advanced code. Once we get to using the Ruby on Rails framework, you will see and use both of these methods there.
So far, we’ve only written code in read-only mode. Now, let’s look at creating and modifying file contents.
Creating and modifying file contents
One practical use of reading and writing a file would be creating and modifying a counter value saved in a text file. What if we wanted to keep track of how many times a script has been executed? We could add a file with a number and each time we run the script, we could increment this value and simply output it to the user. We’ll start by creating a file called `counter.rb` with the following code:
counter.rb
file_instance = File.open("counter.txt", "w")
counter = file_instance.read
puts "Time(s) script has been run: #{counter}"
Again, we are opening a file, but in this case, we’ve added an additional parameter (`"w"`) so that we can write contents to the file. Additionally, we are going to try to create the file with our script instead of creating it by ourselves. So, let’s run this script from the shell with `ruby counter.rb`. The output should be as follows:
counter.rb:3:in 'read': not opened for reading (IOError)
from counter.rb:3:in '
Unfortunately for us, this is an error. If we look closer at the error description, it reads `not opened for reading`. This is because we set `"w"` mode, which is a write-only mode. We can only write to the file in this mode. However, we need to both read and write the contents of the file. Also, notice that the `counter.txt` file has been created, and that’s an advantage of `"w"` mode. If the file we are trying to write doesn’t exist, it will create it for us. We want this behavior, but we also want to be able to read the contents of the file. So, let’s change the mode to `"a+"` in our script:
counter.rb
file_instance = File.open("counter.txt", "a+")
counter = file_instance.read
puts "Time(s) script has been run: #{counter}"
Don’t forget to delete the `counter.txt` file and execute the script again. The output will now look like this:
Time(s) script has been run:
If we check the folder in which our script is, we will notice that the `counter.txt` file has been created. However, the value is empty, which is unintended. So, let’s tweak our script to convert that into a number:
counter.rb
file_instance = File.open("counter.txt", "a+")
counter = file_instance.read.to_i
puts "Time(s) script has been run: #{counter}"
Notice that on line 3 of our script, we’ve added `.to_i` at the end of the line, which converts the contents of the string into a number. In this scenario, the file is empty and thus returns an empty string, which, in turn, is converted into a `0`. Let’s run this script again. The output will be as follows:
Time(s) script has been run: 0
So far, so good. However, if we run it again, the output will remain the same as we have not added the functionality to increment the number. Let’s do just that:
counter.rb
file_instance = File.open("counter.txt", "a+")
counter = file_instance.read.to_i
puts "Time(s) script has been run: #{counter}"
counter += 1
File.write("counter.txt", counter)
With the last two lines, we’ve incremented the counter value by one and written the said value to the same `counter.txt` file. As a final test for this script, let’s delete the `counter.txt` file once more and run the script a few times. This should be the output:
Time(s) script has been run: 0
Time(s) script has been run: 1
Time(s) script has been run: 2
Time(s) script has been run: 3
With this output, we can confirm that our script has run correctly a couple of times. As I mentioned previously, even with the existence of DBs, file reading and writing can be useful, be it for saving configuration values or for logging, and it is fast and easy to implement. You can find additional examples and modes at [`www.rubyguides.com/2015/05/working-with-files-ruby/`](https://www.rubyguides.com/2015/05/working-with-files-ruby/).
Now that we’ve established how we can read and write to and from files, let’s take a look at the next feature that will help us give our scripts more usefulness: command-line arguments.
Command-line arguments
So far, we’ve added both variable and fixed (either numeric or string) values to our code. To make our scripts more generic and more usable for other folks, we can add parameters that won’t be hardcoded within the code. If you’re not familiar with the term, *hardcoded* is the practice of writing fixed variable values within code. In our previous examples, we added the filename that we were going to open as a fixed value – that is, to change it, we would have to change the source code. To avoid that, we could pass the script a value (a filename, in this case) that whoever runs the script can change. Passing values to a script is what we commonly refer to as command-line arguments. We can have multiple arguments, a single argument, or as we’ve done so far, no arguments. Let’s start with a simple example, then work our way up to more complex examples that will help us make our scripts more generic.
Let’s start by taking a string as a command-line argument on a script, format it, and output it to the shell. We will start by creating a script called `command_line.rb` with the following code:
command_line.rb
input_arguments = ARGV
puts "Hello #{input_arguments[0]}"
In this script, we are using `ARGV`, which is an array that contains any parameters passed to our script, then assigns it to a variable, to finally pass its first value to a string to be outputted. Let’s try running the script. First, let’s try it with no arguments by running this on the shell:
ruby command_line.rb
This will output the following:
Hello
We received this output we have not passed any command-line arguments to our script. So, how do we pass arguments, you may ask? Well, it’s as simple as writing the value right after the filename when we run it. Now, let’s try this with a name value. On the shell, run the following:
ruby command_line.rb marco
We will now see the following output:
Hello marco
As we can see, Ruby detects a single value on the `ARGV` array, and as a result, the output shows the same value we passed to the script. Unlike the value we obtained through opening a file, the `ARGV` array works a bit differently. Let’s try adding a second argument to our script. Let’s run it with both a different name and a second parameter. Back in the shell, run the following:
ruby command_line.rb ben franco
The output will be as follows:
Hello ben
This is because we are only using the first value of the `input_arguments` array – that is, the value contained in `input_arguments[0]`. Ruby takes the string that is passed as an argument, automatically splits it by spaces, and then places each element in the `ARGV` array. Let’s use all of the arguments that are being passed and wrap up this example. We will take the `map()` and `join()` combo that we previously used in the file handling examples to glue and show all arguments passed to the script, and since it’s a name, we will capitalize it in the process. So, let’s tweak our script once more so that it does just that:
command_line.rb
input_arguments = ARGV
puts "Hello #{input_arguments.map(&:capitalize).join(' ')}"
Now, let’s run it a couple of times with different names each time:
ruby command_line.rb ben aaron jones
This will output the following:
Hello Ben Aaron Jones
Let’s try it with more parameters:
ruby command_line.rb gaby audra luna WOODHOUSE
This will output the following:
Hello Gaby Audra Luna Woodhouse
The same goes for running it with fewer arguments. Let’s give this one more try with a single name:
ruby command_line.rb al
This will also run but with a single name, just like on the first iteration of the script. The output will be as follows:
Hello Al
Now that we understand the basics of command-line arguments, let’s start giving them a bit more usefulness. Let’s write a script that will take two arguments, the first being a name and the second being a digit. We will get our script to get the digit and print the name as many times as that digit. We will also add an error message if the script is run with fewer or more arguments than what we need for our script to work. So, let’s start by creating a file called `validate_arguments.rb` and add the following code to it:
validate_arguments.rb
input_arguments = ARGV
name = input_arguments.first
cycle_times = input_arguments.last.to_i
cycle_times.times { 输出 name }
In this script, we get the command-line arguments and use the first one as the name and the last one as the counter for our cycle. With the `cycle_times` variable, we’re casting (or converting) the value of the last element of the `input_arguments` array from a string into an integer. Then, we’re using the `times` method to repeat a piece of code inside the curly brackets. Now, let’s try running our script with the following values:
ruby validate_arguments.rb gabriela 5
As expected, the output of the script is as follows:
gabriela
gabriela
gabriela
gabriela
gabriela
We may think our work here is done, but we’d be wrong. This is what we call the *happy path*, in which we feed our script values that the script is expecting and thus the script’s behavior is correct. However, this is utopic as this never happens in real life. In real life, the user will forget to feed the script both parameters or will feed the parameters in the wrong order. We, as coders, need to take this into account and code appropriately. We need to validate that the input we feed the script is either correct or that we need to tell the user that the parameters are incorrect. What happens if we invert the parameters? Let’s see:
ruby validate_arguments.rb 5 gabriela
This outputs nothing. Let’s try running the script with no arguments:
ruby validate_arguments.rb
This also outputs nothing. This is a mistake on our side because we know how our script works, but someone who might be using the script doesn’t. There is no documentation and the least we can do is output error messages that can guide the user as to the correct usage of the script. Additionally, we have to assume that the end user does not know how to program and is not going to open the script to view its usage. So, let’s start by validating that the script only receives two arguments. Also, let’s add an error message to help the user out. Let’s create our validation in our code. Our `validate_arguments.rb` script will now look like this:
validate_arguments.rb
if ARGV.size != 2
输出错误。脚本执行失败!
end
input_arguments = ARGV
name = input_arguments.first
cycle_times = input_arguments.last.to_i
cycle_times.times { 输出 name }
Now, when we run it again without any arguments, we will see an error message:
错误。脚本执行失败!
However, even though we are seeing an error, the script is still going through the whole code, which is not what we want. To prove this, let’s add another message at the end of the code:
validate_arguments.rb
if ARGV.size != 2
输出 "错误。脚本执行失败!"
end
input_arguments = ARGV
name = input_arguments.first
cycle_times = input_arguments.last.to_i
cycle_times.times { 输出 name }
输出 "但我们仍在运行脚本"
Let’s run the script again (with no arguments):
ruby validate_arguments.rb
The output will be as follows:
错误。脚本执行失败!
但我们仍在运行脚本。
As we learned previously, we need to stop the execution of the script once we’ve figured out that we have errors. So, let’s fix this and stop the execution with a `Kernel::exit()` call. Let’s also add a suggestion to fix the problem:
validate_arguments.rb
if ARGV.size != 2
输出。错误。脚本执行失败!
输出。用法:ruby validate_arguments.rb name times_to_repeat
Kernel::exit(1)
end
input_arguments = ARGV
name = input_arguments.first
cycle_times = input_arguments.last.to_i
cycle_times.times { 输出 name }
输出 "但我们仍在运行脚本"
Let’s run the script once again without arguments:
ruby validate_arguments.rb
This time, we will get the correct output, and the execution will be stopped at the right point:
错误。脚本执行失败!
用法:ruby validate_arguments.rb name times_to_repeat
As you can see, we are no longer outputting the `But we are still running the script.` message. This is because once the script does not pass the validation block, its execution is stopped. This is great progress for our script. However, we are still missing one piece of validation. In previous examples, we inverted the arguments by passing the number and then the name. Even with our tweaks and validations, this is a use case that will still make our script behave erroneously. Let’s try it out:
ruby validate_arguments.rb 3 henry
This will output the following:
我们仍在运行脚本。
Again, we have no information as to why the output is empty, which can be very frustrating to the user. So, let’s add more validations to our script. We, as the creators of the script, know that the script is failing because the second argument should be a number – an integer, to be more precise. However, this validation can be slightly tricky because of the way a lot of programming languages behave, including Ruby and PHP. The behavior I’m referring to is the way Ruby converts a text string into an integer. As an example, a string such as `'22'` will be converted into an integer, `22`. However, the `'henry'` string will unexpectedly be converted into a valid `0`, and while this is not what we need, we can certainly take advantage of this behavior. For our script, we need the second argument to be larger than `0`. So, that’s exactly what we are going to validate:
validate_arguments.rb
if ARGV.size != 2
输出 "错误。脚本执行失败!"
输出 "用法:ruby validate_arguments.rb name times_to_repeat"
Kernel::exit(1)
end
input_arguments = ARGV
name = input_arguments.first
cycle_times = input_arguments.last.to_i
if cycle_times < 1
输出 "错误。第二个参数必须是一个整数!"
输出 "用法:ruby validate_arguments.rb name times_to_repeat"
Kernel::exit(1)
end
cycle_times.times { 输出 name }
输出。我们仍在运行脚本
Let’s run the script with the incorrect arguments again:
ruby validate_arguments.rb 3 henry
We will get the following output:
错误。第二个参数必须是一个整数!
用法:ruby validate_arguments.rb name times_to_repeat
With that, we have successfully made our script validate that the input must be two arguments. If we feed the script either no arguments, one argument, or more than two arguments, it will fail and send an error message describing the correct usage of the script. Now that we’ve looked into different ways to add input to our script, let’s look at another way to interact with the user: user input.
User input
So far, we’ve made use of command-line arguments to make our scripts more generic, thus helping with what could be an automated script. Be it a shell script (as we’ve done so far) or a crontab script to be run at a designated time each day, we’ve learned the basic usage of these arguments that are fed to our scripts. But there is another type of argument that, while technically not a command-line argument, is closely related and is super useful when making scripts that interact with human users. In comes *user input*. User input helps us make a script more interactive with the user as it makes a pause in the execution of the script to wait for the user to type data and resume after the user has typed a carriage return (or the *Enter* key). This makes a more user-friendly interaction with the user. Let’s look at a simple example to see this interaction at play. We will create a file called `user_input.rb` and add the following code:
user_input.rb
输出 "输入你的名字:"
name = gets
输出 "Hello #{name}"
Now, let’s run it on our shell:
ruby user_input.rb
We will notice that the output shows the following message:
输入你的名字:
We will also notice that the shell looks slightly different as it is expecting input from us. So, let’s do that and type a name, hitting the *Enter* key right after entering it:
输入你的名字:
brandon
Immediately, the execution of our script continues and outputs the greeting that we included in our code:
输入你的名字:
brandon
Hello brandon
Notice how interactive our script has become. It asks for your name; we type the name and immediately we are greeted. While this is friendlier and more interactive, we still need to fix a couple of things in our code. The method we are currently using (`Kernel::gets()`; see [`ruby-doc.org/2.7.7/Kernel.html#method-i-gets`](https://ruby-doc.org/2.7.7/Kernel.html#method-i-gets)) not only includes the name, but it also includes the return of carriage character (`\n`) or in layman’s terms, the *Enter* key character. So, if we tried to compare the input to a string, we would be surprised to see that it wouldn’t behave as we would expect it to. Let’s try it with the following code:
user_input.rb
输出。输入你的名字:
name = gets
输出 "Hello #{name}" if name == "brandon"
Now, let’s re-run our script:
ruby user_input.rb
Let’s input the name `brandon`:
输入你的名字:
brandon
This time, notice that we don’t see the greeting. And this is not because of a typo. This is because the `Kernel::gets()` method is capturing a character at the end of the name. In comes another method to save the day: `chomp()`. The `chomp()` method removes carriage return characters and trailing new lines from the string. Please refer to [`apidock.com/ruby/String/chomp`](https://apidock.com/ruby/String/chomp) for more details regarding this method. Essentially, it cleans up our string and leaves the original text. So, let’s modify our code so that it includes this method. Our code will now look like this:
user_input.rb
输出。输入你的名字:
name = gets.chomp
输出 "Hello #{name}" if name == "brandon"
Let’s run it once more:
ruby user_input.rb
If we input the name `brandon` again, we will get the following result:
输入你的名字:
brandon
Hello brandon
We will finally get the correct greeting. So, from now on, we will get input from the user with `gets.chomp` for safe measure. Now, let’s fetch an integer from the user and run some code multiple times, depending on the number the user typed:
user_input.rb
输出。输入你的名字:
name = gets.chomp
输出 "Hello #{name}" if name == "brandon"
输出 "输入尝试该过程的次数"
repeat_n = gets.chomp.to_i
repeat_n.times do
输出。尝试中…
sleep(1)
end
Let’s run it on the shell again:
ruby user_input.rb
Now, let’s input the name `brandon` and enter `3` afterwards. This will be the whole sequence:
输入你的名字:
brandon
Hello brandon
输入你想尝试该过程的次数:
3
尝试中…
尝试中…
尝试中…
After entering `brandon`, we now output instructions to enter a digit, and right after that, we fetch an integer from the user. With this digit, we will print a `trying…` message and use the `sleep()` method to pause the execution for 1 second. If you would like more detailed information regarding the `sleep()` method, please check out [`apidock.com/ruby/Kernel/sleep`](https://apidock.com/ruby/Kernel/sleep). You will notice that the script will show the `trying…` message, pause for a second, show the second message, pause again, and finally show the last message, pause one last time, and finish the execution of the script. The `sleep()` method is useful when we are waiting for a process to finish. It’s especially useful when working with API calls, which may take some time to finish. As a final exercise, let’s dive into a script’s friendliness and usefulness.
Putting it all together
Reading and understanding someone else’s code is essential to learning Ruby. With that intent, we will now look at the following example, which was written with some of the techniques we learned about in this chapter, and figure out what the script is doing:
main.rb
第一部分:Ruby 版本验证
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.6')
输出 "请验证 Ruby 版本!"
Kernel::exit(1)
end
第二部分:打开或创建 user_name 文件
file_instance = File.open("user_name.txt", "a+")
user_name = file_instance.read
第三部分:空名称验证
if user_name.empty?
puts "输入您的名字:"
name = gets.chomp
File.write("user_name.txt", name)
第四部分:程序主日志
File.write("main.log", "Writing #{name} as the entry to user_name.txt at #{Time.now}\n", mode: "a")
user_name = name
end
第五部分:程序标题
puts "Hello #{user_name.capitalize}"
puts "欢迎来到第四章"
puts "请输入您希望创建日志条目次数"
第六部分:程序周期
repeat_n = gets.chomp.to_i
repeat_n.times do
puts "正在添加日志条目..."
File.write("main.log", "Adding entry to log at #{Time.now}\n", mode: "a")
sleep(1)
end
For us to understand the intent of the script, we will divide it into six sections that we have commented on the code itself. This is not compulsory when writing code, but I’ve taken the liberty to do this for teaching purposes. So, let’s take a look at the first section:
…
第一部分:Ruby 版本验证
if Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.6')
puts "请验证 Ruby 版本!"
Kernel::exit(1)
end
…
Here, we are comparing our currently installed Ruby version and making sure that the version is higher than 2.6\. If the version is lower than that, we print an error message and exit the program. This is something you might find very often in scripts as major versions tend to differ in terms of functionality and sometimes in syntax.
Let’s move on to the next section of our script:
…
第二部分:打开或创建 user_name 文件
file_instance = File.open("user_name.txt", "a+")
user_name = file_instance.read
…
In this section, we are opening a file, but as we’ve seen in previous examples, it’s using `"a+"` mode so that if the file does not exist, it creates it. If the file already exists, it reads its contents. The script’s intent in this section is to read a user’s name from this file, but if the file is empty, the name will be empty too. This may seem slightly different from what we’ve been doing so far. However, let’s look at the next section, where this will make more sense. In sections 3 and 4, we can see the following:
…
第三部分:空名称验证
if user_name.empty?
puts "输入您的名字:"
name = gets.chomp
File.write("user_name.txt", name)
第四部分:程序主日志
File.write("main.log", "Writing #{name} as the entry to user_name.txt at #{Time.now}\n", mode: "a")
user_name = name
end
…
In section 3, we can see that if the username fetched from the file is not empty, the script moves on to the next section. But if the username is empty, then we prompt the user to provide a username. Once the user types a username, the script will write the name to the `user_name.txt` file and move to section 4.
In section 4, the script simply writes a log entry to a `main.log` file in which it writes the name obtained from the user and the time in which the user did this. Lastly, the script assigns the `user_name` variable to be used later on in the script.
In section 5, we have this code:
…
第五部分:程序标题
puts "Hello #{user_name.capitalize}"
puts "欢迎来到第四章"
puts "请输入您希望创建日志条目次数"
…
In this section, we greet the user by capitalizing the name, print a welcoming message, and then print another aiding message so that the user knows what they’ll do next, which is enter a number.
In our last section, we are doing something similar to what we did in our previous looping example. Let’s take a look:
…
第六部分:程序周期
repeat_n = gets.chomp.to_i
repeat_n.times do
puts "正在添加日志条目..."
File.write("main.log", "Adding entry to log at #{Time.now}\n", mode: "a")
sleep(1)
end
…
In section 6, which is the last section of the script, we’re getting a number from the user, then taking this number and doing a cycle to execute a code the number of times the user provided. The code to be executed simply shows a message for the user and adds multiple entry logs to the same `main.log` file. In this case, the script is also using the append writing mode so that when the script writes to this file, it will write contents at the end of the file instead of replacing the previous contents. This type of logging is both common and useful in the scripting and programming realms. It helps other users debug the functionality of the script, especially when things start failing. We are only missing one thing now: running the script. Let’s run it:
ruby main.rb
The first time we run this file, we will get the following output on the shell:
输入您的名字:
Let’s enter `daniel`. After we enter this name, we will get the following output:
Hello Daniel
欢迎来到第四章
请输入您希望创建日志条目次数
Here, the script requires that we enter a number. Let’s type `2`. The script will respond with the following output:
正在添加日志条目...
正在添加日志条目...
Initially, this seems simple enough. However, if we take a look at the contents of the folder where our script resides, we’ll notice two new files: `main.log` and `user_name.txt` . If we open the `user_name.txt` file, its contents will coincide with the name we typed:
daniel
And if we look at the `main.log` file, we will see the following output:
将 daniel 写入 user_name.txt 作为条目于 2022-12-25 16:33:24 -0600
正在日志中添加条目于 2022-12-25 16:34:53 -0600
正在日志中添加条目于 2022-12-25 16:34:54 -0600
This content coincides with what happened on the initial run of the script. It wrote `daniel` to the log and added two entries to the same log. Now, let’s run the script once more:
ruby main.rb
This time, we will notice that the script does not ask for a user, instead using the previous username we entered. The output will look like this:
Hello Daniel
欢迎来到第四章
请输入您希望创建日志条目次数
This is very practical as we don’t need to enter the name every time we run the script. Lastly, it prompts us for a number to run the log process again. Let’s type `1` and wait for the response, which should look like this:
正在添加日志条目...
This time, as we typed `1`, the cycle only ran the code once, which is what we expected. I hope you found this reading exercise useful and I hope you make the habit of reading other developer’s code to both learn good practices and understand how to do things in Ruby.
Summary
In this chapter, we learned how to write more useful scripts that we will probably reuse in the future. We also got a glimpse at some of the tools that Ruby has to handle text. We also learned how to open, read, and write content to and from a file and how this may come in handy when writing scripts. Lastly, we were exposed to Ruby’s command-line arguments, which make our automation work easier. We also learned of Ruby’s user input arguments, which make our scripts more interactive for users. Having learned this, we are now ready to undercover some misconceptions about PHP, Ruby, Ruby on Rails, and other frameworks.
第五章:库和类语法
到目前为止,我们只使用过 Ruby 及其核心组件,而没有利用 Ruby 中可用的其他库的优势。此外,我们只对 Ruby 对象和类进行了部分了解。在本章中,我们将与您一起进行一次测试驾驶,了解 Ruby 库(gem)以及如何利用 Gemfile 来利用它们。最后,我们还将学习类语法的基础知识,以帮助我们迈向更高级的工具,例如 Ruby on Rails。
考虑到这一点,在本章中,我们将涵盖以下主题:
-
让我们准备好打包吧!!!
-
Gemfile 与
composer.json
的比较 -
在 Ruby 中将库集成到代码中
-
在 Ruby 中声明类
-
Ruby 中的对象
-
Ruby 中的继承
技术要求
要跟随本章内容,我们需要以下内容:
-
任何 IDE 用于查看/编辑代码(例如,SublimeText,Visual Studio Code,Notepad++,Vim,Emacs 等)
-
对于 macOS 用户,您还需要安装 Xcode 命令行工具
-
已安装 Ruby 版本 2.6 或更高版本,并准备好使用
本章中展示的代码可在github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails/
找到。
让我们准备好打包吧!!!
编程语言本身,虽然有用,但无法考虑到程序员可能遇到的每一个单一用例。语言的核心包括许多有用的库,因此开箱即用,语言本身就非常实用。这适用于大多数编程语言。然而,当我们需要超越核心库并使用其他库来解决手头的问题时,就会出现这种情况。在 Ruby 中,社区已经创建了许多库,这些库被亲切地称为 gems。为了跟踪这些 gems,Ruby 社区想出了一个名为 bundler 的工具。对于 PHP 开发者来说,bundler 的 PHP 对应物是 Composer (getcomposer.org/
)。这两个工具都服务于相同的目的(管理库),但 bundler 在安装库到计算机上时有所不同,而 Composer 只是使它们对项目可用。但是等等——我们甚至还没有安装一个库。让我们退一步,安装一个 Ruby gem。
安装 gem
在 bundler 接管之前,我们应该了解在我们的系统上安装 gem 的什么和如何操作。Ruby 有解析 JSON 对象的能力,但有一个名为 oj
的 gem 在处理 JSON 字符串并将它们转换为 Ruby 哈希方面更为高效。让我们首先安装 oj
gem。在 shell 中,让我们输入以下命令:
gem install oj
在按下 Enter 键后运行前面的命令,我们应该得到以下输出:
Fetching oj-3.14.2.gem
Building native extensions. This could take a while...
Successfully installed oj-3.14.2
Parsing documentation for oj-3.14.2
Installing ri documentation for oj-3.14.2
Done installing documentation for oj after 2 seconds
1 gem installed
恭喜!我们已经安装了我们的第一个 gem,现在可以在代码中使用它了。让我们通过创建一个名为 reading_json.rb
的文件并添加以下代码来测试这个 gem:
json_text = '{"name":"Sarah Kerrigan", "age":23, "human":
true}'
ruby_hash = Oj.load(json_text)
puts ruby_hash
puts ruby_hash["name"]
现在,让我们回到我们的 shell 中,尝试执行以下脚本:
ruby reading_json.rb
我们将得到以下错误:
reading_json.rb:5:in `<main>': uninitialized constant Oj (NameError)
为什么我们的脚本会失败?嗯,我们安装了我们的 gem,并且在安装过程中没有出现任何错误。实际上,我们不仅需要安装我们的 gem,还需要在我们的代码中导入它。让我们在文件的开始处做这件事,这样我们的代码现在看起来就像这样:
require 'oj'
json_text = '{"name":"Sarah Kerrigan", "age":23, "human":
true}'
ruby_hash = Oj.load(json_text)
puts ruby_hash
puts ruby_hash["name"]
然后,让我们再次执行我们的脚本,使用以下命令:
ruby reading_json.rb
现在,我们将得到正确的输出:
{"name"=>"Sarah Kerrigan", "age"=>23, "human"=>true}
Sarah Kerrigan
如您所见,我们已经将 JSON 字符串转换成了 Ruby 哈希,现在它已经准备好在我们的脚本中使用。这在从 JSON 文件加载配置或处理返回 JSON 响应的 API 调用时非常有用。然而,我们还没有处理一个问题。我们如何确保使用我们脚本的其他人会得到相同的结果?嗯,这就是 bundler 和 Gemfile 进入场景的提示。
Gemfile 与 composer.json 的比较
如我之前提到的,bundler 帮助我们处理我们程序的所有依赖项——也就是说,我们需要安装的所有内容,以便我们的程序能够正确运行。为了完成这项任务,bundler 使用一个文本文件,我们将称之为Gemfile
。Composer 以非常相似的方式工作,通过让我们创建一个名为composer.json
的文件,但与 Composer 将所需的库下载到文件夹不同,bundler 将它们安装到我们的系统上。如果 bundler 确定缺少依赖项,它将自动尝试安装它。Ruby 的方式有点更神奇(或自动)。让我们通过测试 bundler 来进一步了解这个过程。我们将首先使用 shell 中的以下命令卸载之前安装的oj
gem:
gem uninstall oj
之前的命令将确认当 gem 从我们的系统中移除时:
Successfully uninstalled oj-3.14.2
现在,如果我们再次尝试运行我们的reading_json.rb
,我们将得到以下错误:
…kernel_require.rb:54:in `require': cannot load such file -- oj (LoadError)
from...kernel_require.rb:54:in `require'
from reading_json.rb:1:in `<main>'
我们回到了起点,但现在我们将用 Gemfile 来解决我们的库(或 gem)安装问题。让我们创建一个名为Gemfile
的文件,内容如下:
source 'https://rubygems.org'
gem 'oj'
通过这种方式,我们告诉 bundler 在哪里下载依赖项以及我们的依赖项是什么。我们可以告诉 bundler 其他事情,比如 gem 版本或甚至要使用的 Ruby 版本(或等效版本),但现在我们将保持简单。现在,让我们试试。在 shell 中,让我们输入以下命令:
bundle install
根据您所使用的操作系统,您可能需要输入您的 root 或管理员密码,但一旦您完成,命令的输出应该如下所示:
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Using bundler 1.17.2
Installing oj 3.14.2 with native extensions
Bundle complete! 1 Gemfile dependency, 2 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
现在我们已经安装了 gem,我们可以安全地再次使用以下命令运行我们的脚本:
ruby reading_json.rb
我们应该得到与之前完全相同的输出:
{"name"=>"Sarah Kerrigan", "age"=>23, "human"=>true}
Sarah Kerrigan
然而,现在我们会注意到一些细微的差别。如果我们查看我们脚本和 Gemfile 所在的文件夹的内容,我们还有一个新文件叫做Gemfile.lock
。此外,如果我们查看内容,应该会有类似以下的内容:
GEM
remote: https://rubygems.org/
specs:
oj (3.14.2)
PLATFORMS
ruby
DEPENDENCIES
oj
BUNDLED WITH
1.17.2
Gemfile.lock
文件充当依赖关系的地图。如果没有地图,bundler 必须从头开始构建它,这个过程有时会花费一些时间。然而,如果有锁文件,即使依赖项尚未安装,安装它们的过程也会更加高效。
我们已经从仅仅安装 gem 发展到现在为我们的脚本运行创建理想的场景。在下一节中,我们将探讨我们可以通过 Gemfile 设置的额外选项(例如使用 gem 的特定版本),以进一步明确我们的 Ruby 环境。
在 Ruby 中将库集成到你的代码中
在你成为资深 Ruby 开发者的道路上,应该掌握的一项最有用的技能是将其他 gem 集成到你的代码中。正如我们之前所看到的,这是通过使用 Gemfile 来实现的,但我们将探讨一些可以添加到其中的额外选项,并将它们集成到我们自己的脚本中。让我们编写一个脚本,它使用 GitHub 公共 API 并列出用户@PacktPublishing
的所有公共仓库。我们可以用几种方法来做这件事,但在这个例子中,我选择了一个名为Faraday的 gem。你可以在这里查看源代码:github.com/lostisland/faraday
。
Faraday 是一个客户端库,可以帮助我们使用 Ruby 附带的自带的Net::HTTP
库。让我们创建一个名为integrating_gems
的文件夹,并导航到该文件夹:
mkdir integrating_gems
cd integrating_gems
现在,让我们创建一个名为Gemfile
的文件,其内容如下:
source 'https://rubygems.org'
gem 'oj'
gem 'faraday'
在安装这些 gem 之前,我想使用 Gemfile 语法中可用的更多选项。我们将锁定 Faraday 的版本为 2.5,这意味着我们安装一个特定的版本。所以,让我们将 Faraday 行更改为以下内容:
gem 'faraday', '2.5'
将版本锁定当然有其优点和缺点。优点之一是你可以确信脚本将以相同的方式,通常使用相同的语法,在多个环境中工作。缺点可能是你可能会被锁定在某个 Ruby 版本上,你可能会陷入无法升级 Ruby 直到升级 gem 的场景。对于oj
gem,我们将使用~
运算符并将其设置为以下内容:
gem 'oj', '~> 3.13.0'
~
运算符用于界定版本范围。在这个特定的情况下,我们告诉 bundler 获取 3.13.0 和 3.14 之间的最高发布版本,不包括 3.14 - 换句话说,我们需要一个大于或等于 3.13 但小于 3.14 的版本。那么,为什么使用这个运算符呢?简而言之,~
运算符用于在我们依赖关系中增加稳定性。我不会深入解释为什么我们会使用这种语法。只需知道你迟早会遇到它。
如果你想要深入了解这个话题,请参考以下链接:
现在的 Gemfile 将看起来像这样:
source 'https://rubygems.org'
gem 'oj', '~> 3.13.0'
gem 'faraday', '2.5'
现在,让我们再次使用 bundler 安装我们的依赖项。在 shell 中输入以下命令:
bundle install
我们将得到类似以下的输出:
Fetching gem metadata from https://rubygems.org/........
Resolving dependencies…
Using bundler 1.17.2
Using faraday-net_http 2.1.0
Following files may not be writable, so sudo is needed:
/usr/local/bin
Using ruby2_keywords 0.0.5
Using faraday 2.5.0
Using oj 3.13.23
Bundle complete! 2 Gemfile dependencies, 5 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
现在,有了我们的 gem 准备就绪,我们可以在脚本中使用它们。让我们创建一个名为faraday_example.rb
的脚本,内容如下:
require 'faraday'
require 'oj'
conn = Faraday.new(
url: 'https://api.github.com',
headers: {'Content-Type' '> 'application/json'}
)
让我们退一步看看我们在做什么。我们正在导入faraday
和oj
gem。然后,我们创建了一个将连接到 GitHub API 的客户端。客户端对象需要一个 URL 和一些头信息,我们已经提供了。到目前为止,我们还没有调用 API。让我们这样做。在文件末尾添加对 API 的调用,并使用以下内容输出响应:
response = conn.get('/users/PacktPublishing/repos')
puts response.body
现在,让我们尝试使用以下内容执行此脚本:
ruby faraday_example.rb
这将输出大量文本,所以我只会包括一个摘录:
[{"id":184740404,"node_id":"MDEwOlJlcG9zaXRvcnkxODQ3NDA0MDQ=","name":"-.NET-Core-Microservices","full_name":"PacktPublishing/-.NET-Core-Microservices","private":false,"owner":{"login":"PacktPublishing","id":10974906,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEwOTc0OTA2"
…
通过这个输出,我们可以确认我们已经成功连接到 GitHub API。对于练习的最后部分,让我们使用oj
gem 将 JSON 文本转换为 Ruby 哈希,并输出 API 返回的所有响应的名称。所以,让我们删除puts
命令,并用oj
对象替换它。我们的代码现在将如下所示:
require 'faraday'
require 'oj'
conn = Faraday.new(
url: 'https://api.github.com',
headers: {'Content-Type' => 'application/json'}
)
response = conn.get('/users/PacktPublishing/repos')
repo_hash = Oj.load(response.body)
因此,我们已经从 GitHub API 服务中获取了响应并将其转换为 Ruby 哈希。最后,让我们通过在脚本末尾添加以下内容来输出 repo 的名称:
repo_hash.each { |repo| puts repo['name'] }
如果我们再次运行脚本,输出的一部分将看起来像这样:
-.NET-Core-Microservices
-Accelerate-Deep-Learning-on-Raspberry-Pi
-Accelerate-Deep-Learning-on-Raspberry-Pi-
…
有了这个,我们已经成功使用 Faraday gem 连接到 API,并使用oj
gem 处理了输出。为了参考,代码最终应该看起来像这样:
require 'faraday'
require 'oj'
conn = Faraday.new(
url: 'https://api.github.com',
headers: {'Content-Type' => 'application/json'}
)
response = conn.get('/users/PacktPublishing/repos')
repo_hash = Oj.load(response.body)
repo_hash.each { |repo| puts repo['name'] }
这是如何在其他环境中运行代码的起点。如果我们想让其他人能够运行我们的脚本,我们应该将脚本和 Gemfile 一起包含在我们的共享代码中,这样其他人就知道在运行脚本之前需要安装什么。
此外,只是让您知道,这仅仅是一个练习,目的是学习如何在代码中包含和使用 gem。Gemfile 上的语法只是为了学习目的而获取的。您应该始终追求拥有 gem 的最新版本。然而,锁定到一个版本在您想要控制升级的时间和地点时是有用的。
既然我们已经了解了 gem 的工作原理及其使用方法,现在我们可以继续学习 Ruby 中的面向对象编程。
Ruby 中声明类
PHP 和 Ruby 都是使用面向对象编程(OOP)范式的语言,Ruby 是设计如此,PHP 则是通过其自身的发展。到目前为止,严肃的开发者应该非常熟悉这种范式。在 PHP 中,所有框架都使用 OOP。虽然我们不会深入探讨这种范式在 Ruby 中的实现方式,但我们会介绍类语法的基础知识。
一个类基本上是对现实世界实体的抽象。它是这个抽象的蓝图。让我们先创建一个简单的类来表示一个人,为这个人定义一些属性,以及为他们定义一个动作(或方法)。让我们创建一个名为 class_syntax.rb
的文件,并包含以下内容:
class Person
end
这是最简单的了,但仅凭这一点并不很有用。为了使其有用,我们需要添加代表人的特征的属性。所以,让我们添加一些属性,比如他们的名字和姓氏。我们的代码现在将看起来像这样:
class Person
@first_name = nil
@last_name = nil
end
注意,我们通过在它们前面加上 @
来定义我们的属性。这些被称为实例变量,并且只能通过方法来访问。现在,让我们定义一个方法来打印出全名:
class Person
@first_name = nil
@last_name = nil
def full_name
puts "#{@first_name} #{@last_name}"
end
end
除了实例变量之外,注意我们之前没有做过任何我们没有学过的事情。我们定义了一个方法(或函数),并在方法要打印的字符串上包含了名字和姓氏变量。我们最后要做的最后一件事是添加一个构造函数。在面向对象编程中,构造函数是一个可以在使用类定义创建对象时自定义以添加行为和属性的方法。换句话说,当我们拿蓝图时,我们定义并使用它来创建一个特定的对象,我们可以在对象创建时控制某些值。在 PHP 中,这个方法是通过简单地命名方法为 __constructor()
来实现的。在 Ruby 中的等效方法是命名方法为 initialize()
。现在让我们将其包含在我们的类定义中。我们的类定义应该看起来像这样:
class Person
def initialize
@first_name = 'James'
@last_name = 'Raynor'
end
def full_name
puts "#{@first_name} #{@last_name}"
end
end
注意,我们不再需要实例变量定义,因为这是在构造函数(初始化器)中完成的。恭喜!我们的第一个类已经准备好使用了。现在让我们继续到下一节,使用这个类定义来创建我们的第一个对象。
Ruby 中的对象
在前面的章节中,我们定义了我们的抽象人应该是什么样子。这是一个将有一个名字和一个姓氏的人,我们将能够打印出他们的全名。与建筑业务并行,因为我们现在有了蓝图,我们现在可以按照这些规格建造我们的建筑了。这种创建就是我们所说的类的实例或对象。类定义是通用的,而实例是具体的。不深入探讨类定义和对象之间的关系,我们将看看代码中这种关系看起来是什么样子,以及这将如何帮助我们编写更好、更易读的代码。让我们用我们之前的代码创建我们的第一个对象:
# class_syntax.rb
class Person
def initialize
@first_name = 'James'
@last_name = 'Raynor'
end
def full_name
puts "#{@first_name} #{@last_name}"
end
end
person = Person.new
我们现在根据我们的类创建了一个特定的人,我们现在可以调用这个人的某些方法了。我们现在可以调用 full_name()
方法。让我们通过在文件的末尾添加以下行来实现这一点:
person.full_name
让我们在 shell 中再次运行我们的脚本,如下所示:
ruby class_syntax.rb
然后,我们应该得到以下输出:
James Raynor
哇!我们终于给了我们的类一个更实用的用途。现在,我们可以使用类定义来创建任意数量的对象(人)。然而,我们仍然有一个问题。我们的类只允许我们创建名为 James Raynor 的人。我们想要的是具体性,但结果却过于具体。我们需要修改我们的类,以便我们可以创建更通用的对象。所以,让我们通过向构造函数添加参数来实现这一点。我们的代码现在将看起来像以下这样:
# class_syntax.rb
class Person
def initialize( first_name, last_name )
@first_name = first_name
@last_name = last_name
end
def full_name
puts "#{@first_name} #{@last_name}"
end
end
person = Person.new
person.full_name
我们已经向构造函数方法添加了两个参数。我们传递给类的第一个名字将被分配给我们的实例变量@first_name
,这样它就可以供其他方法使用。对于姓氏也是如此。我们现在必须做出的额外调整是将第一个和最后一个名字传递给构造函数。所以,让我们这么做:
# class_syntax.rb
class Person
def initialize( first_name, last_name )
@first_name = first_name
@last_name = last_name
end
def full_name
puts "#{@first_name} #{@last_name}"
end
end
jim = Person.new( 'James', 'Raynor' )
jim.full_name
现在,我们可以创建任意数量的实例(或对象)。让我们再添加两个人:
# class_syntax.rb
class Person
def initialize( first_name, last_name )
@first_name = first_name
@last_name = last_name
end
def full_name
puts "#{@first_name} #{@last_name}"
end
end
jim = Person.new( 'James', 'Raynor' )
sarah = Person.new( 'Sarah', 'Kerrigan' )
arcturus = Person.new( 'Arcturus', 'Mengsk' )
jim.full_name
sarah.full_name
arcturus.full_name
现在,让我们在我们的 shell 上运行我们的脚本:
ruby class_syntax.rb
输出将是以下内容:
James Raynor
Sarah Kerrigan
Arcturus Mengsk
我们已经使我们的蓝图更通用,现在我们可以从这个蓝图创建不同的角色。
在我们继续到下一个主题继承之前,我想让我们看看你将来会遇到的一个非常有用的类工具——属性访问器。
属性访问器
在我们的类定义中,我们拥有first_name
和last_name
属性,同时还有一个full_name()
方法。然而,如果我们想输出一个人的名字呢?我们可能会尝试以下做法:
jim.first_name
然而,这会以以下错误惨败:
class_syntax.rb:23:in `<main>': undefined method `first_name' for #<Person:0x000000014d935460> (NoMethodError)
这就是 Ruby 与 PHP 在外观或行为上的不同之处。Ruby 显然可以有一个方法和一个具有相同名称的属性。虽然这从技术上讲并不正确,但为了辩论的需要,让我们假设这是真的,这样我们就可以暂时继续这个练习。让我们创建一个名为first_name
的方法:
…
def first_name
@first_name
end
…
这个方法看起来很奇怪,但让我们记住,Ruby 不需要我们显式地返回一个值,因为它会自动完成。所以,这个方法只是返回@first_name
中包含的值。虽然很有用,但我们必须为每个定义的属性都这样做。此外,我们只创建了方法来获取值。我们还需要创建一个方法来设置值。然而,我有好消息要告诉你。Ruby 已经通过属性访问器解决了这个问题。属性访问器会自动创建获取和设置值的方法。我们只需要指出我们想要这个“魔法”作用于哪个属性。让我们定义属性访问器,然后利用它们。我们的最终代码应该看起来像这样:
# class_syntax.rb
class Person
attr_accessor :first_name, :last_name
def initialize( first_name, last_name )
@first_name = first_name
@last_name = last_name
end
def full_name
puts "#{first_name} #{last_name}"
end
end
jim = Person.new( 'James', 'Raynor' )
sarah = Person.new( 'Sarah', 'Kerrigan' )
arcturus = Person.new( 'Arcturus', 'Mengsk' )
jim.full_name
sarah.full_name
arcturus.full_name
puts jim.first_name
puts sarah.first_name
puts arcturus.last_name
让我们再次使用以下命令运行它:
ruby class_syntax.rb
我们应该得到以下输出:
James Raynor
Sarah Kerrigan
Arcturus Mengsk
James
Sarah
Mengsk
注意我们不需要定义last_name
方法,但它仍然可用。在你学习 Ruby 的道路上,我保证你会在某个时候遇到attr_accessor
工具。Ruby 还有attr_reader
和attr_writer
,它们将attr_accessor
本身做的两件事分开成两个方法。如果你想更深入地了解属性访问器,了解它们的确切作用,并查看其他示例,你可能想访问www.rubyguides.com/2018/11/attr_accessor/
。
你准备好创建更强大的类了吗?那么,让我们跳到下一节。
Ruby 中的继承
到目前为止,我们已经探讨了 Ruby 实现面向对象范式的一些特性,但我们忽略了一个核心特性,它有助于我们重用代码。继承可以简化为将一个类的特性传递给创建一个新的子类。有了这个新类,我们可以使用父类中的任何特性,创建新的特性,或者自定义从父类继承的特性。继承的语法可能和 PHP 中的语法大不相同,但行为却非常相似。考虑到这一点,让我们看看一些用例,并看看它是如何工作的。
假设我们想要一个可以让我们连接到数据库的类。而不是必须编写所有连接到数据库的功能,我们可以获取一个已经创建的数据库类,创建一个新的类,它继承了所有的数据库功能,然后只专注于创建我们需要的特性。这是使用继承重用代码的一种方式,但让我们用一个更简单的例子来展示继承的实际应用。假设我们想要创建一个用户抽象。用户必须具有名字、姓氏、年龄和电子邮件详情。我们可以从上一节中定义的Person
类继承特性,并在新的User
类中只关注缺失的部分。
那么,让我们将我们的Person
类取名为inheritance_example.rb
并创建一个包含以下内容的文件:
# inheritance_example.rb
class Person
attr_accessor :first_name, :last_name
def initialize( first_name, last_name )
@first_name = first_name
@last_name = last_name
end
def full_name
puts "#{first_name} #{last_name}"
end
end
现在,让我们在原始类下面创建一个新的类,名为User
,并从Person
类继承。我们将使用<
操作符来完成这个操作。让我们将以下内容添加到Person
类的末尾:
# inheritance_example.rb
…
class User < Person
end
只用两行代码,我们就创建了一个全新的类,它现在(暂时)的行为与Person
类完全相同。让我们通过创建一个新的User
对象来确认这一点。现在,让我们将以下内容添加到文件的末尾:
# inheritance_example.rb
…
user = User.new
现在,让我们尝试在我们的 shell 中运行这个脚本:
ruby inheritance_example.rb
这应该会输出以下内容:
inheritance_example.rb:4:in `initialize': wrong number of arguments (given 0, expected 2) (ArgumentError)
from inheritance_example.rb:18:in `new'
from inheritance_example.rb:18:in `<main>'
我们的脚本失败了,但为什么呢?当我们阅读错误信息时,它指出构造函数期望两个参数,但一个都没有提供。从我们之前的执行示例中,我们可以推断出我们必须给构造函数提供名字和姓氏的参数。所以,让我们添加这些参数,同时也要调用full_name()
方法。我们的代码现在看起来是这样的:
class Person
attr_accessor :first_name, :last_name
def initialize( first_name, last_name )
@first_name = first_name
@last_name = last_name
end
def full_name
puts "#{first_name} #{last_name}"
end
end
class User < Person
end
user = User.new
user = User.new("Amit", "Seth")
user.full_name
让我们运行这个脚本:
ruby inheritance_example.rb
脚本将输出以下内容:
Amit Seth
因此,我们已经确认,这个新创建的类已经从Person
类继承了所有功能。我们根本不需要定义full_name()
方法,因为它已经可用。此外,构造函数自动将名字和姓氏分配给了我们的@first_name
和@last_name
实例变量。再次强调,我们只需要让这个类从Person
类继承。然而,使用本节开头提供的示例,我们想要添加一个额外的属性,称为email
。所以首先,我们将为email
属性添加一个属性访问器。现在我们的User
类看起来是这样的:
class User < Person
attr_accessor :email
end
我们现在可以使用以下方式为我们的对象分配em.ail
属性:
user.email = "my@fakemail.com"
然而,我们想要做的是将这个分配包含在新的User
构造函数中。这并不像看起来那么容易,但也不是那么困难。所以,我们首先必须为我们的新User
类定义一个构造函数。让我们就这样做。现在我们的User
类看起来是这样的:
class User < Person
attr_accessor :email
def initialize( email )
@email = email
end
end
然而,通过这样做,我们已经覆盖了原始的构造函数方法(初始化器)。但不要绝望,因为原始构造函数仍然可以通过super()
方法访问。super()
方法调用原始构造函数,但你必须提供原始的参数数量。所以,为了完成这个例子,我们再次将名字、姓氏和电子邮件添加到我们的构造函数中,并最终调用super()
方法。我们的最终代码将如下所示:
class Person
attr_accessor :first_name, :last_name
def initialize( first_name, last_name )
@first_name = first_name
@last_name = last_name
end
def full_name
puts "#{first_name} #{last_name}"
end
end
class User < Person
attr_accessor :email
def initialize( first_name, last_name, email )
@email = email
super( first_name, last_name )
end
end
user = User.new
user = User.new("Amit", "Seth")
user.full_name
puts user.email
我们已经成功使用继承重用了Person
类的功能,并创建了一个名为User
的新类。当谈论使用继承的类时,你会听到术语“层次结构”。当我们谈论层次结构时,我们指的是我们类在想象中的结构中的位置,在最上面,我们将有最通用的类,在最下面是最具体的类。对于这个例子,Person
类和User
类之间的层次关系可能开始变得有意义,其中Person
类是最通用的类,因此位于顶部。换句话说,用户是人;因此,人必须具有用户和人的属性。反之则不然。一个人不一定是用户。一个人可能是客户,并且有一个不同的用例,我们不需要电子邮件属性。在设计你的 Ruby 类时,如果你考虑到这个层次结构,这将使你更容易确定应该将哪些功能放在哪里,以编写更少的代码,并且不会出现重复不必要的代码的问题。虽然这个例子非常简化,但它向我们展示了构建可重用类是多么容易。
摘要
在本章中,我们学习了如何安装、使用和命名(gem)Ruby 库。我们还学习了如何使用 bundler 工具安装 gem,为我们的脚本和程序创建正确运行的环境。最后,我们学习了最基本的面向对象编程(OOP)语法,用于创建、实例化和继承类。现在,我们已准备好开始调试。
第六章:Ruby 的调试
就像任何其他重要的编程语言一样,Ruby 有许多工具可以帮助我们分析、修复和改进我们的代码。根据多年前我遇到的一位软件架构师的说法,拥有调试技能的程序员与缺乏调试技能的程序员之间的差距,相当于初级程序员与高级程序员之间的区别。通过使用正确的工具来调试我们的代码,我们不仅可以改进我们的代码,还可以提高我们对语言如何被解释的理解,从长远来看,这对我们作为开发者的道路是有益的。让我们看看 Ruby 自带的一些工具,以及 Ruby 社区开发的用于调试代码的 gem。
考虑到调试,在本章中,我们将涵盖以下主题:
-
Ruby 与 PHP 中的调试函数
-
调试 gem
-
理解交互式 Ruby (IRB)的有用性
技术要求
要跟随本章内容,你需要以下内容:
-
任何 IDE 来查看/编辑代码(例如,Sublime Text,Visual Studio Code,Notepad++,Vim,Emacs 等)
-
对于 macOS 用户,你还需要安装 Xcode 命令行工具
-
已安装并准备好使用的 Ruby 版本 2.6 或更高版本
本章中展示的代码可在github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails/
找到。
Ruby 与 PHP 中的调试函数
到目前为止,我们已经编写了确保代码每次都能正确运行的脚本和代码片段。然而,在现实世界中,我们会接触到别人编写的代码,这些代码可能没有经过测试,或者没有在之前未曾出现过的场景下进行过测试。这种情况很常见,我们应该准备好动手解决这些问题。在 PHP 中,我们有几个函数可以帮助我们以最简单的方式调试。你可以只阅读以下示例,不必跟着操作。现在,让我们看看 PHP 的var_dump()
函数。我们可以打开命令行 shell 并创建一个包含以下内容的文件:
<?php //buggy_code.php
$person['firSt'] = 'Thomas';
$person['last'] = 'Anderson';
echo "Hi {$person['first']} {$person['last']}";
假设我们在 shell 上运行了以下 PHP 脚本:
php buggy_code.php
应该输出类似以下内容:
PHP Warning: Undefined array key "first"…
Warning: Undefined array key "first" in…
Hi Anderson
初看时,我们可能看不到代码中的错误。如果你一眼就看到了错误,恭喜你,你有一个良好的开端。但对于那些一眼没看到错误的人来说,让我们使用var_dump()
函数。让我们注释掉最后一行,并将其添加到代码的结尾行:
<?php //buggy_code.php
…
//echo "Hi {$person['first']} {$person['last']}";
var_dump($person);
然后让我们再次从我们的 shell 中运行这个脚本:
php buggy_code.php
这应该会输出以下内容:
…
array(2) {
["firSt"]=>
string(6) "Thomas"
["last"]=>
string(8) "Anderson"
}
在 PHP 中,var_dump()
函数非常有用,因为它输出变量、其内容及其内容类型。注意,在这种情况下,var_dump($person)
告诉我们我们有一个数组,并且这个数组有两个元素。这两个元素都是字符串。我们还注意到数组中的第一个关联元素(["firSt"]
)看起来很奇怪。它在中间有一个大写的“S”。所以,让我们进行更正并移除我们的调试代码。我们的代码现在应该看起来像这样:
<?php //buggy_code.php
$person['first'] = 'Thomas';
$person['last'] = 'Anderson';
echo "Hi {$person['first']} {$person['last']}";
我们在 shell 上再次运行它:
php buggy_code.php
它返回以下输出:
Hi Thomas Anderson
虽然这可能不是最好的调试工具,但它确实是一种非常直接的方式来调试 PHP 中的代码。我个人非常喜欢这个函数,因为它是我用来调试人生中第一个函数。此外,这是一个过于简化的例子,但本质上描绘了真实场景,尤其是在进行网页开发调试时,有时你不知道什么被发送到你的脚本中。如果你对这个函数更感兴趣,你应该查看该函数的文档页面:www.php.net/manual/en/function.var-dump.php
。
你可能还想看看print_r()
和var_export()
函数。
在 Ruby 中,我们实际上没有var_dump()
函数,而是每个对象都有一个名为inspect()
的方法。inspect()
方法将对象的内容转换为字符串并返回它。让我们用 Ruby 重做相同的例子。让我们创建一个名为buggy_code.rb
的文件,并添加以下代码:
# buggy_code.rb
person = Hash.new
person['firSt'] = 'Thomas'
person['last'] = 'Anderson'
print "Hi #{person['first']} #{person['last']}"
假设我们将在 shell 上使用以下内容运行此脚本:
ruby buggy_code.rb
我们将得到以下输出:
Hi Anderson
我们事先就知道错误是什么,但为了教育目的,让我们通过在我们的代码中添加以下内容来调试person
对象,使其现在看起来像这样:
# buggy_code.rb
person = Hash.new
person['firSt'] = 'Thomas'
person['last'] = 'Anderson'
#print "Hi #{person['first']} #{person['last']}"
print person.inspect
我们使用以下命令在 shell 上运行此脚本:
ruby buggy_code.rb
这将返回以下结果:
{"firSt"=>"Thomas", "last"=>"Anderson"}
虽然我们可以通过仅使用print()
函数来实现相同的效果,但这只适用于我们在这个例子中分析哈希的内容。如果前面的例子有一个类对象,print()
函数将不会显示对象的所有内容。inspect()
方法更通用,因为它输出任何类型对象的内容。
由于我们已经知道解决方案,我们可以直接修复示例。我们的代码现在应该看起来像这样:
# buggy_code.rb
person = Hash.new
person['first'] = 'Thomas'
person['last'] = 'Anderson'
print "Hi #{person['first']} #{person['last']}"
最后,我们可以再次运行它,以验证我们的脚本是否正确运行,通过在 shell 上运行以下命令:
ruby buggy_code.rb
这将返回以下结果:
Hi Thomas Anderson
通过这种方式,我们已经学习了一种非常 PHP 风格的调试脚本方法。幸运的是,我们不会继续这条路。相反,让我们看看其他更 Ruby 风格的调试方法。
调试用的宝石
如你所猜,Ruby 社区遇到了调试问题,就像其他编程社区一样,因此想出了库(或 gem)来封装不同的调试行为。我们将具体讨论三个 gem,它们可以使你的调试体验与仅仅在代码中输出值完全不同:
-
Debug
-
Pry
-
Byebug
所有这些 gem 都是为了同一个目的而设计的,但每个 gem 都有自己的特性,你将根据自己的偏好选择在项目中使用哪一个。让我们先看看第一个。
debug gem
这个 gem 作为传统lib/debug.rb
标准库的替代品(和改进)。虽然有多种使用这个 gem 的方法,但让我们先安装我们的 gem 并创建一个简单的可调试示例。首先,我们必须做一些设置。让我们使用在前面章节中学到的 Gemfile。在一个空文件夹中创建一个名为Gemfile
的文件,并向其中添加以下代码:
# Gemfile
gem "debug", ">= 1.0.0"
接下来,我们将通过在 shell 中运行以下命令来安装此文件中列出的 gem:
bundle install
这应该输出类似以下的内容:
Resolving dependencies...
Using bundler 2.4.6
Using debug 1.7.1
Bundle complete! 1 Gemfile dependency, 2 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
通过这种方式,我们在系统中安装了 debug gem。现在让我们测试一下。为了节省时间,你应该从 GitHub 仓库下载示例:
github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails/blob/main/chapter06/debuggable_example.rb
你也可以输入整个代码,尽管我不推荐这么做,因为它包含一个非常长的字符串:
# debuggable_example.rb
require 'digest'
user = Hash.new
user['name'] = 'admin'
user['password'] = 'secret'
SECRET_SHA2_PASSWORD = '32b363908ba2382c892800589d6aa3104dc41e6789d2d6a12512c34ec0632834'
user_sha2_input = Digest::SHA2.hexdigest(user['password'])
if user_sha2_input == SECRET_SHA2_PASSWORD
print "Your password is CORRECT"
else
print "Your password is INCORRECT"
end
上述代码一开始可能看起来有点复杂,但就目前而言,让我们专注于运行它和输出,我们将在后面解释脚本的输出结果。
让我们用以下命令从 shell 中运行我们的代码:
ruby debuggable_example.rb
这将输出以下内容:
Your password is INCORRECT
我们的代码基本上是用 SHA2 算法加密密码,并与使用相同算法加密的已加密字符串进行比较。我们使用digest
模块,在文件开头导入,用Digest::SHA2
加密密码。如果密码匹配,我们应该得到消息您的密码是正确的
。然而,我们可以从输出中推断出密码不匹配。为了调试,而不是用print()
函数在这里那里打印值,让我们使用调试 gem。因为我们已经安装了 gem,所以我们可以直接将这个 gem 导入到我们的脚本中。让我们这么做。让我们在第三行代码中添加require
语句,这样前四行代码现在看起来是这样的:
# debuggable_example.rb
require 'digest'
require 'debug'
user = Hash.new
…
现在 gem 已经导入,我们可以创建一个断点了。断点是一行代码,调试程序会在这里暂停执行,让我们分析特定行内的程序。一旦我们尝试一下,就会更清楚。所以,让我们在为name
设置的 hash 值之后添加一个断点。现在我们的代码应该看起来是这样的:
# debuggable_example.rb
require 'digest'
require 'debug'
user = Hash.new
user['name'] = 'admin'
user['password'] = 'secret'
SECRET_SHA2_PASSWORD = '32b363908ba2382c892800589d6aa3104dc41e6789d2d6a12512c34ec0632834'
binding.break
user_sha2_input = Digest::SHA2.hexdigest(user['password'])
if user_sha2_input == SECRET_SHA2_PASSWORD
print "Your password is CORRECT"
else
print "Your password is INCORRECT"
end
现在,就像其他脚本一样运行我们的脚本:
ruby debuggable_example.rb
但你会注意到一些新东西。我们的脚本现在会在执行过程中暂停。而不是输出文本,我们的 shell 现在看起来像这样:
[4, 13] in debuggable_example.rb
4|
5| user = Hash.new
6| user['name'] = 'admin'
7| user['password'] = 'admin'
8| SECRET_SHA2_PASSWORD = '32b3639…'
=> 9| binding.break
10| user_sha2_input = Digest::SHA2.hexdigest
(user['password'])
11|
12| if user_sha2_input == SECRET_SHA2_PASSWORD
13| print "Your password is CORRECT"
=>#0 <main> at debuggable_example.rb:9
(rdbg)
我已经截断了加密值('32b3639…'
),但在你的屏幕上,它应该显示完整的值。注意执行已经暂停。这是什么魔法?这不过是 debug gem 在起作用。现在我们甚至可以测试一些事情。例如,让我们看看用户哈希中有什么。在我们的调试 shell 中,让我们输入以下内容:
user
在按下Enter后,shell 将返回分配给user
的值:
(rdbg) user
{"name"=>"admin", "password"=>"secret"}
(rdbg)
注意我们的user
变量既有name
也有password
索引。但如果我们尝试获取user_sha2_input
变量的值会发生什么?让我们试试。在调试 shell 中,输入以下内容:
user_sha2_input
这返回一个空值:
(ruby) user_sha2_input
nil
(rdbg)
这是因为程序还没有到达设置该变量的代码。然而,这个特殊的 shell 已经通过显示与执行相关的各种数据提供了有用的信息,例如我们正在运行的脚本(debuggable_example.rb
)和当前所在的行(=> 9| binding.break
)。我们还可以与此 shell 交互。让我们输入next
然后按Enter:
(rdbg) next
这将逐行前进到断点。shell 将再次渲染,因此现在显示以下内容:
[5, 14] in debuggable_example.rb
5| user = Hash.new
6| user['name'] = 'admin'
7| user['password'] = 'admin'
8| SECRET_SHA2_PASSWORD = '32b3639…'
9| binding.break
=> 10| user_sha2_input = Digest::SHA2.hexdigest
(user['password'])
11|
12| if user_sha2_input == SECRET_SHA2_PASSWORD
13| print "Your password is CORRECT"
14| else
=>#0 <main> at debuggable_example.rb:10
(rdbg)
注意我们已经从第 9 行移动到第 10 行。让我们再移动一行代码,以便允许程序分配密码。这次我们将使用next
的快捷键,即第一个字母,n
:
(rdbg) n
当它再次渲染时,我们现在看到我们的断点已经移动到了if
语句:
[5, 14] in debuggable_example.rb
5| user = Hash.new
6| user['name'] = 'admin'
7| binding.break
8| user['password'] = 'admin'
9|
=> 10| if user_sha2_input == SECRET_SHA2_PASSWORD
11| print "Your password is CORRECT"
12| else
13| print "Your password is INCORRECT"
14| end
=>#0 <main> at debuggable_example.rb:10
(rdbg)
现在我们可以查看密码的内容。再次,在这个调试 shell 中,输入user_sha2_input
变量:
(rdbg) user_sha2_input
我们确认值现在已经在那里,因为输出现在是以下内容:
(rdbg) user_sha2_input
2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b
(rdbg)
这就是我们的 shell 变得极其方便的地方。我们可以在执行之前测试if
子句。让我们复制if
子句,但不包括if
关键字,并将其粘贴到调试 shell 中:
user_sha2_input == SECRET_SHA2_PASSWORD
这返回if
句子正在评估的内容:
false
显然,有人更改了我们的脚本的密码,但犯了一个错误。你看,SHA2 算法的问题在于它不是一个你可以加密和解密的加密算法,以便你可以得到原始值。SHA2 是一个散列算法,这意味着你只能加密一个值。如果你需要用它作为密码,那么你所做的就是保存这个加密值,并将其与用户的加密输入进行比较。这样,甚至程序本身也无法获得原始密码,从而保护用户的密码免受程序本身的侵害。在这种情况下,我们可以快速确认密码不匹配。让我们调试这个问题。让我们比较加密值。首先,让我们输出user_sha2_input
值:
user_sha2_input
这返回以下内容:
2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b
如果我们将它与我们的代码中的 SECRET_SHA2_PASSWORD
值进行比较,我们会看到以下内容:
32b363908ba2382c892800589d6aa3104dc41e6789d2d6a12512c34ec0632834
看到它们靠近,我们可以很容易地只从前四个数字看出它们有不同的值。但在我们修复代码之前,让我们通过在 debug 控制台中输入 continue
调试命令来完成脚本执行:
(rdbg) continue
这将继续执行我们的脚本,直到它找到另一个断点(在这种情况下没有更多断点)或者直到它到达程序的末尾(就像这个例子一样)。此外,它将输出与我们第一次运行脚本时相同的文本:
(rdbg) continue # command
Your password is INCORRECT
现在 shell 恢复正常,我们可以开始修复问题。不幸的是,我们无法获取原始密码(至少在我们能够使用量子计算机之前是这样)。幸运的是,我们已经从之前的调试会话中获得了正确的值:
2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b
我们可以将这个值直接复制到我们的 SECRET_SHA2_PASSWORD
变量中,并移除断点,因此我们的代码现在看起来像这样:
# debuggable_example.rb
require 'digest'
require 'debug'
user = Hash.new
user['name'] = 'admin'
user['password'] = 'secret'
SECRET_SHA2_PASSWORD = '2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b'
user_sha2_input = Digest::SHA2.hexdigest(user['password'])
if user_sha2_input == SECRET_SHA2_PASSWORD
print "Your password is CORRECT"
else
print "Your password is INCORRECT"
end
让我们再次运行它,通过在 shell 中输入以下内容来确认修复是否正确:
ruby debuggable_example.rb
输出应该是以下内容:
Your password is CORRECT
通过这种方式,我们已经成功地使用 debug 钩子调试了我们的脚本。正如我们之前看到的,使用 debug 钩子,我们可以逐行运行脚本,并添加多个断点,这只是一个简单的例子。我建议你更多地尝试这个钩子,并查看它支持的其它命令:github.com/ruby/debug
。
pry 钩子
debug 钩子是我们现在可用的用于调试代码的选项之一。我们将要查看的第二个用于调试的钩子是 pry 钩子。正如其名称明显暗示的那样,pry 是一个能够深入观察 Ruby 程序的交互式控制台钩子。从其 GitHub 页面,我们了解到 pry 是尝试替代经典的 交互式 Ruby(或 IRB)。尽管它与 debug 钩子有相似之处,但它对调试范式的处理有自己的方法。让我们试驾一下,以便你能看到我在说什么。我们将从简单地通过在命令行中运行以下命令来安装钩子开始:
gem install pry
我们应该看到以下输出:
Fetching pry-0.14.2.gem
Successfully installed pry-0.14.2
Parsing documentation for pry-0.14.2
Installing ri documentation for pry-0.14.2
Done installing documentation for pry after 0 seconds
1 gem installed
现在,你可以从 github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails/blob/main/chapter06/pryable_example.rb
下载示例,或者简单地输入代码。我们下载或创建的文件应该命名为 pryable_example.rb
,它看起来像这样:
# pryable_example.rb
class Person
attr_accessor :first_name, :last_name
def full_name
puts "#{first_name}#{last_name}"
end
end
person = Person.new
person.first_name = "Zach"
person.last_name = "Smith"
person.full_name
现在让我们从 shell 中执行我们刚刚创建的脚本:
ruby pryable_example.rb
它应该只输出一个名字:
ZachSmith
当然,我们故意犯了一个拼写错误(在名字和姓氏之间没有包括空格),但现在让我们看看 pry 的魔法。让我们导入 pry
并添加一个断点。我们的代码现在应该看起来像这样:
# pryable_example.rb
require 'pry'
class Person
attr_accessor :first_name, :last_name
def full_name
puts "#{first_name}#{last_name}"
end
end
person = Person.new
person.first_name = "Zach"
person.last_name = "Smith"
binding.pry
person.full_name
让我们再次运行我们的脚本:
ruby pryable_example.rb
就像 debug gem 一样,我们也将进入一个交互式 shell:
9: end
10:
11: person = Person.new
12: person.first_name = "Zach"
13: person.last_name = "Smith"
=> 14: binding.pry
15: person.full_name
[1] pry(main)>
我们必须考虑的是,命令将会不同。我们将在这个 shell 中开始一个简单的命令。让我们输入ls
(这是一个小写的 L 和一个小写的 S),就像 Linux 命令一样:
ls
提示符将返回类似以下内容:
[6] pry(main)> ls
self.methods: inspect to_s
locals: _ __ _dir_ _ex_ _file_ _in_ _out_ person
pry_instance
[7] pry(main)>
Pry 列出我们程序(在内存中)的内容,并且 pry 将其列出,就像它是一个文件系统。如果我们想“pry”到person
对象中,我们可以像导航文件夹一样导航。让我们用以下命令来做这件事:
cd person
然后,紧接着,我们再次执行ls
命令:
ls
现在提示符将显示person
的内容:
[7] pry(main)> cd person
[8] pry(#<Person>):1> ls
Person#methods: first_name first_name= full_name last_name last_name=
self.methods: __pry__
instance variables: @first_name @last_name
locals: _ __ _dir_ _ex_ _file_ _in_ _out_
pry_instance
[9] pry(#<Person>):1>
根据你可能安装的 Ruby 版本,你可能会看到之前的提示或略有不同的类似提示,但无论如何,它们都会显示person
对象的全部内容。如果我们仔细观察,我们会看到first_name
、last_name
和full_name
方法,以及@first_name
和@last_name
变量。这就是pry
的一个优点,因为它允许我们深入挖掘对象。我们还可以做的另一件有用的事情是修复并重新加载我们的代码,这意味着我们可以更改代码的一部分,然后将其重新加载到内存中,而无需停止当前执行。如果我们回到我们的源代码,并在第七行中在first_name
和last_name
之间添加一个空格,我们的代码现在应该看起来像这样:
# pryable_example.rb
require 'pry'
class Person
attr_accessor :first_name, :last_name
def full_name
puts "#{first_name} #{last_name}"
end
end
person = Person.new
person.first_name = "Zach"
person.last_name = "Smith"
binding.pry
person.full_name
最后,在我们交互式 shell 中,我们运行以下命令:
reload-method
这应该使提示符再次显示我们的断点位置:
9: end
10:
11: person = Person.new
12: person.first_name = "Zach"
13: person.last_name = "Smith"
=> 14: binding.pry
15: person.full_name
现在,让我们再次通过输入以下内容来获取全名:
person.full_name
我们随后在输出中看到修正后的完整名称:
[1] pry(main)> person.full_name
Zach Smith
=> nil
[2] pry(main)>
现在,我们可以使用 pry 的退出别名(!!!
)退出调试 shell:
!!!
这将退出调试 shell。所以,正如你所看到的,我们有很多优点,比如我们观察对象深度的水平。然而,我们确实有一个主要的缺点。在使用 pry 时,我们只能看到特定执行时刻的内容。我们不能像使用 debug 那样移动到另一行代码。我们可以做的是添加多个断点,因为 pry 允许这样做。无论如何,不要让这个缺点阻止你尝试它。
The byebug gem
另一个对我们可用的工具是 byebug gem。Byebug 是另一个不依赖于内部核心资源的调试器。它的工作方式与我们之前审查的两个选项类似。此外,这个 gem 支持流行的 IDE,如 Sublime、Atom 和 VS Code。
让我们快速了解一下这个工具。我们将安装 byebug gem。在 shell 中输入以下命令:
gem install byebug
我们应该在 shell 上看到以下输出:
Fetching byebug-11.1.3.gem
Building native extensions. This could take a while...
Successfully installed byebug-11.1.3
Parsing documentation for byebug-11.1.3
Installing ri documentation for byebug-11.1.3
Done installing documentation for byebug after 4 seconds
1 gem installed
既然这个 gem 已经可用,让我们创建一个名为byeable_example.rb
的文件,并包含以下内容:
# byeable_example.rb
require 'byebug'
[1,5,7,9].each do |index|
not_label = index ? "NOT":""
output = "#{index} is #{not_label} larger than 6"
puts output
end
让我们先在没有断点的情况下运行我们的程序。在我们的 shell 中,让我们输入以下内容:
ruby byeable_example.rb
输出将会是以下内容:
1 is NOT larger than 6
5 is NOT larger than 6
7 is NOT larger than 6
9 is NOT larger than 6
我们的项目为每个数字显示相同的消息,这并不是我们预期的。由于我们的 gem 已经安装并加载,我们只需要添加一个断点。我们通过添加byebug
文本来实现这一点。让我们在我们的代码中输出变量之前添加这一行。我们的代码现在应该看起来像这样:
# byeable_example.rb
require 'byebug'
[1,5,7,9].each do |index|
not_label = index ? "NOT":""
output = "#{index} is #{not_label} larger than 6"
byebug
puts output
end
再次,让我们在 shell 中运行我们的脚本:
ruby byeable_example.rb
就像其他调试器一样,我们看到我们的 shell 变成了一个交互式调试 shell:
1: # byeable_example.rb
2: require 'byebug'
3:
4: [1,5,7,9].each do |index|
5: not_label = index ? "NOT":""
6: output = "#{index} is #{not_label} larger than 6"
7: byebug
=> 8: puts output
9: end
(byebug)
从这个 shell 中,我们可以编写命令并查看变量的内容,就像我们使用 debugger 和 pry 时做的那样。我们看到文件和我们在第 7 行定义的断点。在这个时候,我们可以访问index
变量。让我们在 byebug shell 中输入以下内容:
index
我们立即看到这个变量的值:
1
由于我们处于each
循环中,index
变量的值已经被数组([1,5,7,9]
)的第一个值填充,在这个例子中是1
。我们也可以查看not_label
变量内部的内容。让我们在 byebug shell 中输入它:
not_label
现在,我们看到该变量的内容:
"NOT"
这是因为not_label
变量正在获取index
值,而不是将其与 6 进行比较,我们只是将其传递给三元运算符(?
符号),因此not_label
变量将始终具有文本"NOT"
作为其值。我们可以通过输入关键字continue
来验证这一点:
continue
continue
命令将继续执行程序,直到找到另一个断点。由于我们处于循环中,我们将移动到数组的下一个元素,直到再次达到断点。让我们再次在 shell 中查看index
值:
index
这将输出以下内容:
5
这确认我们已经移动到数组的下一个元素。让我们再次查看not_label
变量内部的内容:
not_label
我们应该看到相同的值:
"NOT"
这将发生在分配给index
变量的每个值上。所以,让我们通过在 byebug shell 中输入exit
命令来退出:
exit
我们的 shell 应该恢复正常。现在让我们通过在not_label
变量内添加比较并移除我们的断点来修复我们的代码。我们的代码现在应该看起来像这样:
# byeable_example.rb
require 'byebug'
[1,5,7,9].each do |index|
not_label = index < 6 ? "NOT":""
output = "#{index} is #{not_label} larger than 6"
puts output
end
现在让我们运行我们的脚本的最终版本:
ruby byeable_example.rb
这应该正确输出文本消息:
1 is NOT larger than 6
5 is NOT larger than 6
7 is larger than 6
9 is larger than 6
恭喜!我们的代码已经被调试并修复。再次强调,这是一个大大简化的例子,但我希望你能理解 byebug 有多有用。byebug 有额外的命令、配置选项,甚至还有我们命令的历史记录。请查看 byebug 的 GitHub 页面以获取更多信息:github.com/deivid-rodriguez/byebug
。
如你可能注意到的,所有这些 gem 都以非常相似的方式工作。正如我们将在下一节中看到的,这是因为它们都是基于 IRB 构建的。
理解 IRB 的有用性
在第三章中,我们简要地看到了IRB shell,我希望你注意到了 IRB 和我们的调试工具之间的一些相似之处。基本上,这些 gem 的作用是增强 IRB,使我们能够查看内存中的变量,移动执行点,并在程序的“冻结”状态下工作。最终,你将能够选择这些工具中哪一个更适合你的日常使用。我可以引导你选择一个,或者你也可以简单地使用内置的 IRB。我看到一位开发者使用的一个技巧是,他在 IRB 中编写了大部分代码,一旦代码运行无误,他就会将代码从 IRB 复制到 IDE 中保存工作。这节省了他很多本可以用于测试的时间。另一位同事使用了一个非常流行的 IDE,叫做 RubyMine。这个工具允许我们通过点击按钮添加断点(以及其他许多功能)。此外,使用调试器的缺点之一是在设置断点时添加调试代码。如果你忘记在将代码提交到代码库时移除断点,这可能会破坏你的代码。RubyMine 消除了这种风险,因为断点不是添加到你的代码中,而是由 IDE 管理的。
现在,大多数 IDE 都集成了某种调试机制,以便于你的使用。由于这可能超出了本书的范围,你可能需要自己检查这些 IDE 及其对调试的实现:
无论你选择哪条道路,完全取决于你自己。我只是想让你了解 IRB 是什么,以及它在 Ruby 开发中可以有多么强大。
来自 Laravel 背景的 PHP 程序员会对 tinker 工具很熟悉,这个工具基本上是一个加载了 Laravel 组件的 PHP 交互式 shell。我必须承认,tinker(github.com/laravel/tinker
)看起来很像 Ruby on Rails 的一个工具:Rails 控制台。Rails 控制台基本上是加载了 Ruby on Rails 组件的 IRB,以便于使用。但让我们不要跑题。我们首先必须了解 Ruby on Rails。
摘要
在本章中,我们学习了在 PHP 中可以使用的不同调试功能及其在 Ruby 中的等效功能,以及如何通过使用三种易于配置和安装的工具来调试我们的代码。虽然我不想强推我偏爱的调试宝石,但我可以说,在 byebug 出现之前,我使用了几年 pry。我建议你不仅尝试 byebug,还要留意新的调试宝石。我们还学习了如何将断点添加到我们的调试代码中,以及这些断点在开发过程中的有用性和强大功能。最后,我们了解到所有这些宝石基本上都是增强版的 IRB,因此我们可以轻松地使用它们,因为它们的行为非常相似。
在看到所有这些之后,我们现在已经准备好登上 Ruby on Rails 的列车。
第二部分:Ruby 与 Web
在本部分,你将通过最流行的 Ruby 框架 Ruby on Rails 了解 Ruby 在 Web 开发中的应用。除此之外,你还将学习数据库处理和视图的基础知识,最后,将概述 Ruby on Rails 应用程序与 PHP 应用程序在托管方面的差异。
本部分包含以下章节:
-
第七章, 理解约定优于配置
-
第八章, 模型、数据库和活动记录
-
第九章, 整合一切
-
第十章, 托管 Rails 应用程序与 PHP 应用程序的考虑因素
第七章:理解约定优于配置
一旦我们开始使用 Ruby on Rails,你将经常遇到“约定优于配置”这个短语。是的,你没听错——我们准备好开始使用 Ruby 开发者最喜爱的网页框架之一了。虽然很有趣,但在我们开始编程之前,我们确实需要了解结构和 Ruby on Rails 的配置工作方式。
在本章中,我们将介绍 Ruby on Rails 的安装及其文件结构,以便我们能够轻松地在框架中移动。一旦我们了解如何放置事物,我们将继续使用采用 MVC 模式的框架。最后,我们将学习如何通过表单和会话发送和接收数据。
考虑到 Ruby on Rails 的配置,在本章中,我们将涵盖以下主题:
-
安装 Ruby on Rails
-
Ruby on Rails 文件结构
-
Ruby on Rails 的 MVC 实现
-
用户与 Ruby on Rails 的交互
技术要求
为了跟随本章内容,我们需要以下内容:
-
任何用于查看/编辑代码的 IDE(SublimeText、Visual Studio Code、Notepad++、Vim、Emacs 等)
-
对于 macOS 用户,你还需要安装 XCode 命令行工具
-
已安装并准备好使用的 Ruby 版本 2.6
-
在我们的本地机器上安装 Git 客户端
本章中展示的代码可在 github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails/
找到。
如果有 Ruby 魔法,就有 Rails 魔法
到目前为止,你可能已经熟悉了被亲切地称为 Ruby 魔法的概念。我们看到了一些 Ruby “神奇地”从看似合理的语法中生成输出的例子,但我们并不知道它是如何做到的(提示:元编程)。以同样的方式,Ruby on Rails(也称为 Rails 或简称 RoR)在内部使用元编程来生成我们看不到的功能。我们将通过查看更多示例来理解它,但首先,我们需要安装 Ruby on Rails。
安装 Ruby on Rails
就像我们迄今为止看到的库一样,Rails 是一个开源的 gem。它与我们所看到的 gem 有点不同,因为它使用了许多依赖项并且可以生成代码示例,但最终,它仍然是一个 gem。这意味着我们可以单独安装它,或者我们可以在 Gemfile 中包含它。对于本节,我们必须将过程分为三个单独的部分——macOS 安装、Windows 安装和 Linux 安装——因为每个操作系统的行为都不同。
在 macOS 上安装 Ruby on Rails
设置本地环境的第一个步骤是安装 rbenv
。对于大多数 Mac 安装,brew
将简化此过程。让我们开始以下步骤:
-
让我们打开一个 shell 并运行以下命令:
rbenv program. Now, you’ll need to add the following line to your bash profile:
eval "$(rbenv init -)"
-
一旦你将这一行添加到你的配置文件中,你应该通过打开一个新的 shell 或运行以下命令来激活这个更改:
rbenv installed, we need to install Ruby 2.6.10 with the following command:
rbenv install 2.6.10
-
一旦安装了 Ruby 2.6.10,我们必须使用以下命令设置默认的 Ruby 版本:
bundler. Let’s install it with the following command:
gem install bundler
到此为止,我们的环境已经准备好进行本章的下一步。
如果你想了解更多关于这个安装的详细信息,请参考以下网页:www.digitalocean.com/community/tutorials/how-to-install-ruby-on-rails-with-rbenv-on-macos
。
在 Windows 上安装 Ruby on Rails
按照以下步骤在 Windows 上安装 Ruby on Rails:
-
为了设置我们的本地环境,首先,我们必须为 Windows 安装 Git。我们可以从
gitforwindows.org/
下载安装包。下载完成后,我们可以运行安装程序;它应该打开安装程序应用程序:
图 7.1 – Git 安装程序
你可以安全地接受默认选项,除非你想从 Git 更改任何特定的行为。在安装过程结束时,你只需取消选择向导的所有选项,然后继续下一步:
图 7.2 – Git 最终安装
我们还需要安装 Git SDK,因为 Ruby on Rails 需要一些依赖项。我们可以从github.com/git-for-windows/build-extra/releases/tag/git-sdk-1.0.8
获取安装程序。
请小心,并选择适合你平台(32 位或 64 位)的正确选项。在我的情况下,我必须选择 64 位,因此我下载了git-sdk-installer-1.0.8.0-64.7z.exe二进制文件:
图 7.3 – Git SDK 下载
- 一旦下载了这个安装包,运行它;我们将被询问希望将 Git SDK 安装在哪里。默认选项是合适的(
C:\git-sdk-64
):
图 7.4 – Git SDK 安装位置
由于需要下载其他附加包,这个安装包可能需要一些时间才能完成,但它会自动完成。请耐心等待。一旦这个安装包完成安装 SDK,它将打开一个 Git Bash 控制台,其外观类似于 Windows PowerShell。我们可以关闭这个 Git Bash 控制台窗口并打开另一个 Windows PowerShell。
-
一旦我们打开了新窗口,我们必须输入以下命令:
rbenv, which allows us to install multiple versions of Ruby. However, this program wasn’t created for Windows, so its installation is a little different than in other operating systems.
-
让我们打开一个浏览器并访问rbenv for Windows网页:
github.com/ccmywish/rbenv-for-windows
。在那个页面上,我们将找到如何安装
rbenv
的说明,我们现在将进行安装。 -
让我们打开一个新的 Windows PowerShell 并输入以下命令:
rbenv installation.
-
运行这个命令后,我们必须使用以下命令下载剩余的所需文件:
iwr -useb "https://github.com/ccmywish/rbenv-for-windows/raw/main/tools/install.ps1" | iex
-
一旦这个命令从 GitHub 下载完文件,我们就可以在 Windows PowerShell 中运行以下命令来修改用户的配置文件:
rbenv-for-windows web page, we can see what the content of the file should be. Let’s add it with Notepad so that the profile file now looks like this:
$env:RBENV_ROOT = "C:\Ruby-on-Windows"
rbenv 正在运行,我们的控制台将自动安装一个默认的 Ruby 版本。这可能需要一些时间,并且会考验我们的耐心。一旦过程完成,我们应该看到类似于以下输出的内容:
图 7.5 – rbenv 安装后脚本
现在,我们已经准备好安装其他版本的 Ruby。对于 Ruby on Rails 5,我们将安装 Ruby 2.6.10
。
-
让我们在刚刚打开的相同 Windows Powershell 窗口中运行以下命令来安装它:
rbenv install 2.6.10
程序会询问我们是否想安装轻量版还是完整版。请选择完整版。这又可能需要一些时间,所以请耐心等待。
-
一旦这个命令运行完成,我们必须为整个系统设置这个 Ruby 版本。我们可以通过运行以下命令来完成:
rbenv global 2.6.10
-
为了确认这个版本的 Ruby 已经被安装并启用,使用以下命令:
bundler to manage all the dependencies on our system. So, let’s install this program with the following command:
gem install bundler
-
一旦安装了这个 gem,我们必须使用以下命令更新 RubyGem 系统:
gem update –-system 3.2.3
这个命令的计算也会花费一些时间,但一旦完成,我们就可以在 Windows 上使用 Ruby on Rails 了。
接下来,让我们看看在 Linux 上安装 Ruby on Rails 的步骤。
在 Linux 上安装 Ruby on Rails
对于 Ubuntu 和 Debian Linux 发行版,我们还必须安装rbenv
以及 Ruby on Rails 正确运行所需的依赖项:
-
让我们先打开一个终端并运行以下命令:
apt, we must install our dependencies for Ruby, Ruby on Rails, and some gems that require compiling. We’ll do so by running the following command:
使用以下命令安装 rbenv:
rbenv to our $PATH. Let’s do so by running the following command:
使用以下命令将 rbenv 添加到我们的 bash 配置文件中:
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
-
接下来,运行 bash 配置文件,使用以下命令:
rbenv executable available to us. Now, we can install Ruby 2.6.10 on our system with the following command:
安装 Ruby 2.6.10,我们需要将其设置为整个机器的默认 Ruby 版本。我们可以通过运行以下命令来实现:
rbenv global 2.6.10
-
我们可以通过运行以下命令来确认这个版本的 Ruby 已经被安装:
bundler to manage all the dependencies on our system. So, let’s install this program with the following command:
gem install bundler
-
一旦安装了这个 gem,我们可以使用以下命令更新 RubyGems 系统:
gem update –-system 3.2.3
这个命令的计算也会花费一些时间,但一旦完成,我们就可以在 Linux 上使用 Ruby on Rails 了。
对于其他 Linux 发行版和其他操作系统,请参阅官方 Ruby-lang 页面:www.ruby-lang.org/en/documentation/installation/
。
下载我们的 Ruby on Rails 应用程序
虽然有几种使用 Rails 代码的方法,但为了方便使用,我们将下载一个现有项目作为示例。我们将使用 Git 工具克隆项目。打开一个终端并输入以下命令:
git clone https://github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails.git
这将生成一个名为 From-PHP-to-Ruby-on-Rails
的文件夹。现在,让我们使用以下命令导航到项目文件夹:
cd From-PHP-to-Ruby-on-Rails/chapter07/rails5/
一旦我们进入这个文件夹,您会注意到一个 Gemfile
。如果我们用我们选择的 IDE 打开那个 Gemfile
,我们会看到依赖项的开始部分:
source 'https://rubygems.org'
git_source(:github) do |repo_name|
repo_name = "#{repo_name}/#{repo_name}"
unless repo_name.include?("/")
"https://github.com/#{repo_name}.git"
end
# Bundle edge Rails instead: gem 'rails', github:
'rails/rails'
gem 'rails', '~> 5.1.7'
…
我们不要纠结于所有细节(至少现在不要),除了那里声明的 Rails 钩子。如您从前面的章节中回忆起来,我们可以使用 bundle
命令安装钩子和它们的依赖项。所以,让我们就这样做。输入以下命令:
bundle install
bundle
命令会获取 Gemfile
中声明的所有钩子,创建一个依赖映射(称为 Gemfile.lock
),并安装这些依赖项。此命令的输出应类似于以下内容:
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
Using rake 13.0.6
Using concurrent-ruby 1.2.2
Using minitest 5.18.1
…
Using rails 5.1.7
Using sass-rails 5.0.7
Bundle complete! 16 Gemfile dependencies, 79 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
为了简洁起见,输出已被截断。为了证明我们的依赖项已经正确安装,我们应该运行以下命令:
bundle exec rails --version
我们应该获取我们刚刚安装的 Rails 版本:
Rails 5.1.7
哇!我们已经成功安装了 Ruby on Rails。然而,在我们开始使用这个框架之前,我们还需要澄清一些事情。首先,您可能想知道为什么我们使用了 bundle exec rails --version
命令而不是仅仅 rails --version
。嗯,由于 Rails 开发者必须处理 Rails 能够在不同的环境和平台上运行(无论是 Windows、Linux、macOS 还是其他),确保“bundle”的库能够正确工作的方法之一是在“bundle”的上下文中运行 rails 命令。这只是说“使用我们刚刚安装的库运行命令”的一种花哨说法。所以,从现在起,我们所有的 Rails 命令都将用 bundle exec
命令包装。如果您对这个命令的更多细节感兴趣,请参阅 bundler.io/v2.4/man/bundle-exec.1.html
。
现在我们终于安装了 Ruby on Rails,我们将启动服务器并体验 Rails。
开始我们的 Ruby on Rails 项目
运行我们的示例 Rails 应用程序需要我们运行以下命令:
bundle exec rails server
按下 Enter 键后,我们应该看到以下输出:
=> Booting Puma
=> Rails 5.1.7 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.6 (ruby 2.6.10-p210), codename: Llamas in
Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3000
Use Ctrl-C to stop
一旦我们看到这条消息,这意味着我们已经准备好大干一场了。打开任何网页浏览器,在地址栏中输入以下内容:
http://localhost:3000
我们应该看到以下页面:
图 7.6 – Rails 登录页面
恭喜!我们已经成功运行了第一个 Ruby on Rails 示例应用程序。当你第一次看到这个图像时,这总是令人兴奋和神奇的(至少对我来说是这样)。而这里的“魔法”是指 Rails 配置使用网络服务器和其他工具来使这个页面成为可能。现在,在我们开始对这个示例应用程序捣鼓之前,让我们看看 Ruby on Rails 的文件结构。
Rails 文件结构解释
当你学习 Ruby on Rails 时,你需要学习的第一件事就是了解其文件结构中各个部分的位置。以我的情况为例,我花费了很长时间才把这些点连接起来,但一旦我做到了,我就不再在放置事物上挣扎。从长远来看,当新的 Rails 版本发布时,这甚至有助于你,因为各个版本之间的文件结构非常相似。所以,让我们看看所有的文件夹。以下是文件结构:
.
├── app
│ ├── assets
│ │ ├── config
│ │ ├── images
│ │ ├── javascripts
│ │ │ └── channels
│ │ └── stylesheets
│ ├── channels
│ │ └── application_cable
│ ├── controllers
│ │ └── concerns
│ ├── helpers
│ ├── jobs
│ ├── mailers
│ ├── models
│ │ └── concerns
│ └── views
│ └── layouts
├── bin
├── config
│ ├── environments
│ ├── initializers
│ └── locales
├── db
├── lib
│ ├── assets
│ └── tasks
├── log
├── public
├── test
│ ├── controllers
│ ├── fixtures
│ │ └── files
│ ├── helpers
│ ├── integration
│ ├── mailers
│ ├── models
│ └── system
├── tmp
│ ├── cache
│ │ └── assets
│ ├── pids
│ └── sockets
└── vendor
这可能看起来很多,甚至令人感到压倒,但我们将使用五个文件夹:app/controllers
、app/models
、app/views
、config
和 public
。Rails 是 controllers
文件夹,模型在 models
文件夹中,视图在 views
文件夹中。在 config
文件夹中,我们将存储配置值,例如我们定义的 URL 路由、数据库连接值以及可能因环境而异(即开发、生产测试)的值。最后但同样重要的是,在 public
文件夹中,我们将存储网络服务器需要访问的某些资产。我们偶尔可能需要处理其他文件夹,但作为 RoR 新手开发者,我们大部分时间将主要处理上述文件夹。
现在我们已经了解了 Rails 的文件结构,我们可以继续到下一部分,我们将深入探讨 MVC 模式是如何发挥作用的。
MVC 之最佳实践
如前所述,Rails 是一个 MVC 控制器。如果你过去使用过 PHP 框架,例如 CodeIgniter、Symfony 或 Laravel,你可能对这个词很熟悉。如果你不熟悉,我建议查看这些页面:
总结来说,MVC 模式将我们的应用程序分为三个组件——模型,其中我们保存所有的业务逻辑(主要是通过连接到数据库,但不限于),视图,其中我们存放要在浏览器上显示的内容(主要是 HTML),以及控制器,它作为前两个组件的组织者。如果我们用一个例子来解释这一点,用户认证组件将如下工作:在视图中创建显示用户和密码字段的 HTML 表单。一旦用户点击按钮提交这些字段,控制器将接收表单数据(用户和密码),并将它们传递给模型。之后,模型将连接到数据库并尝试找到与用户和密码匹配的数据库条目。如果我们找到一个用户条目,模型将回传在数据库上找到的用户条目。下一步将是控制器告诉浏览器重定向到一个显示用户已登录并显示模型传递的用户数据的页面。虽然这可能听起来工作量很大,但 Rails 在抽象这些组件方面做得非常出色,几乎对我们来说是不可见的。这正是我们将看到在 Rails 中约定优于配置是如何工作的。在其他框架中,我们可能需要定义我们的控制器、模型和视图所在的位置。我们有这样的自由。然而,在 Rails 中,我们不需要这样的区分。这就像魔法一样。
让我们考虑创建一个控制器的例子。最简单的方法是使用 Rails 生成器。Rails 生成器是帮助我们生成样板控制器、模型等的工具。我们将使用这个工具来生成我们的控制器。让我们去终端,那里我们的 Rails 项目仍在运行。在那个终端内,按下(并持续按下)Ctrl 键。然后(仍然按住 Ctrl 键)按下 C 键。这将向我们的应用程序发送一个停止信号;终端应该会显示类似以下的内容:
…
^C- Gracefully stopping, waiting for requests to finish
=== puma shutdown: 2023-07-22 21:52:24 -0700 ===
- Goodbye!
Exiting
现在,让我们生成一个 Home
控制器。我们可以通过运行以下命令来完成:
bundle exec rails generate controller Home
这将输出以下内容:
Running via Spring preloader in process 76607
create app/controllers/home_controller.rb
invoke erb
create app/views/home
invoke test_unit
create test/controllers/home_controller_test.rb
invoke helper
create app/helpers/home_helper.rb
invoke test_unit
invoke assets
invoke coffee
create app/assets/javascripts/home.coffee
invoke scss
create app/assets/stylesheets/home.scss
此过程还会生成控制器和一些其他用于测试和格式的文件;我们将忽略它们。让我们只关注在 app/controllers/home_controller.rb
中生成的控制器。让我们看看它的内容:
class HomeController < ApplicationController
end
目前,它是一个空的控制器,但我们将很快用通过 Rails 路由映射的动作来填充它(guides.rubyonrails.org/routing.html
)。
如果你对这个概念不熟悉,routes
文件只是将特定的 URL 映射到控制器和动作。简单来说,这指定了当调用特定 URL 时将调用哪个控制器动作。让我们先创建一个当我们在浏览器中打开 http://localhost:3000/home
时将被调用的 URL。
所以,让我们首先再次启动我们的应用程序,但现在,我们不会使用 rails server
,而是使用快捷命令,rails s
:
bundle exec rails s
在按下 Enter 键后,我们应该看到以下输出:
=> Booting Puma
=> Rails 5.1.7 application starting in development
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.12.6 (ruby 2.6.10-p210), codename: Llamas in Pajamas
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3000
Use Ctrl-C to stop
由于我们没有创建路由,如果我们现在打开我们的浏览器并访问 http://localhost:3000/home,我们会看到一个错误页面:
图 7.7 – Rails 路由错误页面
这是因为我们没有定义任何路由。让我们创建我们的 home
路由。我们可以通过打开 config/routes.rb
文件来实现:
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
所以,让我们定义一个路由。我们的代码现在应该如下所示:
Rails.application.routes.draw do
# For details on the DSL available within this file, see
http://guides.rubyonrails.org/routing.html
get 'home', to: 'home#index'
end
在这里,我们正在告诉 Rails,当应用程序接收到带有 home
URL 的 GET
请求时,它将指向 home
控制器,并执行 index
动作。让我们回到我们的浏览器并刷新页面。现在,我们的错误应该如下所示:
图 7.8 – Rails 未知动作错误页面
因此,Rails 找到了 HomeController
,但它找不到 index
动作,因为它还不存在。让我们创建它。打开我们的 app/controllers/home_controller.rb
文件,并向其中添加以下代码:
class HomeController < ApplicationController
def index
end
end
这是熟悉的语法 – 这是 HomeController
继承自 ApplicationController
类,该类有一个名为 index
的方法。现在,我们只为这个类做这些。现在,让我们再次刷新我们的浏览器;我们将看到一个更详细的错误:
图 7.9 – Rails 未知格式错误页面
这个错误是因为 Rails 找到了控制器和 index
动作,但无法将视图加载到浏览器中,因为没有定义视图。让我们添加视图。我们可以通过创建包含以下内容的 app/views/home/index.html.erb
文件来实现:
<h2>Home controller</h2>
<h3>Index Action</h3>
现在,让我们再次刷新浏览器;我们应该看到以下输出:
图 7.10 – 渲染的视图
这是我自从第一次运行 Rails 应用程序以来就非常喜欢的一个特性。无需任何额外配置,Rails “知道”在哪里查找视图。在这种情况下,Rails “知道”在 views
文件夹内应该有一个 home
文件夹。Rails 还“知道”在这个 home
文件夹内查找 index.html.erb
文件。在其他框架(尤其是 PHP 框架)中,你必须在控制器中指定你将渲染为视图的文件。这最终会变得重复且不实用。Rails 以非常优雅和直观的方式解决了这个问题。这就是约定优于配置所指的是的。了解约定后,我们不需要配置 Rails 应该在哪里查找视图。
现在我们已经对 MVC 模式及其组件如何构成我们的项目结构有了基本的了解,让我们继续下一部分,我们将学习如何使用 Rails 的 MVC 结构来发送、接收和保存数据。
Rails 中的 POST、GET 和 SESSION
在进行 Web 开发时,PHP 和 Ruby 之间一个主要的不同之处在于 PHP 是开箱即用的基于 Web 的,而 Ruby 则不是。PHP 所需要的一切就是一个启用了 PHP 的 Web 服务器,我们就准备好了。对于开发,PHP 甚至自带了一个内部 Web 服务器。另一方面,Ruby 需要使用框架才能使用 Web 协议和工具。Ruby on Rails 并不是我们用于 Web 开发的唯一框架,但它是最受欢迎的一个。你也可能想了解一下 Sinatra 框架,以便有另一个不同于 Rails 的选择:sinatrarb.com/
。
我们将坚持使用 Rails 进行 Web 开发。在 Web 开发中使用最流行的工具之一是表单。表单帮助我们从用户那里获取数据,并处理这些数据以完成不同的任务。我们可以设置搜索标准,验证用户,或者简单地显示之前保存的数据。在 PHP 中,我们可以通过 $_POST
、$_GET
和 $_SESSION
数组访问这些工具。相比之下,Rails 处理这些方式略有不同,但仍然是一种有用且直观的方式。让我们从使用来自 URL 或 $_GET
值的值创建一些示例开始。首先,让我们通过在我们的浏览器中添加参数将值添加到 URL 中:http://localhost:3000/home?search=php
。由于我们没有对视图进行任何更改,浏览器中的信息将保持不变。现在,让我们使用我们的 Home
控制器的 index
动作并添加这些参数。再次,我们必须打开我们的 app/controllers/home_controller.rb
文件,然后添加以下更改:
class HomeController < ApplicationController
def index
search = params[:search]
puts "GET value for search: #{search}"
end
end
我们添加了一个名为 search
的变量,它反过来使用内部的 params
变量。params
等同于 PHP 中的 $_REQUEST
数组。在这个例子中,我们使用它通过 URL(搜索)获取值并将其设置为一个变量。此外,我们还会显示获取到的值。如果我们刷新浏览器,我们不会看到任何变化。再次强调,这是因为我们没有修改视图文件。然而,如果我们去应用程序仍在运行的控制台,我们会看到以下输出:
Started GET "/home?search=php" for ::1 at 2023-07-22 11:48:18 -0700
Processing by HomeController#index as HTML
Parameters: {"search"=>"php"}
GET value for search php
Rendering home/index.html.erb within layouts/application
Rendered home/index.html.erb within layouts/application (2.3ms)
Completed 200 OK in 21ms (Views: 19.3ms)
如我们所见,控制台告诉我们很多关于执行情况的信息。首先,它告诉我们我们使用什么方法(GET
)来调用我们的 URL。然后,它告诉我们正在发送的数据(search
)。最后,它显示我们添加到代码中的消息。虽然这不是调试 Rails 应用程序的最佳方式,但它确实让我们了解了我们的代码做了什么以及何时做了什么。在 PHP 中,每次我们写echo
,它就会立即传递给浏览器。在这种情况下,如果我们想将数据传递给浏览器,首先,我们必须将它传递给视图。所以,让我们这么做。让我们在我们的控制器上的index
动作中添加另一行代码,使代码现在看起来像这样:
class HomeController < ApplicationController
def index
search = params[:search]
puts "GET value for search #{search}"
@search = search
end
end
这种语法看起来很熟悉。如果您不记得,我们正在使用实例变量。这是将值传递给视图的最简单方法。现在,让我们打开视图并显示这个@search
值。让我们打开app/views/home/index.html.erb
文件并添加代码,使我们的视图现在看起来像这样:
<h2>Home controller</h2>
<h3>Index Action</h3>
<p>
<b>Search parameter</b>: <%= @search %>
</p>
这看起来也非常熟悉。在 PHP 中,我们会使用<?= $search ?>
。在 Rails 中,实例变量可以立即在视图中使用。让我们最后一次刷新我们的浏览器;我们应该看到以下内容:
图 7.11 – 带有变量的渲染视图
我们已成功从 URL 中获取值。接下来,我们将查看通过POST
方法发送的值。
POST
方法用于发送我们不想在浏览器上显示的数据。想象一下通过浏览器发送密码。附近的人可能会发现我们的最深秘密。幸运的是,这就是POST
值救命的地方。
首先,让我们添加GET
和POST
路由来渲染表单并发送表单数据。让我们打开我们的路由文件,config/routes.rb
,并添加以下路由:
Rails.application.routes.draw do
get 'home', to: 'home#index'
get 'user', to: 'home#user'
post 'user', to: 'home#user'
end
我们必须对 http://localhost:3000/user 进行两次单独的调用——一次用于渲染表单,另一次用于获取表单数据。现在,让我们在控制器上创建一个动作。让我们打开我们的app/controllers/home_controller.rb
文件并添加用户动作:
class HomeController < ApplicationController
def index
…
end
def user
@password = params[:password]
end
end
我们只将密码传递给视图,以便我们可以将其与一个值进行比较。我想在这里指出,在现实生活中不应该这样做;我们只是在教学目的下这么做。现在,让我们在app/views/home/user.html.erb
上创建一个视图,并添加我们将要发送数据的表单。为此,我们将使用 Rails 表单辅助工具(guides.rubyonrails.org/form_helpers.html
)。
使用这个工具编写表单更容易,尽管一开始可能会有些困惑。所以,让我们在我们的视图中添加以下代码:
<%= form_with url: "/user", method: :post do |form| %>
<%= form.label :password, "Password:" %>
<%= form.text_field :password %>
<%= form.submit "SEND" %>
<% end %>
通过这段代码,我们创建了一个表单,它调用相同的 URL,但使用POST
方法。此外,我们正在发送密码的值。如果我们打开我们的浏览器并将 URL 设置为 http://localhost:3000/user,我们会看到以下表单:
图 7.12 – 渲染的 HTML 表单
然而,让我们不要立即发送它,因为我们还没有对这个值做任何事情。让我们回到视图代码,app/views/home/user.html.erb
,并添加以下代码,以便表单看起来像这样:
<%= form_with url: "/user", method: :post do |form| %>
<%= form.label :password, "Password:" %>
<%= form.text_field :password %>
<%= form.submit "SEND" %>
<% end %>
<% if @password == '1234' %>
Password is correct
<% end %>
现在,让我们回到浏览器,并在密码表单字段中输入 1234
。一旦我们点击 发送 按钮,我们应该看到以下内容:
图 7.13 – 带有消息的渲染 HTML 表单
如果我们输入任何其他值,例如 2345
,然后点击 发送 按钮,我们就不会再看到这条消息:
图 7.14 – 无消息的渲染 HTML 表单
这是因为我们输入了密码的错误值。同样,这个例子只是为了教学目的。我认为我不需要告诉你发送密码到视图(即使你不想显示密码)是个坏主意,但为了我们的目的,我认为这个例子对我们很有帮助。现在,让我们看看会话值。
会话值有助于保存对浏览器独特的唯一数据。当我们处理返回用户或甚至认证组件时,它们非常有用。现在,让我们做点简单的事情:让我们尝试查找一个会话值,然后创建它。你已经知道了步骤:首先,我们必须创建一个路由。让我们打开 config/routes.rb
并添加以下路由:
Rails.application.routes.draw do
…
get 'get_name', to: 'home#name_get'
get 'set_name', to: 'home#name_set'
end
现在,让我们在控制器上创建 get
和 set
动作。在我们的 app/controllers/home_controller.rb
文件中,添加以下内容:
class HomeController < ApplicationController
def index
…
end
def user
…
end
def name_get
@name = session[:name]
end
def name_set
session[:name] = "David"
end
end
在这里,我们添加了 name_get
和 name_set
动作。这在控制器上可能就是最简单的情况了。除了将一个名为 @name
的变量设置以传递给 name_get
动作中的视图外,没有太多的事情要做。同时,我们将在 name_set
动作中将名称设置为 "David"
。最后,让我们添加两个视图。首先,创建一个 app/views/home/name_get.html.erb
文件,内容如下:
Getting the name from session <%= @name %>
使用这种方式,我们只显示从会话中获取的值。现在,让我们创建设置会话值的视图。让我们创建一个 app/views/home/name_set.html.erb
文件,内容如下:
Setting the name for the session.
保存所有更改后,我们可以在浏览器中尝试运行。首先,让我们将我们的网页浏览器指向 http://localhost:3000/get_name。这应该会给我们以下输出:
图 7.15 – 名称空的页面
由于我们没有设置任何会话值,@name
变量是空的。现在,让我们在一个设置了 http://localhost:3000/get_name 值的浏览器中打开 URL。这个页面应该会显示以下内容:
图 7.16 – 设置会话值的页面
现在,让我们再次在浏览器中打开我们的 get name URL(http://localhost:3000/get_name);我们现在应该看到以下输出:
图 7.17 – 获取会话值的页面
我们已经成功设置了会话名称并检索了它。这对访问我们网站的访客和可能已经创建账户的回头客很有用。正确的方法是从数据库中获取用户名,将其设置为会话值,然后显示给用户。请注意,会话是基于 cookie 的,所以如果用户在浏览器中禁用了 cookie,那么这一切都不会起作用。我们可以确认这种基于 cookie 的行为。如果我们以隐身模式打开浏览器,我们的get_name
路由将显示一个空名称,直到我们浏览到set_value
路由。只是要注意,就像在 PHP 中一样,会话值是基于 cookie 的。
如果你来自 PHP 背景(就像我当时的情形),我还有一些额外的注意事项想要与你分享。一点是,与 PHP 不同,你不能有一个“Ruby”网络服务器。在 Ruby 领域,你的网络服务器始终需要一个框架来执行 Ruby 代码。这让我花了一些时间来消化,但一旦我接受了这一点,Ruby on Rails 就成为了我的首选框架。第二点是更多关于设置本地环境的相关内容。我在不同的操作系统上测试了这个设置,包括不同版本的 Mac、Windows 和 Linux,结果各不相同。通过使用rbenv
(github.com/rbenv/rbenv
),我实现了最佳统一的设置。
这个工具(rbenv
)允许你在机器上安装不同的 Ruby 版本。我发现的一个非常有用的“技巧”是,当安装 Rails 失败时,我简单地尝试使用另一个版本的 Ruby,大多数情况下,第二次尝试 Rails 都能完美运行。尝试安装不同的版本,包括新版本和旧版本,看看它们的运行情况。发现 Ruby 和 Ruby on Rails 版本之间的细微差别将使你成为一个更好的开发者。
摘要
在本章中,我们学习了 Rails、MVC 应用程序模式以及如何将其作为 gem 安装。我们还学习了如何生成控制器,以及在使用 Rails 时这些控制器在哪里派上用场。最后,我们学习了当开发者提到 Ruby on Rails 的“约定优于配置”范式时,他们指的是什么,以及这个特性如何使我们在使用 Rails 时生活更轻松。现在,我们准备好开始使用 Rails Models 连接和使用数据库了。
第八章:模型、数据库和 Active Record
在 MVC 应用程序设计模式中,M 代表 model,在这个上下文中,我们将使用 Ruby 的模型抽象通过另一个称为 Active Record 的设计模式连接到数据库。我们必须记住,虽然模型主要用于连接到数据库,但它们也可以用于连接到其他数据源。我们可能有一个模型连接到文件系统、Web 服务等等。模型背后的目的是组织我们的业务规则,而这个目的可能包括连接到各种数据源。
在本章中,我们将首先使用 Rails 的一些命令行生成器生成一个模型。然后,我们将使用这个模型连接到我们的数据库。最后,我们将查看 Active Record 并以非常直观的方式在我们的数据库中执行操作。
带着这个目的和 Active Record 的想法,在本章中,我们将涵盖以下主题:
-
使用 Rails 生成模型
-
连接到数据库
-
Active Record 操作
技术要求
要跟随本章的内容,您需要以下内容:
-
任何用于查看/编辑代码的 IDE(例如 SublimeText、Visual Studio Code、Notepad++ Vim、Emacs 等)
-
对于 macOS 用户,您还需要安装 Xcode 命令行工具
-
已安装并准备好使用的 Ruby 版本 2.6 或更高版本
-
在您的本地机器上安装 Git 客户端
本章中展示的代码可在 github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails/
找到。
使用 Rails 生成模型
模型是我们可能在日常生活中找到的对象的抽象。无论是人、书还是汽车,模型都充当数据库中这些对象的表示。就像控制器一样,Rails 内置了生成器,可以帮助我们以非常简单直观的方式创建模型。但首先,让我们设置我们的环境。您可以选择从上一章结束的地方开始,或者下载本章的示例代码。如果您还没有这样做,请打开终端并输入以下 git
命令:
git clone https://github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails.git
如果您已经这样做,那么只需通过运行以下命令导航到您项目中的 chapter08
文件夹:
cd From-PHP-to-Ruby-on-Rails/chapter08/rails5_models/
再次,让我们使用以下命令安装我们的依赖项:
bundle install
为了确认我们的设置是否正确,让我们运行以下命令:
bundle exec rails --version
输出应该类似于以下内容:
Rails 5.1.7
现在我们已经准备好生成我们的模型了。我们将生成一个代表人的模型。我们将为每个人添加一个名为 name
的属性,另一个名为 birthday
的属性。要生成我们的模型,让我们在我们的 shell 中输入以下命令:
bundle exec rails generate model Person name:string birthday:date
使用这个命令,我们告诉我们的 Rails 生成器创建一个名为Person
的模型,它有一个名为name
的属性,还有一个名为birthday
的属性。name
属性将是一个字符串,而birthday
属性是一个日期。一旦我们按下Enter键,我们应该看到以下输出:
bundle exec rails generate model Person name:string birthday:date
Running via Spring preloader in process 32161
invoke active_record
create db/migrate/20230727031200_create_people.rb
create app/models/person.rb
invoke test_unit
create test/models/person_test.rb
create test/fixtures/people.yml
如果我们仔细观察这个输出,我们应该会注意到两个重要的文件已经被创建,一个是迁移文件,另一个是模型本身。让我们首先打开我们的迁移文件,db/migrate/20230727031200_create_people.rb
。其内容应该看起来像这样:
class CreatePeople < Active Record::Migration[5.1]
def change
create_table :people do |t|
t.string :name
t.date :birthday
t.timestamps
end
end
end
此文件包含了生成我们数据库结构的指令。如果我们仔细观察,它指出将创建一个包含name
、birthday
和timestamps
列的people
表。为什么需要这个文件?要回答这个问题,我们需要看看 Rails 迁移是什么。
Rails migrations
在过去,跟踪一个数据库(DB)是一件麻烦事。每当你在需要数据库的其他开发者项目上工作时,总有人需要创建数据库及其表,并且通常还需要填充测试数据。如果新成员加入团队,你只需给他们一个数据库的副本,他们就可以开始工作了。但是等等——如果有人修改了数据库结构会怎样?如果我们需要一个新的字段呢?如果某个字段不再需要了呢?那么,负责这个数据库的人将不得不进行更改,然后将新的数据库副本分发给团队中的所有开发者。你可以看到,如果项目上有两个以上的开发者,这种情况可能会变得难以控制。这时,迁移就出现了来拯救我们!
迁移是一系列按顺序重建数据库结构的命令。在刚才我刚刚概述的假设例子中,迁移可以无问题地解决这个问题。最初,你会创建一个迁移来创建一个具有特定字段的表。如果表需要一个新的字段,你会创建另一个迁移来创建那个新字段。如果某个字段不再需要,你会创建一个迁移来删除那个字段。而且,你可能已经注意到了,迁移的名称有一个时间戳。这是为了按顺序运行迁移——我们首先创建表,然后添加字段,最后删除字段。如果新开发者加入团队,他们只需运行所有迁移,就可以拥有与所有人相同的数据库结构。回到当前的迁移,我们有(迁移)指令,但我们仍然需要执行这些指令来影响数据库。所以现在,让我们运行命令来执行这个迁移。在命令行中,输入以下命令:
bundle exec rails db:migrate
这应该会输出以下内容:
== 20230727031200 CreatePeople: migrating
=====================================
-- create_table(:people)
-> 0.0002s
== 20230727031200 CreatePeople: migrated (0.0002s)
============================
这意味着我们的数据库结构已经创建,有一个名为people
的表。这是 Rails 魔法的部分。我们还没有在我们的项目中配置任何数据库,但是这个命令成功执行,这意味着 Rails 已经连接到了一个数据库。背后的原因是 Rails 的开发者希望您能够直接使用一个现成的项目,为此,他们让初始项目默认连接到一个名为 SQLite 的数据库。SQLite 是一系列库,允许我们使用基于项目中的sqlite3
文件的轻量级数据库。如果您对这个主题感兴趣,我建议您阅读 SQLite 官方页面:www.sqlite.org/index.html
我们现在已经生成了模型,并准备好进入下一部分,我们将使用我们的模型和 Rails 配置来连接到数据库。
连接到数据库
到目前为止,我们已经创建了一个Person
模型以及我们数据库结构所需的迁移。现在我们准备连接到我们的数据库。但是等等,我们之前已经连接到一个数据库了!正如之前所述,如果我们能够成功运行迁移,这意味着我们确实连接到了 SQLite 数据库。现在让我们看看 Rails 是如何配置来做到这一点的。让我们检查我们的 Gemfile,在这个过程中,我们会看到以下这一行:
…
# Use sqlite3 as the database for Active Record
gem 'sqlite3'
…
前面的行安装了sqlite3
gem,它允许 Rails 与 SQLite 数据库通信。但是等等,还有更多。如果我们打开app/config/database.yml
文件,我们还会看到我们项目的数据库设置:
…
default: &default
adapter: sqlite3
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
…
default
部分定义了数据库适配器为sqlite3
,环境设置数据库源文件位于db/development.sqlite3
。如果我们检查这个,我们会看到文件确实存在。Rails 迁移命令是负责创建这个文件的命令。请不要打开这个文件,因为它是一个二进制文件,除非你在 IDE 上安装了读取 SQLite 文件的插件,否则你只能看到对计算机有意义而对人类无意义的数据。现在让我们使用这些新获得的信息,实际上使用 Rails 控制台来操作数据库中的数据。
Rails 控制台
Rails 的创造者付出了巨大的努力,使 Ruby on Rails 成为与 Rails 搭配的首选框架。因此,他们受到了 Ruby 附带的一些工具的启发,特别是交互式 Ruby(IRB)控制台。Rails 附带了一个类似的控制台,但经过调整,可以加载和查询 Rails 组件。让我们用以下命令试一试:
Bundle exec rails console
这将显示以下输出,并使我们能够与 Rails 交互:
Running via Spring preloader in process 85479
Loading development environment (Rails 5.1.7)
irb(main):001:0>
正是这里,我们的Person
模型派上了用场。让我们创建一个名为single_person
的新对象,并将一些数据添加到我们的数据库中。在这个 Rails 控制台中,让我们运行以下命令:
single_person = Person.new
这将基于我们通过Model
文件和迁移定义的Person
模型创建一个对象。前面的命令将输出以下内容:
=> #<Person id: nil, name: nil, birthday: nil, created_at: nil, updated_at: nil>
irb(main):002:0>
我们可以看到,我们已经创建了一个没有 ID、没有名字和没有生日的空对象。现在让我们设置对象的名字和生日。我们将通过首先输入以下命令来完成:
single_person.name = "Benjamin"
我们的提示将使用以下输出确认我们已经设置了名字:
=> "Benjamin"
现在让我们使用以下命令设置生日:
single_person.birthday = "1986-02-03"
我们正在以year-month-day
格式设置日期。前面的命令将出生日期设置为 1986 年 2 月 3 日。就像前面的命令一样,它将通过返回我们刚刚设置的值来确认这一点:
=> "1986-02-03"
我们可以进一步了解我们的对象,通过仅输入我们的single_person
变量名来查看我们迄今为止设置的属性。让我们这样做:
single_person
前面的命令将返回以下输出:
=> #<Person id: nil, name: "Benjamin", birthday: "1986-02-03", created_at: nil, updated_at: nil>
有一个需要注意的事情是,这个信息目前仅在内存中。我们还没有将数据持久化(或写入)到数据库中。你可能会经常听到持久化这个词,当处理数据库时,它指的是将数据写入数据库。按照现在的状况,如果我们离开 Rails 控制台,数据将会丢失。你可能会问,我们如何持久化数据?简单:我们调用single_person
对象的save
方法。但在我们这样做之前,让我们确认我们的数据库中没有数据。我们将通过输入以下来自Person
类的静态方法来完成:
Person.all
这将输出以下内容:
Person Load (0.7ms) SELECT "people".* FROM "people" LIMIT ? [["LIMIT", 11]]
=> #<Active Record::Relation []>
此输出显示了数据库用于操作数据的结构化查询语言(SQL)命令。SQL 是一种标准语言,用于“与”数据库“交流”。许多数据库使用这种语言,因此你不需要为每个数据库学习不同的语言。如果你想了解更多关于 SQL 的信息,我建议你查看这个页面:
如果你之前没有遇到过 SQL 命令,这可能会显得有些晦涩,但请相信我,在现实中它并不那么糟糕。输出的一部分告诉我们 Rails 正在加载一个 SQL 命令,该命令是SELECT "people".* FROM "people"
。星号(*
)是一个过滤器,用于选择与people
表关联的所有字段。在这种情况下,这意味着 Rails 将获取 ID、name
、birthday
和timestamp
字段。命令中的FROM
部分告诉数据库引擎从people
表获取条目,没有任何过滤器。最后,输出的最后一行告诉我们people
表中没有条目。正如我之前提到的,数据仍在内存中,所以现在让我们使用save
方法将数据持久化到数据库中。让我们在 Rails 控制台中输入以下命令:
single_person.save
这将输出一条消息,确认我们已经将数据保存到数据库中:
irb(main):006:0> person.save
(3.1ms) begin transaction
SQL (5.8ms) INSERT INTO "people" ("name", "birthday", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Benjamin"], ["birthday", "1986-02-03"], ["created_at", "2023-08-06 22:11:54.475516"], ["updated_at", "2023-08-06 22:11:54.475516"]]
(0.4ms) commit transaction
=> true
现在让我们再次使用以下命令检索数据库中的所有条目:
Person.all
这将返回以下输出:
Person Load (0.9ms) SELECT "people".* FROM "people" LIMIT ? [["LIMIT", 11]]
=> #<Active Record::Relation [#<Person id: 1, name: "Benjamin", birthday: "1986-02-03", created_at: "2023-08-06 22:11:54", updated_at: "2023-08-06 22:11:54">]>
最后一行表明我们确实在数据库中持久化了信息。如果我们关闭 Rails 控制台并再次打开,然后获取所有记录,我们会得到刚才同样的结果。恭喜你,我们已经成功使用简单但强大的命令连接到数据库。现在是我们充分利用 Active Record 的时候了。
Active Record 操作
Active Record 是一种设计模式,旨在简化与数据库的通信。历史上,SQL 是与大多数数据库通信的标准。然而,每个数据库都采用了自己独特的一套 SQL 命令和标准。虽然它们都非常相似,但每个都有其独特的特性,部分原因是因为并非所有数据库都有相同的功能集。一篇详细介绍 Active Record 设计模式概念的精彩文章是这篇:
以为例,PostgreSQL 提供的数据类型比 MySQL 更为复杂。另一个例子是用于网络应用的 SQLite,它非常容易设置,但在大型应用中表现不佳。在 Rails 中,SQLite 主要用于快速设置和开发。Active Record 在哪里发挥作用呢?Active Record 使用一种称为 对象关系映射器(ORM)的技术。这种技术涉及将数据库对象“映射”到编程对象。这种映射的一个例子是表中的条目变成一个对象,而每个列成为该对象的属性。我们为什么要这样做呢?简单的答案是,作为开发者,我们处理对象比处理 SQL 命令更容易。更复杂的答案是,Active Record 使用相同的语言(对象)并将这些对象转换为数据库使用的任何特定风格的 SQL。你可以在开发者几乎不费力的前提下切换数据库类型(PostgreSQL、MySQL、SQLite 等)。使用比 SQL 语法更易读的命令来执行数据库操作。学习几个直观的命令比学习 SQL 所需的整个语法要容易得多。我们已经看到了这个功能在行动中的表现,但让我们看看简单 Active Record 命令和 SQL 命令之间的区别。当仍然在 Rails 控制台中时,让我们输入以下内容:
Active Record::Base.connection.execute("Select * from people")
这将返回数据库中的条目数组:
(4.4ms) Select * from people
=> [{"id"=>1, "name"=>"Benjamin", "birthday"=>"1986-02-03", "created_at"=>"2023-08-06 22:11:54.475516", "updated_at"=>"2023-08-06 22:11:54.475516"}]
我们可以通过使用 Active Record 使用的 ORM 技术通过以下命令获得相同的结果:
Person.all
前面的命令也返回条目数组:
Person Load (1.9ms) SELECT "people".* FROM "people" LIMIT ? [["LIMIT", 11]]
=> #<Active Record::Relation [#<Person id: 1, name: "Benjamin", birthday: "1986-02-03", created_at: "2023-08-06 22:11:54", updated_at: "2023-08-06 22:11:54">]>
我不知道你们是否这样认为,但我觉得记住 Person.all
比记住 SELECT * FROM people
更容易。请别误会——知道 SQL 语法总是有优势的。然而,知道 SQL 语法和如何利用 Active Record 更有利。让我们看看我们可以使用 Active Record 执行的其他操作。
到目前为止,我相信安装一个客户端来可视化我们的 SQLite 数据将非常有用。有许多客户端和浏览器插件可以完成这项任务,但我推荐使用 DB Browser for SQLite。您可以在以下页面查看其页面:
这将在 Windows 和 Mac 上实现可视化数据的目标,甚至在某些 Linux 发行版上,但对于这个工具不可用的平台,您也可以依赖 Chrome 的 SQLite Manager for Google Chrome 插件:
SQLite Manager for Google Chrome
它们都以类似的方式工作,而且,我们只想将这个工具用作可视数据浏览器。
一旦我们安装了工具,让我们打开数据库文件。在这种情况下,数据库文件位于chapter08/rails5_models/db/development.sqlite3
。在 DB Browser for SQLite 应用程序打开的情况下,让我们点击这里显示的打开数据库按钮:
图 8.1 – 在 SQLite Browser 中打开数据库的按钮
然后让我们导航到我们的development.sqlite3
文件:
图 8.2 – 导航到 sqlite3 文件
一旦我们打开那个文件,我们就能查看我们迄今为止创建的表,以及 Rails 自己创建的一些其他表。
图 8.3 – 在 SQLite Browser 中打开人表
我们现在可以忽略所有其他表,只需专注于people
表。如您所见,我们具有与创建Person
模型实例时在Person
对象上看到的相同的列(id
、name
、birthday
、created_at
和updated_at
)。但除了结构之外,我们对这个表内的条目(或记录)更感兴趣。让我们右键单击people
表,并选择以下截图所示的浏览表选项:
图 8.4 – 在 DB Browser for SQLite 中浏览表数据
现在我们应该能看到我们在用 Rails 控制台玩耍时添加的单个条目:
图 8.5 – 在 SQLite Browser for SQLite 上显示的人表条目
条目应包含我们输入到 Rails 控制台中的相同数据。
我们已确认初始数据设置正确,但现在是我们使用 Active Record 在数据库上创建新记录的时候了。
创建记录
我们之前看到,我们可以通过创建一个Person
对象,添加属性(name
和birthday
),并最终保存记录来向我们的数据库添加条目。但有一个单行命令可以完成相同的事情。让我们在 Rails 控制台中尝试以下行:
Person.create(name: "Oscarr", birthday: "1981-02-19")
应该输出以下内容:
(2.7ms) begin transaction
SQL (7.6ms) INSERT INTO "people" ("name", "birthday", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Oscarr"], ["birthday", "1980-02-19"], ["created_at", "2023-08-07 01:14:35.044370"], ["updated_at", "2023-08-07 01:14:35.044370"]]
(0.5ms) commit transaction
=> #<Person id: 2, name: "Oscarr", birthday: "1981-02-19", created_at: "2023-08-07 01:14:35", updated_at: "2023-08-07 01:14:35">
"Oscarr"
这个名字是有意为之的。正如我们所见,create
方法生成一个 SQL 语句,在我们在其上工作的同一张表上创建记录,但具有不同的属性。我们拥有的属性越多,如果我们使用其他方法添加数据,我们就需要更多的代码行。这两种方法都是将数据插入我们数据库的有效方法。我只是想展示两种选项,这样你可以选择对你特定用例更方便的一种。现在,让我们确认这个新条目确实存在于数据库中。让我们回到 SQLite 浏览器应用程序,通过刷新按钮或按 Mac 用户的CMD + R或 Windows 和 Linux 用户的Ctrl + R来刷新视图。现在,这个新条目应该会显示在表格上:
图 8.6 – 在 SQLite 浏览器中显示的人员表上的新条目
哦,等等。我们犯了两个错误。我在添加名字"Oscarr"
时打错了字,我本想只输入"Oscar"
。我还犯了一个关于年份的错误,因为我本想添加1980
而不是1981
。这个幸运的错误带我们到了下一个操作:SELECT
。
选择记录
到目前为止,我们看到了一种选择所有条目的方法:all
方法。但 Active Record 还提供了两个其他非常有用的方法:first
和last
。正如其名所示,我们可以选择任何选择中的第一个记录。让我们在 Rails 控制台中输入以下命令来执行此操作:
Person.all.first
这应该返回以下输出:
Person Load (0.9ms) SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Person id: 1, name: "Benjamin", birthday: "1986-02-03", created_at: "2023-08-06 22:11:54", updated_at: "2023-08-07 02:20:36">
正如你所见,这个命令选择了我们people
表中的第一个条目。现在让我们在 Rails 控制台中尝试使用last
方法,输入以下内容:
Person.all.last
这应该输出以下内容:
Person Load (1.8ms) SELECT "people".* FROM "people" ORDER BY "people"."id" DESC LIMIT ? [["LIMIT", 1]]
=> #<Person id: 2, name: "Oscarr", birthday: "1981-02-19", created_at: "2023-08-07 01:14:35", updated_at: "2023-08-07 02:24:21">
我们已经选择了数据库中的最后一个记录。这些方法在寻找测试数据时非常有用。现在让我们使用where
方法通过一个确定的字段来过滤数据。让我们在 Rails 控制台中尝试以下代码:
Person.where( name: "Benjamin" )
这将输出以下内容:
Person Load (0.9ms) SELECT "people".* FROM "people" WHERE "people"."name" = ? LIMIT ? [["name", "Benjamin"], ["LIMIT", 11]]
=> #<Active Record::Relation [#<Person id: 1, name: "Benjamin", birthday: "1986-02-03", created_at: "2023-08-06 22:11:54", updated_at: "2023-08-07 02:20:36">]>
此代码已选择所有名为"Benjamin"
的条目。在这种情况下,只有一个条目。然而,这里有一个需要注意的小陷阱。注意,在前一个输出中的Relation
一词之后,有一个方括号([
),它最终在行尾几乎关闭(>]>
)。这是因为当我们使用where
方法时,它总是返回一个对象数组。这在显示视图中的网格或表格数据时很有用,但当我们想要选择单个条目时可能会很棘手。现在,让我们谈谈选择单个记录。在更新任何记录之前,我们需要选择我们要修改的记录。为此,Rails 上的 Active Record 实现提供了两个方便的方法。第一个是find_by
。find_by
方法需要一个参数,形式为包含我们想要过滤的属性的哈希,后跟值。在这种情况下,我们想要通过name
属性和Oscarr
值进行过滤。让我们通过在 Rails 控制台中输入以下内容来测试它:
found_person = Person.find_by name:"Oscarr"
这将返回以下输出:
irb(main):002:0> found_person = Person.find_by name:"Oscarr"
Person Load (0.8ms) SELECT "people".* FROM "people" WHERE "people"."name" = ? LIMIT ? [["name", "Oscarr"], ["LIMIT", 1]]
=> #<Person id: 2, name: "Oscarr", birthday: "1981-02-19", created_at: "2023-08-07 01:14:35", updated_at: "2023-08-07 01:14:35">
Rails 返回它通过find_by
方法提供的标准找到的第一个条目。在这种情况下,它找到第一个名字等于"Oscarr"
的条目。我们可以通过输入任何属性并查看其内容来确认这一点。让我们通过在 Rails 控制台中输入以下行来尝试:
found_person.id
这将返回以下输出:
2
我们可以用任何其他属性做同样的事情。让我们用birthday
属性试一试。让我们在我们的 Rails 控制台中输入以下内容:
found_person.birthday
我们应该得到以下输出:
Thu, 19 Feb 1981
因此,正如你所看到的,found_person
对象包含了我们正在寻找的条目。然而,这有一个前提。可能有不止一个人有相同的名字。如果我们想要选择第二个人,那么我们的代码将失败,因为find_by
方法自动返回第一个找到的条目。为了解决这个难题,Active Record 提供了一个特殊的方法,称为find
。此方法假设我们的表有一个唯一的id
列,每个条目都是唯一的。所以,在我们的上一个用例中,如果有两个人有相同的名字,我们只需通过唯一的 ID 进行过滤。在这种情况下,我们只需在我们的 Rails 控制台中输入以下内容:
found_person = Person.find(2)
这将输出与之前相同的内容:
irb(main):015:0> Person.find(2)
Person Load (4.6ms) SELECT "people".* FROM "people" WHERE "people"."id" = ? LIMIT ? [["id", 2], ["LIMIT", 1]]
=> #<Person id: 2, name: "Oscarr", birthday: "1981-02-19", created_at: "2023-08-07 01:14:35", updated_at: "2023-08-07 01:14:35">
如果存在另一个同名条目,对我们来说无关紧要。我们的代码将选择id
等于2
的那个条目。既然我们已经选择了一个条目,那么让我们继续更新其内容。
更新记录
就像创建记录一样,我们有几种方法可以在数据库中更新记录。既然我们已经选择了名字中存在错别字的记录,让我们看看我们可以使用的第一个更新记录的选项。假设我们将以下代码输入到我们的 Rails 控制台中:
found_person
这将输出以下记录:
=> #<Person id: 2, name: "Oscarr", birthday: "1981-02-19", created_at: "2023-08-07 01:14:35", updated_at: "2023-08-07 01:14:35">
通过这种方式,我们可以确认这是我们想要修改的正确记录。要修改名称,我们将在 Rails 控制台中输入以下代码:
found_person.name = "Oscar"
这将只输出我们刚刚分配给 name
属性的字符串:
=> "Oscar"
但请记住我们之前所说的关于对象的话:这个更改仍然只是在内存中。我们需要持久化这个更改。我们将使用之前相同的 save
方法。让我们在 Rails 控制台中输入以下内容:
found_person.save
这将输出以下内容:
irb(main):019:0> found_person.save
(0.3ms) begin transaction
SQL (0.4ms) UPDATE "people" SET "name" = ?, "updated_at" = ? WHERE "people"."id" = ? [["name", "Oscar"], ["updated_at", "2023-08-07 02:14:06.188761"], ["id", 2]]
(0.9ms) commit transaction
=> true
这将把我们的更改保存到数据库中,并返回一个 true 值。这个 true 值将在下一章中非常有用。但就现在而言,我们可以通过打开 SQLite 浏览器并刷新视图来确认这个更改。现在应该显示正确的名称:
图 8.7 – 在 SQLite 浏览器中显示的人表修改条目
这是一种修改记录的方法。然而,还有另一种方法可以使用 update
方法。让我们在 Rails 控制台中用以下代码尝试一下:
found_person.update(birthday: "1980-02-19")
这将输出以下内容:
(0.4ms) begin transaction
SQL (0.4ms) UPDATE "people" SET "birthday" = ?, "updated_at" = ? WHERE "people"."id" = ? [["birthday", "1980-02-19"], ["updated_at", "2023-08-07 02:24:21.753388"], ["id", 2]]
(0.7ms) commit transaction
=> true
现在,让我们再次在 SQLite 浏览器应用中确认这个更改。让我们只是刷新视图,我们应该能看到更改显示出来:
图 8.8 – 在 SQLite 浏览器中显示的人表更新条目
因此,正如你所看到的,我们已经以两种不同的方式更新了我们的记录。再次强调,这两种方法都是有效的,你可以选择更适合你需求的方法。现在让我们看看最后一种方法(目前),即 destroy
方法。
删除记录
到目前为止,我们已经在数据库中创建了、选择了和更新了记录。接下来我们要查看的最后一种 Active Record 操作是销毁操作。你应该特别小心这个操作,因为这个操作会从你的数据库中删除数据,而无需事先进行任何确认。此外,这个操作是不可逆的——一旦执行了销毁操作,就无法撤销。所以,让我们首先在 Rails 控制台中用以下命令创建另一个条目:
Person.create( name: "Bernard", birthday: "1981-07-16" )
这应该输出以下内容:
(0.5ms) begin transaction
SQL (1.0ms) INSERT INTO "people" ("name", "birthday", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Bernard"], ["birthday", "1981-07-16"], ["created_at", "2023-08-07 02:37:29.615141"], ["updated_at", "2023-08-07 02:37:29.615141"]]
(0.4ms) commit transaction
=> #<Person id: 3, name: "Bernard", birthday: "1981-07-16", created_at: "2023-08-07 02:37:29", updated_at: "2023-08-07 02:37:29">
然后,再次在 SQLite 浏览器应用中刷新我们的视图:
图 8.9 – 在 SQLite 浏览器中显示的人表新条目
现在我们已经确认新条目存在,我们可以继续删除它。就像 update
方法一样,我们首先必须在数据库中选中一个条目。让我们在 Rails 控制台中输入以下内容:
person_to_delete = Person.find(3)
这应该输出以下内容:
Person Load (0.4ms) SELECT "people".* FROM "people" WHERE "people"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
=> #<Person id: 3, name: "Bernard", birthday: "1981-07-16", created_at: "2023-08-07 02:39:56", updated_at: "2023-08-07 02:39:56">
通过这种方式,我们确认我们已经选中了正确的记录。最后,让我们在 Rails 控制台中用以下代码删除记录:
person_to_delete.destroy
这将输出以下内容:
(0.5ms) begin transaction
SQL (0.5ms) DELETE FROM "people" WHERE "people"."id" = ? [["id", 3]]
(0.9ms) commit transaction
=> #<Person id: 3, name: "Bernard", birthday: "1981-07-16", created_at: "2023-08-07 02:39:56", updated_at: "2023-08-07 02:39:56">
如您所见,它生成了并执行了删除条目的代码。如果我们进入我们的 SQLite 浏览器应用程序并刷新视图,我们应该会看到该条目已不再存在:
图 8.10 – 如在 SQLite 浏览器中所示,已删除的人事表条目
如我们所见,该条目已被删除,并且永远消失。我无法强调这种操作在开发应用程序时有多么危险。我所能说的是,使用时要格外小心。
您可能已经注意到,所有数据库的数据操作都是通过 Rails 控制台完成的。这是因为我相信 Rails 控制台是理解和学习如何使用 Active Record 操作的最简单方式。一旦我们掌握了这些易于使用且直观的方法,那么在控制器和视图中应用这些知识将毫无困难。如果您想了解更多关于这些 Active Record 操作的信息,请查看 Active Record 基础知识页面:
guides.rubyonrails.org/active_record_basics.html
摘要
在本章中,我们学习了模型、迁移以及 Rails 控制台作为轻松操作数据库数据的首选工具。我们还学习了 Active Record 在 Rails 中的实现是多么有用,以及我们如何可以通过非常易于使用的命令与数据库进行通信。现在,我们已经准备好通过从数据库中获取数据并在视图中显示它来将这些知识综合起来,这正是我们将在下一章中要做的。
第九章:将一切整合在一起
到目前为止,我们已经看到了如何以某种分离的方式使用控制器、视图和模型。在上一章(模型、数据库和 Active Record)中,我们操作了数据库中的数据。然而,我们没有看到如何从控制器与数据库数据交互,更不用说如何将数据库数据加载到我们的视图中。在本章中,我们将看到一切是如何结合在一起的——也就是说,我们将从控制器加载一个模型并将模型数据传递给我们的视图,以便最终用户可以在浏览器上看到数据。我们还将做相反的操作,即将用户数据从视图传到我们的数据库,从视图开始到模型结束。此外,我们还将学习 Rails 方式执行这些操作,因为它们与我们用 PHP 做这些任务的方式大不相同。
考虑到模型、视图和控制器,在本章中,我们将涵盖以下主题:
-
使用生成器设置我们的初始应用程序
-
以 Rails 方式处理数据
-
不要,我再重复一遍,不要重新发明轮子
技术要求
为了跟随本章,我们需要以下内容:
-
任何用于查看/编辑代码的 IDE(例如,SublimeText、Visual Studio Code、Notepad++ Vim、Emacs 等)
-
对于 macOS 用户,您还需要安装 Xcode 命令行工具
-
已安装并准备好使用的 Ruby 版本 3.1.1 或更高版本
-
在您的本地机器上安装的 Git 客户端
本章中展示的代码可在 github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails/
找到。
项目准备
在设置我们的应用程序之前,我们将根据操作系统进行一些额外的配置。我们将配置分为两个部分——Windows 配置和基于 Linux 的系统(包括 macOS)。
Windows 配置
在第七章中,我们配置了我们的 Windows 环境使用 rbenv 来使用 Ruby 2.6.10。如果您还没有这样做,请回到 在 Windows 上安装 Ruby on Rails 部分查看,因为这是本章所必需的。对于 Rails 7(我们将在本章中安装),我们需要安装 Ruby 3.1.1 以及一些在 Windows 上不易获得的依赖项。我们将使用 Git SDK 的 bash shell(我们也在第七章中安装了它)来解决这个问题。所以,让我们打开一个 Windows PowerShell 并输入以下命令:
C:\git-sdk-64\git-bash.exe
这将打开一个 Git Bash 控制台,它看起来和表现得很像 Linux shell。让我们通过输入以下命令来确认我们是否有 Ruby 可用:
ruby –version
这应该会输出以下内容:
ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e) [x64-mingw32]
如果您不熟悉这个版本的 Ruby,那是因为它随 Git SDK 一起提供。现在,让我们使用以下命令安装 Ruby 3.1.3 的 bundler:
gem install bundler
接下来,让我们使用以下命令更新我们的系统 bundler:
gem update –system 3.3.3
现在,我们已经准备好设置我们的应用程序。
基于 Linux 的系统配置
对于 macOS 和基于 Linux 的系统(Ubuntu 和 Debian),我们将依赖 rbenv 来安装 Ruby 3.1.1。如果你还没有安装 rbenv,请参阅 第七章 以查看如何在 Linux 上安装 rbenv。rbenv 可用后,让我们在 shell 中使用以下命令安装另一个版本的 Ruby:
rbenv install 3.1.1
现在 Ruby 3.1.1 已经安装,让我们使用以下命令将默认 Ruby 设置为 Ruby 3.1.1:
rbenv global 3.1.1
让我们运行以下命令来确认是否激活了正确的 Ruby 版本:
ruby --version
这应该会输出以下内容:
ruby 3.1.1p18 (2022-02-18 revision 53f5fc4236) [x86_64-linux]
我们应该使用以下命令为这个版本的 Ruby 安装 bundler:
gem install bundler
现在,我们为我们的下一个 Rails 项目做好了准备。
设置我们的应用程序
对于这个练习,我们将有一个假设的场景,在这个场景中,我们是托马斯·A·安德森,我们在一家受人尊敬的软件公司工作。我们将扮演一个对 Rails 知识了解有限的初级网页开发者的角色,并且我们将被分配一个简单的任务。客户要求一个简单的地址簿结构,他们可以在其中保存朋友的联系信息——姓名、姓氏、电子邮件和电话号码。所以,让我们开始工作。确保你已经安装了 Ruby 3.1.1 或更高版本,否则我们可能会遇到项目问题。我们可以下载一个我们已有的模板应用程序,或者从 GitHub 上克隆它。如果你还没有这样做,打开终端并输入以下 git
命令:
git clone https://github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails.git
如果你已经这样做了,只需使用以下命令导航到我们项目中的 chapter09/rails7_original
文件夹:
cd From-PHP-to-Ruby-on-Rails/chapter09/rails7_original/
对于这个小型项目,客户要求我们使用 Rails 7,因为这是他们在其他项目中使用的版本。这也给了我们一个机会来实际看看 Rails 7 的运行情况。现在,让我们使用以下命令安装我们的项目依赖项:
bundle install
为了确认我们的设置正确无误,让我们运行以下命令:
bundle exec rails --version
输出应该类似于以下内容:
Rails 7.0.6
这就是我们的 Rails 开发工作开始的地方。我们将使用一些 Rails 魔法来生成我们大部分的代码。让我们生成一个包含我们即将调用的所有方法的 friends
控制器——index
、new
、edit
、update
、destroy
和 create
。让我们进入我们的 shell 并运行以下命令:
bundle exec rails generate controller Friends index new edit update destroy create
这应该会生成以下输出:
create app/controllers/friends_controller.rb
route get 'friends/index'
get 'friends/new'
get 'friends/edit'
get 'friends/update'
get 'friends/destroy'
get 'friends/create'
invoke erb
create app/views/friends
create app/views/friends/index.html.erb
create app/views/friends/new.html.erb
create app/views/friends/edit.html.erb
create app/views/friends/update.html.erb
create app/views/friends/destroy.html.erb
create app/views/friends/create.html.erb
…
来自 PHP 世界,尤其是 Laravel,你可能会觉得之前的命令有些熟悉。在 Laravel 中,你会使用以下命令生成一个等效的控制台:
php artisan make:controller FriendsController --resource
它们都生成了类似的功能(friends
控制器)和最后的 --resource
选项生成了正确的 HTTP 动词。在 Rails 中,我们刚刚运行的 generator
命令将其第一个参数作为控制器名称(Friends),其余作为控制器方法。该命令也非常详细地说明了它实际上生成了什么。它不仅创建了我们的控制器,还为命令传递的每个方法创建了一个视图。我们可以通过打开 app/views/friends/
文件夹来查看这一点。
图 9.1 – Friends 控制器生成的视图
此外,控制器生成器修改了我们的 routes.rb
文件,该文件定义了控制器内所有操作的 URL。如果我们打开 config/routes.rb
,我们会看到我们新创建的路由:
Rails.application.routes.draw do
get 'friends/index'
get 'friends/new'
get 'friends/edit'
get 'friends/update'
get 'friends/destroy'
get 'friends/create'
# root "articles#index"
end
虽然这样很好,但我们也可以用 Rails 的方式来做。让我们删除所有路由,只留下 do
块内的两行,所以我们的 routes.rb
文件现在看起来是这样的:
Rails.application.routes.draw do
resources :friends
root "friends#index"
end
虽然看起来很整洁,但稍微有点晦涩,因为我们(目前)还不知道这个资源调用具体做什么。资源调用为以下操作生成 RESTful 路由 – index
、new
、edit
、create
、update
和 destroy
。这仅仅意味着所有这些操作都应该使用正确的 HTTP 动词来调用 – GET
、POST
、PATCH
、PUT
和 DELETE
。我不想在这里给你太多信息,所以为了简化,我们只需说我们需要通过 URL 传递一些参数,还有一些其他参数需要从用户那里“隐藏”。如果你对 RESTful 及其用途感兴趣,请查看以下链接:
现在,下一步。让我们生成一个模型,它将在数据库中代表我们的“朋友”。我们将在 shell 上使用以下命令生成我们的模型:
bundle exec rails generate model Friend first_name:string last_name:string email:string phone:string
这将生成以下输出:
invoke active_record
create db/migrate/20230817022418_create_friends.rb
create app/models/friend.rb
invoke test_unit
create test/models/friend_test.rb
create test/fixtures/friends.yml
模型生成器创建了一个迁移来创建一个 friends
表。该表将包含 first_name
、last_name
、email
和 phone
字段。根据我们在上一章学到的知识,我们知道我们必须运行迁移来有效地生成我们的数据库结构。我们将在 shell 上运行以下命令来完成:
bundle exec rails db:migrate
如果这对您来说不是新内容,那可能是因为其他 PHP 框架有类似的工具。对于 Laravel,我们会执行以下命令:
php artisan migrate
对于 Symfony,我们会写以下命令:
bin/console doctrine:migrations:migrate
对于 CodeIgniter,我们会写以下命令:
spark migrate
我们可以从 PHP 框架中的这些示例中推断出,迁移工具已经在网络框架市场中存在了一段时间,并且它将保留下来。
我们设置的最后一步是之前章节中没有涉及的内容,它与 Rails 模型有关。为了帮助开发者尽快获得一个工作环境,Rails 集成了一个名为 database seeds 的工具。种子允许我们根据我们的模型结构生成测试数据。我承认我有点作弊,因为我给你提供了一个已经可以工作的种子文件。它在db/seeds.rb
文件中。让我们看看这个文件中的一个记录:
…
Friend.create(first_name: "rasmus", last_name: "lerdorf", email: "rasmus@email.com", phone: "+1(669)1111111")
…
这段代码相当直观。它创建了一个Friend
条目,包含名字(rasmus
)、姓氏(lerdorf
)、电子邮件地址(rasmus@email.com
)和电话号码(+1(669)1111111
)。当然,这些都是虚假数据,但当我们开始使用数据库时,它们将对我们很有用。最后一步是运行这个种子文件并将这些记录添加到我们的数据库中。我们在 shell 上使用以下命令来完成这个操作:
bundle exec rails db:seed
这个命令将生成种子文件中找到的五条记录。这是我们初始应用程序所需的所有设置。现在,是时候管理这些数据了。
处理数据
到目前为止,我们已经在数据库中手动修改了数据。通过手动,我的意思是所有操作都在 Rails 控制台中完成。然而,由于我们的项目需求是让用户处理friends
条目,我们将通过将我们的模型与控制器和视图集成来实现这一点,以便用户可以在一个友好的界面上查看friends
条目。我们将创建一个CRUD界面。是的,听起来很丑,但这正是软件工程师们想出来的缩写。它代表创建、更新、删除,这正是我们即将构建的——一个创建、更新和删除记录的界面。
设置 CRUD 界面
第一步是确认数据实际上在我们的数据库中。从我们之前的章节中,我们知道我们可以通过调用 Rails 控制台来做这件事,所以让我们通过运行以下命令来实现:
bundle exec rails console
这应该改变我们的 shell 外观,如下所示:
Loading development environment (Rails 7.0.6)
irb(main):001:0>
现在,让我们在这个控制台中输入以下命令:
Friend.all
这将显示数据库中的所有朋友条目。它应该显示类似以下的内容:
Friend Load (0.4ms) SELECT "friends".* FROM "friends"
=>
[#<Friend:0x00000001067cbe90
id: 1,
first_name: "rasmus",
last_name: "lerdorf",
email: "rasmus@email.com",
phone: "+1(669)1111111",
created_at: Thu, 17 Aug 2023 02:41:30.679843000 UTC +00:00,
updated_at: Thu, 17 Aug 2023 02:41:30.679843000 UTC +00:00>,
…
#<Friend:0x00000001068232f8
id: 5,
first_name: "david heinemeier",
last_name: "hansson",
email: "my5@email.com",
phone: "+1(918)5555555",
created_at: Thu, 17 Aug 2023 02:41:30.687162000 UTC +00:00,
updated_at: Thu, 17 Aug 2023 02:41:30.687162000 UTC +00:00>]
irb(main):002:0>
为了简洁,内容已被截断,但你应该能看到五条记录,这些记录与种子文件上的内容相对应。这个输出确认了数据已经在数据库中。现在,让我们在 shell 上输入以下命令来退出 Rails 控制台:
exit
现在,让我们使用以下命令启动我们的 Rails 应用程序:
bundle exec rails server
这个命令应该输出以下内容:
=> Booting Puma
=> Rails 7.0.6 application starting in development
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.6.6 (ruby 3.1.1-p18) ("Birdie's Version")
* Min threads: 5
* Max threads: 5
* Environment: development
* PID: 14464
* Listening on http://127.0.0.1:3000
* Listening on http://[::1]:3000
Use Ctrl-C to stop
最后,打开你选择的浏览器并访问http://127.0.0.1:3000/any
。这应该会显示以下 Rails 错误页面:
图 9.2 – Rails 路由错误页面
我故意提到打开一个不存在的路由来查看这个页面。每次你打开一个在 routes.rb
文件上未注册的路由时,Rails 都会显示这个错误页面,它显示了我们的应用程序中定义的所有路由。正如我之前提到的,资源调用为我们生成了许多与我们的 friends
组件相关的路由。我们将从 root_path
开始。让我们在这个错误页面上找到 root_path
的条目。一旦我们找到条目,我们就可以看到它对应于我们应用程序的根 URL (/
),每次我们通过浏览器访问这个路由时,都会执行 friends
控制器中的 index
方法,如最后列所示(friends#index
)。这个错误页面基本上是 URL 地址和它们将在控制器上执行的动作的映射。从这一页,我们了解到我们需要修改 app/controllers/friends_controller.rb
文件中 friends
控制器的 index
方法。我们应该在这个文件中看到以下代码:
class FriendsController < ApplicationController
def index
end
def new
end
def edit
end
def update
end
def destroy
end
def create
end
end
注意,所有这些操作都是由控制器生成器生成的,所以我们不是从零开始。此外,这些都是我们将构建以创建我们的 CRUD 界面的所有操作。
列出数据
让我们专注于 index
方法。我们想在 index
方法中显示所有记录,所以现在我们将加载 Friend
模型并选择数据库中的所有条目。考虑到这些更改,index
方法现在应该看起来像这样:
class FriendsController < ApplicationController
def index
@friends = Friend.all
end
…
在我们的 friends
变量前添加 @
符号,我们将该变量设置为实例变量。Rails 的工作方式是,这个实例变量随后被传递到视图中。请注意,这不是传递数据从控制器到视图的唯一方法,但它是一种非常简单的方法。现在,让我们回到浏览器,但将 URL 更改为 http://127.0.0.1:9000/
。我们应该看到以下屏幕:
图 9.3 – Rails 索引页面
但是等等!我们加载的数据没有显示出来。这是因为虽然我们确实有 Rails 的魔法,但我们没有 Rails 的奇迹。我们仍然需要在视图中工作。所以,让我们打开 app/views/friends/index.html.erb
视图,并添加我们从数据库中检索到的数据,使用 Friend
模型。我们的视图目前看起来像这样:
<h1>Friends#index</h1>
<p>Find me in app/views/friends/index.html.erb</p>
让我们删除这段代码,并在 @friends
变量内添加一个表格和一个循环。我们的视图代码现在应该看起来像这样:
<% @friends.each do |friend| %>
<%= friend.first_name %></br>
<% end %>
使用这段代码,对于数据库中的每个朋友条目(@friends.each do |friend|
),我们将每个条目重命名为friend
。注意代码标签(<% %>
)在代码的第一行。它们非常像我们用来在页面中嵌入 PHP 代码的 PHP 标签(<?php ?>
),而且就像 PHP 标签一样,这些标签内的任何内容都将被处理为 Ruby 代码。由于代码只是一个循环来获取每个数据库条目,所以第一行之后的任何代码都将为数据库中的每个条目重复,直到 Ruby 循环在第三行关闭。在第二行,我们使用了一组不同的标签(<%= %>
)。这些标签不仅将它们内部的内容作为 Ruby 代码处理,还将结果输出到浏览器。它们与 PHP 标签(<?= ?>
)完全一样。这有点像echo
语句,但是在浏览器上。现在,让我们刷新浏览器,应该会显示如下页面:
图 9.4 – 带有数据的 Rails 索引页面
哇!我们做到了。我们已经从数据库中加载数据到控制器,然后再到视图中。我不知道你怎么样,但第一次我看到这个的时候我真的很兴奋,不仅因为结果,还因为我理解了我是如何使用这个框架的。我希望你现在也做到了。不幸的是,这只是一个测试,以确保我们的数据被正确地加载到视图中。一个合适的网格将会有标题和更多的字段。让我们移除我们刚刚添加到视图中的代码,并添加以下代码:
<table border=1>
<tr>
<th>FIRST NAME</th>
<th>LAST NAME</th>
<th>ACTION</th>
</tr>
<% @friends.each do |friend| %>
<tr>
<td><%= friend.first_name %></td>
<td><%= friend.last_name %></td>
<td><a href="/friends/<%= friend.id %>/edit">
DETAILS</a></td>
</tr>
<% end %>
</table>
通过这个更改,前六行代码构建了一个表格和标题行。在第七行,我们创建了一个循环来迭代每个数据库条目。在循环内部,我们显示了名字和姓氏。最后,我们添加了一个链接到edit
操作。这是为了处理我们的下一个视图。如果我们刷新浏览器,我们应该看到以下页面:
图 9.5 – 带有数据的 Rails 索引表
现在,让我们着手我们的 CRUD 网络界面的更新页面。
更新数据
要编辑一个条目,我们首先必须选择该条目。如果我们仔细看看图 9.2,我们可以看到edit_friend_path
路由传递了一个参数(:id
)。与经典 PHP 不同,我们不是通过 URL 显式传递参数。相反,我们将它们嵌入到 URL 中,这样我们的路由就会是http://localhost:3000/friends/2/edit
而不是http://localhost:3000/friends?id=2
。在 Rails 中,我们很少使用显式的 URL 参数(?parameter_name=value
)。考虑到我们已经有了参数的名称,我们可以使用它来选择单个条目。让我们打开app/controllers/friends_controller.rb
中的friends
控制器上的编辑方法。该方法目前为空。它应该看起来像这样:
class FriendsController < ApplicationController
…
def edit
end
…
现在,让我们使用我们的模型通过 ID 选择单个用户。我们的代码现在看起来像这样:
class FriendsController < ApplicationController
…
def edit
@friend = Friend.find(params[:id])
end
…
现在我们已经选择了条目,让我们看看更多 Rails 魔法的实际应用。让我们打开app/views/friends/edit.html.erb
上的编辑视图。它应该看起来像这样:
<h1>Friends#edit</h1>
<p>Find me in app/views/friends/edit.html.erb</p>
现在,让我们删除前面的 HTML 代码,并用以下代码替换它:
<%= form_with model: @friend do |form| %>
<%= form.label :first_name %><br>
<%= form.text_field :first_name %><br>
<%= form.label :last_name %><br>
<%= form.text_field :last_name %><br>
<%= form.label :email %><br>
<%= form.text_field :email %><br>
<%= form.label :phone %><br>
<%= form.text_field :phone %><br>
<%= form.submit %>
<% end %>
Rails 为我们重复执行的任务提供了一套工具。这些工具被称为辅助函数。辅助函数是我们可以调用来为我们生成任务的函数。有许多类型的辅助函数,但就目前而言,我们将使用表单辅助函数,这些函数帮助我们构建用于数据处理的数据表单。
如果你对这个表单辅助函数的主题感兴趣,请参考 Ruby 指南:
guides.rubyonrails.org/form_helpers.html
在前面的代码中,我们选择了form_with
辅助函数来生成一个用于更新Friend
条目的表单。然后,在表单内部,对于我们在数据库中拥有的每个字段(first_name
、last_name
、email
和phone
),我们生成了一个标签和一个字段。最后,我们添加了一个提交按钮,以便将数据发送回控制器进行处理。现在,让我们打开我们的浏览器,点击首页上的第一个DETAILS
链接。这将带我们到http://127.0.0.1:3000/friends/1/edit
这个 URL,并且应该显示以下页面:
图 9.6 – 带有数据的 Rails 编辑表单
哇!用很少的代码,我们已经生成了一个显示当前字段值并允许我们修改这些值的表单。然而,我们仍然缺少修改数据库中任何这些值的代码。所以,让我们回到app/controllers/friends_controller.rb
中的控制器,但现在,我们将添加一个方法来帮助我们处理表单。
创建数据
我们类中的最后一个方法(create
)在类的末尾:
class FriendsController < ApplicationController
…
def create
end
end
让我们在create
方法之后添加一个名为friend_params
的私有方法。我们的代码应该看起来像这样:
class FriendsController < ApplicationController
…
def create
end
private
def friend_params
params.require(:friend).permit( :first_name,
:last_name, :email, :phone )
end
end
使用这个方法,当被调用时,我们让 Rails 知道通过表单发送的数据应该有一个friend
索引,并且在这个索引中,它可能包含first_name
字段、last_name
字段等。用 PHP 术语来说,这将相当于在以下数组中发送数据:
$_REQUEST['friend'] = array("first_name"=>"rasmus",…);
我对这个过程的简化可能有些过分,但本质上,这个方法需要并允许某些参数。
如果你想了解更多关于参数的信息,请查看以下页面:
现在,让我们实现这个friend_params
方法。请记住,为了显示编辑表单,我们调用的方法是edit
,但为了修改数据库中的数据,我们将调用update
方法。目前,update
方法是空的。让我们修改这个方法,使其现在看起来像这样:
…
def update
@friend = Friend.find(params[:id])
if @friend.update(friend_params)
redirect_to friends_path
end
end
…
使用@friend
变量,我们再次选择将要修改的记录。一旦选择了这条记录,我们就调用这个对象上的update
方法,数据库的更新就会在这里发生。最后,我们将使用redirect_to
辅助函数将用户发送到浏览器上的friends
页面。这段代码非常简洁,几乎就像句子一样——查找朋友和如果朋友使用朋友参数更新,则重定向到朋友路径。这是 Ruby 最精彩的地方。我们还没有分析这段代码片段中的friends_path
辅助函数。然而,它很简单。我们再次参考图 9**.2,其中我们看到一个包含所有定义的路由的表格。当我们在这个页面上找到friends_path
辅助函数时,我们可以确定,当我们使用这个别名时,我们可以将用户发送到正确的控制器和方法(friends#index
)。
现在,让我们继续创建一个新的friend
条目。就像编辑页面一样,让我们首先生成新的条目页面。在app/controllers/friends_controller.rb
文件中,让我们修改空的new
方法。它现在应该看起来像这样:
…
def new
@friend = Friend.new
end
…
由于我们将使用form_for
辅助函数,我们需要传递一个空的模型对象,以便辅助函数能够正确地生成表单。让我们打开app/views/friends/new.html.erb
中的new
视图,它目前看起来像这样:
<h1>Friends#new</h1>
<p>Find me in app/views/friends/new.html.erb</p>
让我们删除前面的内容,并用以下代码替换它:
<%= form_with model: @friend do |form| %>
<%= form.label :first_name %><br>
<%= form.text_field :first_name %><br>
<%= form.label :last_name %><br>
<%= form.text_field :last_name %><br>
<%= form.label :email %><br>
<%= form.text_field :email %><br>
<%= form.label :phone %><br>
<%= form.text_field :phone %><br>
<%= form.submit %>
<% end %>
你可能已经注意到,这与编辑页面的视图完全相同。大多数时候,我们不应该重复相同的代码,但由于我们还在学习 Rails,我们这次可以过关。现在,让我们回到app/controllers/friends_controller.rb
中的控制器,并修改create
方法。它是空的,但我们应该添加一些代码,使其看起来像这样:
…
def create
if Friend.create( friend_params )
redirect_to friends_path
end
end
…
就像update
方法一样,我们使用friend
参数调用create
方法来创建一个新的条目。一旦条目创建完成,我们就将用户重定向到friends
索引页面。现在,让我们在浏览器上试一试。在浏览器中打开http://127.0.0.1:3000/friends/new
URL,它应该显示与编辑页面相同的表单,但没有任何数据:
图 9.7 – Rails 新条目表单
让我们填写字段,如图所示:
图 9.8 – 带有数据的 Rails 新条目表单
当我们点击friends
索引页面时,它应该显示新创建的朋友:
图 9.9 – 带有新条目的 Rails 索引页面
好的,我们几乎完成了——只需再写几行代码。剩下的唯一一个方法就是destroy
方法,但它和其他我们已经编写的方法一样简单。
删除数据
让我们在app/views/friends/edit.html.erb
的编辑视图中打开,并在文件末尾添加另一个表单。文件末尾的表单应该看起来像这样:
…
<%= form_with model: @friend, method: :delete do |form| %>
<%= form.submit "DELETE" %>
<% end %>
注意,不要修改文件开头在 edit.html.erb
视图中已经存在的任何代码。我们应该在文件的 末尾 添加前面的代码。这个新表单生成一个删除按钮。注意我们传递了一个额外的参数 method:
,其值为 :delete
符号。这将自动使表单将数据发送到适当的 destroy
方法。现在,让我们在 app/controllers/friends_controller.rb
控制器上工作 destroy
方法。与其他我们迄今为止覆盖的方法一样,此方法应该是空的。让我们向其中添加以下代码:
…
def destroy
@friend = Friend.find(params[:id])
@friend.destroy
redirect_to friends_path
end
…
使用这个看起来不祥的代码,我们告诉 Rails 通过用户的 ID 选择一个用户,从数据库中删除记录,最后,将用户重定向到 friends
索引页面。现在,让我们在浏览器中尝试一下。让我们打开我们的浏览器并转到 friends
索引页面:http://127.0.0.1:3000/friends
。点击底部的最后一个链接,它应该打开与 Taylor Otwell 对应的编辑页面:
图 9.10 – 包含最新条目的 Rails 编辑页面
注意我们现在有一个 删除 按钮。让我们点击它,就像我们创建这个条目一样快,我们现在已经删除了它。表格现在应该看起来像这样(没有 Taylor Otwell 记录):
图 9.11 – 包含一个更少条目的 Rails 朋友索引页面
就这样,亲爱的读者,我们已经成功地为我们的朋友地址簿创建了 CRUD 接口。我们现在可以列出现有的朋友,创建新的,修改现有的,最后,删除我们不再需要的任何记录。为了避免我们在完成练习后对 app/controllers/friends_controller.rb
控制器的最终样子感到困惑,让我再次分享我们迄今为止所做的所有更改的整个文件:
class FriendsController < ApplicationController
def index
@friends = Friend.all
end
def new
@friend = Friend.new
end
def edit
@friend = Friend.find(params[:id])
end
def update
@friend = Friend.find(params[:id])
if @friend.update(friend_params)
redirect_to friends_path
end
end
def destroy
@friend = Friend.find(params[:id])
@friend.destroy
redirect_to friends_path
end
def create
if Friend.create(friend_params)
redirect_to friends_path
end
end
private
def friend_params
params.require(:friend).permit( :first_name, :last_name, :email, :phone )
end
end
虽然我们的应用程序现在可以处理记录,但它仍有改进的空间,例如以下内容:
-
我们可以使用导航栏或至少一个链接到“新”页面
-
我们应该重构表单视图,以便我们使用相同的表单来修改和添加记录
-
我们应该使用验证
-
如果任何操作失败,我们应该显示错误
我尝试将这些概念过度简化,以便更多地关注实用方法。然而,如果您对更详细的示例集感兴趣,Ruby on Rails 的指南始终可用:guides.rubyonrails.org/getting_started.html
。
前一网页中的示例将涵盖比本章中我们看到的内容更为详细的版本。您可以让每个人都感到自豪,并自行承担这些改进。
回到手头的任务——假设的客户接受了我们简化的 CRUD 接口版本。然而,有人提出了关于安全性的问题。他们不希望任何人都能看到他们在应用中的朋友条目。他们至少希望保护应用有一个登录页面。如果你在想我们需要编写这个认证组件,那就不要再想了,因为我们正好有这个工具。
不要,我重复一遍,不要重复自己
如果你以前使用过框架,你可能对不要重复自己(DRY)原则很熟悉,尽管这个原则更多地关注于编码和编码风格。如果你不熟悉,或者只是需要提醒,DRY 原则简单地说就是你不应该重复自己。
你可以在这里找到更多详细信息:
我们应该尽可能地尝试不重复我们的代码。以这个应用程序为例,我们在 edit
和 new
视图中重复了代码。使用 DRY 原则,我们应该重构我们的代码,使其对这两个动作使用相同的表单。同样地,而不是从头开始构建一切,你应该重用函数、工具,甚至整个库。作为开发者,我们反复做的一项任务是验证用户。如果你有一个有效的认证代码,你可能甚至是从以前的项目中复制过来的。然而,开源工具可以改进你的代码。使用开源工具处理认证的一个优点是它经过了比你自己能想象的更多场景的测试。使用开源工具的另一个原因是它可能已经准备好使用,并且很容易整合到我们的项目中。有几个用于用户认证的 gem,但到目前为止,我们将使用一个非常容易使用的 gem,称为 Devise:github.com/heartcombo/devise
。
Devise 是一个 gem,有趣的是,它为我们生成了一些 Rails 组件,我们可以用于我们的应用程序。Devise 将生成视图、路由和助手来帮助我们进行用户认证。所以,让我们将一个新的 gem 纳入我们的应用程序。第一步是停止 Rails 应用程序服务器。打开当前运行应用程序的 shell 并按下 Ctrl 键和 C。这应该停止 Rails 应用程序并将 shell 返回到正常状态。下一步是将 Devise gem 包含到我们的 Gemfile 中。让我们打开我们的 ./Gemfile
文件,位于我们项目的根目录(chapter09/rails7_original/Gemfile
),并在 Rails gem 行之后添加以下代码。现在,Gemfile 应该看起来像这样:
…
gem "rails", "~> 7.0.6"
gem "devise"
…
现在,让我们安装我们的 gem。让我们进入我们的 shell 并运行以下命令:
bundle install
命令应该输出与 devise gem 相关的消息:
…
Using responders 3.1.0
Using importmap-rails 1.2.1
Fetching devise 4.9.2
Using rails 7.0.6
Installing devise 4.9.2
Bundle complete! 11 Gemfile dependencies, 64 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
Post-install message from devise:
…
我们的 gem 已经安装,但它仍然需要运行额外的任务,我们仍然需要在我们的应用程序中添加一些配置,以便我们能够使用 Devise。让我们在我们的 shell 上运行install
命令:
bundle exec rails generate devise:install
本安装的结果是一系列指令,我们必须在能够在我们项目中使用这个 gem 之前执行:
create config/initializers/devise.rb
create config/locales/devise.en.yml
=============================================================
Depending on your application's configuration some manual setup may be required:
1\. Ensure you have defined default url options in your environments files. Here is an example of default_url_options appropriate for a development environment in config/environments/development.rb:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
In production, :host should be set to the actual host of your application.
* Required for all applications. *
2\. Ensure you have defined root_url to *something* in your config/routes.rb.
For example:
root to: "home#index"
* Not required for API-only Applications *
3\. Ensure you have flash messages in app/views/layouts/application.html.erb.
For example:
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
* Not required for API-only Applications *
4\. You can copy Devise views (for customization) to your app by
…
我们必须遵循这些安装后指令,以便 gem 能够正常工作。第一个任务指的是在我们的环境配置文件中添加一行配置。让我们这样做。让我们打开config/environment/development.rb
并添加以下行到其中。我们的config
文件现在应该看起来像这样:
…
# Don't care if the mailer can't send.
Config.action_mailer.raise_delivery_errors = false
config.action_mailer.perform_caching = false
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
在设置config.action_mailer.perform_caching
标志之后,我们添加了一行来设置config.action_mailer.default_url_options
标志。在生产环境中,我们刚刚添加的配置行将启用通过电子邮件进行密码恢复。由于这在本地环境中不起作用,我们可以忽略它,但配置仍然需要设置以使 Devise 正常工作。第二个指令指的是有一个根路由。我们也可以忽略这个指令,因为我们的应用程序已经包含了一个根路由。第三个任务要求我们修改我们的应用程序布局以包含错误和消息的 HTML 占位符。所以,让我们这样做。让我们打开app/views/layouts/application.html.erb
文件。这个文件控制着我们的应用程序的外观。任何需要在对应用程序进行一般级别的视图更改时,都应该去这个地方。让我们添加这些占位符,使我们的应用程序布局现在看起来像这样:
…
<body>
<p class="notice" ><%= notice %></p>
<p class="alert"><%= alert %></p>
<%= yield %>
</body>
…
当 Devise 发出有关登录过程的任何消息时,这些占位符将现在显示这些消息(如果有)。从安装后指令到使 devise gem 正确工作,我们现在是最后一个任务(编号 4),这个任务可以忽略,因为它指的是自定义我们的登录视图。我们几乎准备好使用 Devise 了,但如我之前提到的,这个 gem 需要一个数据库模型来保存我们的用户数据。所以,让我们在我们的 shell 上运行以下命令来做到这一点:
bundle exec rails generate devise User
这应该输出以下内容:
invoke active_record
create db/migrate/20230817200425_devise_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
insert app/models/user.rb
route devise_for :users
从分析这个输出中,我们可以看到已经创建了一个用户模型和一个数据库迁移。这个命令还把登录路由添加到了我们的应用程序中。记住我们在创建数据库迁移后做了什么?我们需要运行数据库迁移,以便将用户结构添加到数据库中。让我们这样做。运行以下命令:
bundle exec rails db:migrate
这将输出对数据库所做的更改:
== 20230817200425 DeviseCreateUsers: migrating ===================
-- create_table(:users)
-> 0.0015s
-- add_index(:users, :email, {:unique=>true})
-> 0.0004s
-- add_index(:users, :reset_password_token, {:unique=>true})
-> 0.0003s
== 20230817200425 DeviseCreateUsers: migrated (0.0023s) ===========
我们的 gem 现在已准备好被我们的应用程序使用。目前,让我们限制对应用程序的所有访问,以便没有登录就无法查看控制器上的任何方法。我们的app/controllers/friends_controller.rb
文件的开始部分现在应该看起来像这样:
class FriendsController < ApplicationController
before_action :authenticate_user!
Def index
…
通过添加第二行代码,before_action
辅助函数将在控制器执行任何其他操作之前执行用户认证。现在,是时候尝试一下了。让我们进入我们的 shell,并使用以下命令启动 Rails 应用程序服务器:
bundle exec rails s
一旦我们的应用程序启动并运行,我们应该回到浏览器并打开 http://127.0.0.1:3000/
,然后你会被要求输入一个电子邮件地址和一个密码:
图 9.12 – Devise 登录页面
如果你看到这个表单,这意味着 Devise 晶石正在工作。如果你尝试查看任何页面(例如,http://127.0.0.1:3000/friends/1/edit
或任何其他现有路由),你应该会被重定向到登录页面。使用 Devise 晶石创建用户有两种方式。我们可以使用我们的 Rails 控制台,或者我们可以直接注册。让我们使用注册方法。点击 注册 链接,它应该会带你到 注册 表单:
图 9.13 – Devise 注册页面
让我们添加一个电子邮件地址,admin@email.com
,以及 123456
作为密码。然后,当你点击 http://127.0.0.1:3000/
– 尝试一下。你应该能够再次看到首页:
图 9.14 – 认证的首页
作为我们的压轴戏,让我们添加一个注销链接,但只添加到我们的首页。打开 app/views/friends/index.html.erb
文件,并在文件末尾添加以下代码:
…
<%= form_with model: @user, url: destroy_user_session_path, method: :delete do |form| %>
<%= form.submit "Sign out" %>
<% end %>
现在,这个表单看起来相当熟悉。Devise 使用相同的 form_with
辅助函数来构建一个用于注销的表单。让我们回到我们的浏览器并刷新首页视图。它看起来像这样:
图 9.15 – 带有注销按钮的首页
现在,我们有一个 注销 按钮。如果我们点击它,我们的会话结束,我们再次被重定向到登录页面。我们可以使用之前创建的凭据再次登录。
恭喜!在这里我们的工作已经完成。和往常一样,总有改进和进一步学习的空间。这个钩子非常实用,它帮助我多次解决了应用程序的认证部分。你可能已经注意到的一件事(在众多事情中)是,尽管这个钩子使用会话值来处理认证,但你从未看到这些会话值,也从未直接处理它们。这就是 Rails 的做事方式。我们总是倾向于使用辅助工具隐藏应用程序的会话层。Devise 随带一些辅助工具,可以帮助你在代码中根据你的认证状态显示或隐藏组件。你可能想查看 Devise 文档页面上的 user_signed_in?
、current_user
和 user_session
辅助工具。此外,你还可以添加其他一些你可能想要添加到你的认证机制中的自定义设置。确保玩转这个钩子,并自己学习哪些其他配置可能对你有用。
摘要
哇!在这一章中,我们覆盖了大量的内容。我们学习了如何从模型加载数据到控制器,最终在视图中显示数据。我们还学习了如何通过 Rails 的 MVC 架构与表单交互并影响数据库。最后但同样重要的是,我们学习了如何将 Devise 钩子集成到我们的应用程序中,以利用我们的认证机制,而不是从头开始构建。
现在,我们已经准备好进入最后一章,我们将探讨一些关于在服务器上托管我们的 Rails 应用程序的相关附加信息。
第十章:关于托管 Rails 应用程序与 PHP 应用程序的考虑
恭喜你到达这个阶段。你已经创建了一个简单的 Rails 应用程序,它使用了控制器、视图和模型。你使用了生成器来准备你的开发。你还通过迁移设置了数据库。最后,你添加了一个 gem 来帮助你进行身份验证。现在,你很高兴地说,这个应用程序在你的机器上运行正常。现在,是时候将你的应用程序与世界(或可能只是客户)分享,这就在 PHP 应用程序和 Rails 应用程序之间存在着一个重要的区别:托管。
在本章中,我们将探讨发布我们的 Rails 应用程序时必须考虑的托管的一些方面。我们将比较不同的托管选项,并检查我们为 Rails 应用程序需要了解的额外概念。然后,我们将查看 Rails 框架根据环境设置如何表现。更具体地说,我们将查看生产环境中的错误报告。
考虑到 Rails 托管,在本章中,我们将涵盖以下主题:
-
PHP 与 Rails 在价格方面的比较
-
自己动手做,还是让别人帮你做?
-
为什么选择 nginx?
-
错误八卦
技术要求
要跟随本章内容,你需要以下这些:
-
任何用于查看/编辑代码的 IDE(例如,SublimeText,Visual Studio Code,Notepad++,Vim,Emacs 等)
-
对于 macOS 用户,你还需要安装 Xcode 命令行工具
-
已安装并准备好使用的 Ruby 版本 3.1.1 或更高版本
-
在我们的本地机器上安装 Git 客户端
本章中展示的代码可在github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails/
找到。
PHP 与 Rails 在价格方面的比较
在您对我这个不公平的比较提出异议之前,我要说明,我并不是直接比较 PHP 和 Rails。它们实际上是无法比较的,因为一个是编程语言,另一个是框架。一个更公平的比较应该是将 Laravel 框架与 Rails 框架进行比较。我试图传达的是一个 PHP 宇宙,其中许多工具都存在。但要做到这一点,我们必须回到 PHP 最终成为面向对象的时候。这种面向对象的 PHP 开辟了一个全新的可能性世界。在 2004 年,PHP 已经很流行了,而随着这个新特性(面向对象编程)的出现,它变得更加流行。WordPress 成为了管理博客和最终网站的标准。随着 PHP 开发出了更多的网络框架,如 CodeIgniter、Symfony、Laravel 等,这些框架也进入了视野。Joomla 和 Drupal 为开发者提供了另一种选择,这是一种框架和内容管理系统(CMS)的混合体。多亏了这种流行,每个人都提供了支持 PHP 的托管选项,如果我可以补充的话,价格非常便宜。随着其他技术的出现,这些技术的托管也变得可用,但成本要高得多。冒着听起来过于夸张的风险,我要说,我为一个客户在服务器上运行的应用程序,每年只需花费他们 20 美元。简而言之,到 2023 年,事情已经比 2004 年稍微平衡了一些,但您仍然可以找到非常便宜的 PHP 应用程序的托管选项,而(在大多数情况下)Rails 托管将稍微昂贵一些。快速搜索会给我们带来几个 Rails 托管的选项,包括以下更受欢迎的一些:
-
DigitalOcean (
www.digitalocean.com/
) -
Amazon Lightsail (
aws.amazon.com/lightsail/
) -
Heroku (
www.heroku.com/platform
) -
AWS (
aws.amazon.com/
) -
Google Cloud (
cloud.google.com/
) -
Azure (
azure.microsoft.com/
)
在成本方面,Amazon Lightsail 可能是您能找到的(在撰写本文时)最具成本效益的选项,其计划费用低至每月 3.50 美元。这个选项提供了一个简单易用的环境,背后有亚马逊网络服务(AWS)的支持,但避免了复杂性。DigitalOcean 提供了一个价格合理且灵活的选项,起价为每月 6 美元,他们亲切地称这种虚拟机为 Droplet。此计划包括一个 PostgreSQL 数据库。Heroku 的价格稍高,起价为 5 美元,但该价格不包括数据库。一个小型数据库每月额外增加 7 美元,总计 12 美元。最后两个选项我认为仅适用于更高级的需求和可能更高级的客户。它们(最初)可能很便宜,但随着需求的增加,成本也会上升。此外,在任何一个云服务中(无论是 AWS、Google Cloud 还是 Azure)设置一切可能是一个真正的挑战,因为您需要完全理解云生态系统的元素,而其他选项则为您的应用程序提供了一个现成的用户界面。
此外,根据您的应用程序的功能,成本会有所不同,您直到月底才能真正知道您会被收取多少费用。他们确实有内置的成本计算器和提供或多或少精确的月度估计,但由于他们为一切收费(虚拟机的执行时间、网络、数据传输、磁盘使用、固定 IP 以及许多其他事物),因此成本会逐月变化。您对应用程序的部署和执行有绝对的控制权,但这需要付出代价。然后还有最坏的情况,如果您的安全受到威胁,攻击者可能会控制您的服务器以及您的整个基础设施,可能因为一个简单的错误而让您损失数千美元。这并不是说您不应该考虑云选项作为可行的选择。它们只是不是初学者甚至中级开发者的选择。它们是那些能够处理风险并从其使用中受益的公司企业的解决方案。让我们更详细地看看这些捆绑式解决方案(DigitalOcean、Amazon Lightsail 和 Heroku)与自行构建的选项(AWS、Google Cloud 和 Azure)之间的差异。
DIY 或让别人为您完成
我们已经看到了,在成本方面,我们可供选择的不同托管选项。然而,在确定和实施解决方案时,这并不是唯一需要考虑的区分因素。它们在哲学和用途方面也有所不同。目前,我们将坚持使用一个捆绑、包装并准备好使用的选项。通过捆绑解决方案,我指的是一个提供安装、配置并准备好使用的服务器的解决方案。为了简单起见,我选择了 DigitalOcean 并部署了我们的小型应用程序,但请放心,其他捆绑选项在实施上将会非常相似。由于这会产生成本,我不会添加跟随步骤,所以您只需坐下来享受这个过程。通常,您可以从市场上选择一个优化的服务器镜像。大多数捆绑解决方案都有自己的市场,您可以在其中搜索并选择适合您需求的镜像(或模板)。在我们的案例中,我们可以访问marketplace.digitalocean.com/
并查看可用的选项。当然,它们将提供经典的Linux-Apache-MySQL-PHP(LAMP)镜像,但我们感兴趣的是 Ruby-on-Rails 的对应版本,我们在这里可以轻松找到:
marketplace.digitalocean.com/apps/ruby-on-rails
一旦我们选择了将使用的技术,我们就可以查看这个虚拟机(或 Droplet)包含的内容:
图 10.1 – Rails 虚拟机
获取捆绑解决方案的最大优势是您不必担心安装 Ruby、Rails gem 以及我们可能需要的任何其他依赖项。正如我们在图 10.1 中可以看到的,这个虚拟机将包含 Rails 7.0.4.2 以及我们应用程序运行所需的所有依赖项。现在,我们面前有一个 shell,我们可以登录或者将我们的应用程序文件传输到这个服务器上。它甚至提供了一个已经配置好的示例应用程序,这样我们就可以确信 Rails 已经安装并准备好使用。当我浏览 DigitalOcean 为我提供的 IP 时,我遇到了这个熟悉的页面:
图 10.2 – DigitalOcean 上的 Rails 示例应用程序
部署我们的应用程序足够简单,只需要我们在本地执行的相同步骤:
-
将源代码复制到服务器上。
-
运行 Rails 迁移。
-
运行 Rails 种子。
-
添加一个用途
哇哦:
图 10.3 – 在 DigitalOcean 上部署的 Rails 应用程序
我们之前在本地运行的同一应用程序现在已部署到托管解决方案。尽管有一些细微的差别,但我可以保证其他捆绑选项(如 Heroku 或 Amazon Lightsail)将提供类似的环境来运行我们的应用程序。这个过程(部署)的自动化超出了本书的范围,但如果你对更复杂的部署选项感兴趣,我建议你查看 Jenkins、Docker 和持续集成/持续交付(CI/CD):
为了简化我们的应用程序,我们选择了使用 SQLite 数据库。它易于安装和本地实施,但在现实世界的场景中,我们会选择更健壮的选项,如 MySQL 或 PostgreSQL。这个 Droplet 还附带了一个即用型 PostgreSQL 数据库。这正是任何捆绑选项为我们提供的。
总结一下,捆绑选项的优势如下:
-
可以立即设置
-
环境即用
-
依赖关系已解决
-
强健的数据库已准备就绪
然而,这个解决方案可能并不适合每个人。大多数捆绑解决方案在 Ruby 方面具有灵活性,但在操作系统方面则没有。这些解决方案提供商可以并且会删除过时的包。一些解决方案提供商甚至可以在未经你许可的情况下更新包(出于安全目的),这可能会影响你应用程序的行为。此外,当你需要自定义服务器时,这可能是你考虑非捆绑解决方案选项的信号。
DIY
与捆绑选项的托管服务相比,DIY 选项为您提供了更大的灵活性。为什么我们需要这种灵活性呢?有许多情况下这是必要的,但我想出的一个具体用例是扩展应用程序的资源。比如说,如果你的应用程序最初是为服务 500 个用户而设置的,但现在你注意到你的数据库中有 10,000 个用户。你的应用程序现在非常慢,但为 10,000 个用户增加服务器的规模(内存和 CPU)将非常昂贵。你也注意到并发用户数(即同时使用应用程序的用户数)达到了 4,000,而不是 10,000。这时,自动扩展就派上用场了!
自动扩展会在特定时刻确定资源需求,并在并发用户达到峰值时自动扩展,当用户并发下降时再缩小规模。通过 扩展 我指的是平台在需要处理更多用户时即时创建更多服务器。由于托管服务只在那些额外服务器实际运行时向您收费,整个过程可以为公司节省资金,因为您不需要一直开启整个服务器军团,而只是在用户数量达到峰值时才开启。此外,采用这种方法,您的应用程序可用性也会提高。我在简化整个过程,但我确实希望您能理解我的意图。这只是一个非捆绑式解决方案最适合的许多场景之一。如果您想了解更多关于自动扩展的信息,我建议您查看 AWS 官方页面上的自动扩展部分:
我打算为我们的假设解决方案选择 AWS,但同样也可以在任何其他云平台上实现。要在 AWS 上部署我们的简单应用程序,我们会创建一个 EC2 实例并添加安全组。
一旦我们启动了实例,我们会等待它启动,这可能需要几分钟。一旦我们的实例准备就绪,我们会通过 SSH 访问它,然后开始我们的 Ruby 配置。这包括安装 Ruby、bundler、GCC 编译器——所有这些。一旦 Ruby 准备就绪,我们会安装我们的 Rails 晶石。然后我们会将我们的应用程序代码上传到我们的虚拟机,并运行初始设置(安装依赖项、Rails 迁移、Rails 种子以及用户设置)。然后我们会启动服务器。从这里开始,事情会变得越来越复杂。我们会有一系列选项来使这个应用程序对世界可用。一个选项是将端口 3000
打开给世界,然后我们就出发了。然而,Rails 服务器永远不应该直接暴露给外界。我们将在下一节中看到这是为什么,但现在,让我们坚持这个原则。我们需要设置 Nginx 作为我们的 web 服务器,并让它将调用重定向到我们的 Rails 应用。一旦准备好了,我们就可以出发了。
如你所见,这似乎比捆绑选项更复杂。在这个简单的例子中,自己构建它并没有明显的优势,这是因为它实际上并没有给我们带来任何优势。至少,不是针对我们特定的案例——首先,因为 AWS 始终是一个昂贵的选择,其次,因为它是一个企业级解决方案,因此,就像大多数企业级解决方案一样,配置和部署将会更复杂,尽管这将为你的应用程序提供更多的选择。虽然我不会推荐将我们的应用程序部署到任何 DIY 公共云(AWS、Google Cloud 或 Azure)以供全世界分享,但我建议你将其作为一个练习来做,因为这会使你熟悉云解决方案的操作。如今,熟悉这类技术对于开发者来说是必须的。
虽然我们目前并不真的需要深入了解这些解决方案的细节(至少现在还不需要),但我相信我们确实需要掌握一些关于我们应用程序部署的概念。在我们的应用程序后面部署一个网络服务器(Nginx)就是这些概念之一,我们将在下一节中更深入地探讨这一点。
为什么是 nginx?
来自 PHP 背景的我,发现 Rails 缺乏 PHP 中常见的开箱即用的服务器功能是一个难以理解的概念。在 PHP 中,如果它被安装了,你可以在 shell 中打开它并输入以下内容:
php -S localhost:9000
这将启动一个内部 PHP 服务器。现在你可以打开一个浏览器,将其指向http://localhost:9000/
,这就足够了。我们添加到启动 PHP 服务器同一文件夹中的任何 PHP 脚本都将对该服务器可用。我们不需要任何 PHP 框架就可以开始用 PHP 编程。我们可以使用这个内部服务器进行开发,一旦我们将 PHP 应用程序部署到生产服务器,我们的应用程序只需要一个启用了 PHP 的网络服务器。这只是一个过于简化的实际操作过程,但本质上,这就是 PHP 服务器所需的所有内容。
在那些美好的日子里,Apache 是首选。如今,你仍然可以使用 Apache,但 Nginx 在这里已经取得了很大的进展,以至于 Nginx 现在几乎(如果不是)成为了标准。那么什么是 Nginx 呢?Nginx(根据其网站)是一个高级负载均衡器、Web 服务器和反向代理,旨在处理高流量网站和应用。它被广泛用作 Web 服务器,据报道已被 Netflix 和 Airbnb 等公司采用。由于这些公司的参与,我们可以安全地假设它在处理与 Web 相关组件时的效率极高。为什么我们应该将 Nginx 用作 Rails 的 Web 服务器呢?答案是委托。Web 服务器应该处理诸如静态资源(图像、样式表等)、文件上传、SSL 证书显示以及一些 DDoS 保护等任务,仅举几个例子。Rails(以及许多其他 Web 框架)也非常擅长处理这些任务中的大多数,但为什么要在我们能够使我们的 Rails 应用程序只需关注 Rails 任务,而 Nginx 关注 Web 相关任务时重新发明轮子呢?此外,Nginx 在处理这些任务方面比任何 Web 框架都要快得多、效率更高。事实上,我们的演示捆绑式托管解决方案(DigitalOcean)已经安装并配置了 Nginx,指向示例 Rails 应用程序。我禁用了 Rails 上游,并将 Nginx 页面设置为默认页面,这就是我得到的结果:
图 10.3 – DigitalOcean 上的 Nginx 默认页面
设置 Nginx 配置的任务是选择捆绑式解决方案的另一个原因——DIY 解决方案将安装和配置 Nginx 的任务留给了我们。虽然这绝对不是一项复杂的工作,但它确实给 DIY 解决方案增加了另一层工作量。我意识到在考虑托管选项时,这些信息量很大,但如果你想了解更多关于 Nginx 和相关主题的信息,请查看以下网站:
最后,我们需要记住,Rails 在本地运行时与在生产环境中运行时的行为会有所不同,这包括我们在服务器上查看错误的方式。
错误八卦和遗言
到目前为止,我们只使用了 Rails 框架的大部分默认选项。这些默认选项包括一些配置,否则会阻碍并减慢最终用户的整体体验。这些选项在开发中是有意义的,因为它们使得调试和测试变得非常容易。作为开发者,我们需要尽快设置我们的本地环境并开始编程。然而,这些默认选项在生产环境中并不合适。其中之一是关于错误报告的默认设置。我们在本地看到了错误是如何显示的,但在现实生活中,我们绝对不希望以同样的方式显示这些错误。我们不希望有详细的错误输出,包括路径、变量,甚至我们使用的数据库,因为这可能是安全漏洞,至少是开始。我们希望错误就像八卦一样:安静且在我们背后。这正是我们要做的。对于这个最后的练习,我们需要像前几章那样加载一个 Rails 应用程序。如果您还没有下载课程的源代码,请打开终端并输入以下git
命令:
git clone https://github.com/PacktPublishing/From-PHP-to-Ruby-on-Rails.git
如果您已经这样做,那么只需通过运行以下命令进入您项目中的chapter10
文件夹:
cd From-PHP-to-Ruby-on-Rails/chapter10/hosting_original/
再次,让我们使用以下命令安装我们的依赖项:
bundle install
为了确认我们的设置是否正确,让我们运行以下命令:
bundle exec rails –version
输出应该类似于以下内容:
Rails 7.0.6
现在我们已经准备好在生产中运行我们的 Rails 应用程序。我们通过在 Rails 启动命令之前添加RAILS_ENV=production
环境变量来实现这一点。让我们试一试。在我们的 shell 中,让我们输入以下内容:
RAILS_ENV=production bundle exec rails server
一旦我们运行前面的命令,我们的应用程序将像往常一样运行,我们的 shell 将看起来像这样:
=> Booting Puma
=> Rails 7.0.6 application starting in production
=> Run `bin/rails server --help` for more startup options
Puma starting in single mode...
* Puma version: 5.6.6 (ruby 3.1.1-p18) ("Birdie's Version")
* Min threads: 5
* Max threads: 5
* Environment: production
* PID: 6073
* Listening on http://0.0.0.0:3000
Use Ctrl-C to stop
但现在,如果我们去我们的浏览器并导航到http://0.0.0.0:3000
,我们现在看到的是这个页面:
图 10.4 – Rails 在生产模式运行
当然,对于开发来说,这可能是个麻烦,但请相信我,当这个页面出现在您生产部署的应用程序上时,您会感到非常感激。因为我们正在以生产模式运行应用程序,所以所有错误都被隐藏了。现在我们需要在我们的日志中查找错误。到目前为止,我们忽略了日志文件,因为错误总是显示在浏览器上,但日志始终存在。如果我们查看chapter10/hosting_original/log/
文件夹,我们现在有两个文件:
图 10.5 – Rails 日志文件
由于我们在生产环境中运行应用程序,让我们打开production.log
文件。如果日志中信息过多,我们可以删除文件内容,保存文件,然后刷新浏览器。现在我们可以用更少的数据再次查看文件。我们现在应该看到如下内容:
INFO -- : Started GET "/users/sign_in" for 127.0.0.1 at 2023-08-31 22:28:41 -0700
INFO -- : Processing by Devise::SessionsController#new as HTML
INFO -- : Completed 500 Internal Server Error in 5ms (ActiveRecord: 0.5ms | Allocations: 981)
FATAL -- :
ActiveRecord::StatementInvalid (Could not find table 'users'):
…
我们注意到FATAL
关键字,这就是导致我们错误的原因。错误指出我们找不到'users'
表。这是因为我们没有运行 Rails 迁移。所以,让我们来做这件事。通过在 shell 上按Ctrl + C停止我们的应用程序,然后在 shell 上运行以下命令:
RAILS_ENV=production bundle exec rails db:migrate
不要忘记在命令前加上环境前缀。命令应该会输出详细说明已创建表的输出:
I== 20230817050336 CreateFriends: migrating ======================
-- create_table(:friends)
-> 0.0005s
== 20230817050336 CreateFriends: migrated (0.0005s)===============
== 20230817200425 DeviseCreateUsers: migrating ===================
-- create_table(:users)
-> 0.0006s
-- add_index(:users, :email, {:unique=>true})
-> 0.0001s
-- add_index(:users, :reset_password_token, {:unique=>true})
-> 0.0001s
== 20230817200425 DeviseCreateUsers: migrated (0.0008s) ==========
现在让我们再次在 shell 上输入以下命令来重启我们的 Rails 应用程序:
RAILS_ENV=production bundle exec rails server
现在,刷新浏览器。看起来一样,只是出现了一个错误页面。让我们再次删除production.log
文件的内容,并再次刷新浏览器。现在当我们再次查看日志文件时,我们看到一个不同的FATAL
错误:
…
FATAL -- :
ActionView::Template::Error (The asset "application.css" is not present in the asset pipeline.
):
…
我为了清晰起见移除了日志的其余部分,但FATAL
错误正是给我们提供问题线索的地方。在 Rails 中,当我们以生产模式执行应用程序时,一些资源需要被编译或转换。在这种情况下,需要生成和压缩 CSS 和 JavaScript 资源。幸运的是,Rails 有一个命令来完成这个任务。为了完成这个任务,我们需要在 shell 上使用Ctrl + C停止我们的应用程序,然后运行以下命令:
RAILS_ENV=production rails assets:precompile
这应该会输出一系列消息,表明资源已经被生成:
INFO -- : Writing hosting_original/public/assets/manifest-b84bfa46a33d7f0dc4d2e7b8889486c9a957a5e40713d58f54be71b66954a1ff.js
INFO -- : Writing hosting_original/public/assets/manifest-b84bfa46a33d7f0dc4d2e7b8889486c9a957a5e40713d58f54be71b66954a1ff.js.gz
INFO -- : Writing hosting_original/public/assets/application-e0cf9d8fcb18bf7f909d8d91a5e78499f82ac29523d475bf3a9ab265d5e2b451.css
…
生成了很多文件,但我们不会深入探讨其工作机制。简单来说,前端资源已经被生成。现在,让我们再次运行我们的应用程序,使用以下命令:
RAILS_ENV=production bundle exec rails server
当我们再次刷新浏览器时,我们应该看到一个熟悉的页面:
图 10.6 – Rails 登录页面
恭喜,亲爱的读者们!我们现在正在以生产模式运行 Rails 应用程序。正如你所见,调试我们的应用程序变得更加麻烦,因为错误信息现在隐藏在日志中。在真正的生产环境中,我们可能需要查找这些日志被保存在哪里。运行 Rails 任务(数据库、生成器、资源等)可能也不会那么直接。你可能需要调整命令,在某些情况下,甚至不允许运行它们。无论如何,现在这条路已经走到了尽头,我为读者们能坚持与我一起完成这个 Ruby-on-Rails 之旅而鼓掌。
摘要
在本章中,我们涵盖了大量的信息。我们学习了托管成本、市场上的不同选项,以及选择为我们配置好的一切的捆绑解决方案是否合理,或者选择 DIY 选项,它提供了更多的灵活性,但也更加复杂和昂贵。我们还学习了为什么我们应该在 Nginx 后面部署我们的应用程序。最后,我们学习了如何在生产模式下调试应用程序。
我们现在可以使用这本书中学到的知识,并将 Ruby 添加到我们的武器库中。