如何使用和不使用Rails库构建Web应用

250 阅读30分钟

如何使用和不使用Rails库来构建一个Web应用

Ruby on Rails是一个网络框架,它包含了许多你需要创建和部署一个成功的网络应用程序的库。我们经常想当然地认为,运行rails new ,创建一个具有大量内置功能的网络应用。对我们中的许多人来说,这已经很好了,因为Rails的目标是神奇地让所有东西都工作,而不需要知道引擎盖下发生了什么。

为了充分欣赏Ruby on Rails的所有魅力,我们首先需要了解它所要解决的问题。让我们想象一个不存在Rails的世界。这很难,我知道。

我们将如何建立一个只使用标准Ruby库的网络应用?在这篇文章中,我将分解网络应用程序如何工作的关键基础概念,同时从头开始构建一个网络应用。如果我们可以只使用Ruby库来构建一个Web应用,那我们为什么还需要像Rack这样的Web服务器接口和像Ruby on Rails这样的Web应用?在本文结束时,你会对Rails和它的魔力有一个新的认识。

旅程

我将涵盖的主题包括:网络协议(TCP和HTTP)、持久化数据存储、网络服务器接口(Rack)和Rails库(Action Controller、Action Dispatch、Active Record和Action View)。在教程的前半部分,我们将只使用核心的Ruby库,这足以让我们了解网络应用程序工作的关键概念。在后半部分,我们将用Rails的模块代替代码。

在我们开始之前

我的假设是,亲爱的读者,你知道如何使用文件编辑器和浏览终端。你还应该熟悉与互联网有关的术语,如网络浏览器。熟悉Ruby或Ruby on Rails和数据结构并不是必须的,但会有很大的帮助!最后,我们假设你是在一台装有MacOS的电脑上学习,并且知道如何安装和使用Ruby gem

在任何情况下,我都强烈建议跟随代码样本并在你的电脑上运行代码以获得完整的学习体验。毕竟,没有体验就没有学习。

Mirth网络应用程序

复杂的Mirth网络应用程序在运行

我们今天要建立的网络应用程序叫Mirth,是一个网络应用程序,它的功能是让用户看到显示的数据,并输入新的数据,这些数据将在应用程序中持久存在。你想让数据看起来如何,由你自己决定。本教程中的Mirth网络应用程序将是一个生日跟踪器。与其依赖某个社交媒体来通知我们人们的生日,你现在可以询问他们,记录下来,并在列表中查看。

与网络的对话

现在,我们把一切都安排好了。让我们从网络应用程序工作的基本原理开始。网络应用程序或服务器是如何与网络对话的?像人类一样,联网的应用程序有一个特定的协议,它们用来相互通信,称为传输控制协议(TCP)。你以前可能已经听说过TCP,但TCP在网络应用中的作用是什么?

要了解TCP的作用,我们必须首先了解互联网协议(IP)。你很快就会发现,抽象层次是理解其工作原理的一个主题。TCP只是IP的一个抽象,是一个较低级别的协议,用于传输单个数据包。

虽然IP是互联网工作的基础,但它是一个原始协议,因此不可靠。使用IP会带来一些风险,如数据包传输的顺序错误或根本没有传输。TCP建立在IP之上,通过额外的功能使系统更加可靠。为了解决数据包交付顺序错误的问题,TCP允许发送方在数据包内添加一个自动递增的数字,并在接收方的一端重新组合。TCP增加的另一个关键功能是套接字的概念。

创建套接字

套接字是用来表示服务器和客户端之间的持久性连接。Ruby有一个核心库叫 [sockets](https://ruby-doc.org/stdlib-2.7.0/libdoc/socket/rdoc/Socket.html) 的核心库,它建立在你的操作系统的TCP实现之上(顺便说一下,这又是一个抽象!)。

使用Ruby的Socket库

socket 库允许你创建一个TCP服务器套接字。在我们的例子中,Mirth的服务器套接字要注意任何进入的连接。但是,一个连接如何知道要与哪个应用程序连接?TCP在一个特定的端口上创建一个套接字,这个端口可以被识别为使用它的应用程序。在本例中,我们指定Mirth的端口为1337。

接受进入的连接

现在服务器套接字已经创建,我们决定当连接建立时我们要给客户什么。在一个循环中,我们通过#accept ,接受任何客户端。一旦客户端被接受为一个连接,我们就会以IO对象的形式接收信息。

从客户端获取输入

一旦你接受了客户端的连接,现在你可以从客户端获得任何输入。在这个例子中,我们问客户端 "你叫什么名字?",然后检索并转述一个修改过的回答。

关闭客户端套接字

在我们完成对客户端的响应后,我们必须关闭连接。

运行代码以创建服务器套接字和客户端

查看完整的代码片段,mirth-1.rb 在Github上:

  1. 要在你的电脑上运行这段代码,请运行该文件,ruby mirth-1.rb 。通过这样做,你创建了一个服务器套接字。现在你需要一个客户端来连接到服务器。
  2. 打开一个新的终端窗口,运行nc localhost 1337 。现在你已经使用netcat连接到你的本地服务器套接字,端口号为1337。随后,在连接建立后输入任何信息,都会产生我们在代码中的打印语句。

与网络进行更多结构化的对话

现在你已经能够创建一个TCP套接字,你已经准备好进一步的抽象了。目前,我们的TCP服务器仍然不能与浏览器正常通信。这是因为网络浏览器使用的是另一种协议的语言,即超文本传输协议(HTTP)。TCP是与应用无关的,因此任何类型的应用都可以使用TCP。另一方面,HTTP是专门针对网络的。因此,我们实际上可以自己在Ruby中实现HTTP。

网络协议的抽象层

世界上有许多类型的浏览器和服务器,这就是为什么我们有HTTP。这个协议是由HTTP工作组建立的,所以当涉及到网络浏览器时,世界上的每个人都可以用同样的语言说话。

每一条HTTP消息都有相同的结构:

  • 一个起始行
  • 零个或多个标题字段
  • 一个空行
  • 一个可选的信息体

HTTP消息分为两种类型:请求和响应。网络浏览器可以向服务器提出一个页面的请求,而服务器则在其响应中提供所请求的页面。请求和响应的区别只在于HTTP消息的start-line ,其中请求有一个request-line ,响应有一个status-line 。这些请求和响应的结构在一个叫做RFC的文件中被详细规定。

让我们把这部分内容分解成请求和响应。

提出请求

从Mirth网络应用程序的角度来看,它希望在连接后准备好为客户端提供所要求的信息。客户端现在要做的不是像前面的例子那样提供一个你的名字的字符串,而是提出HTTP请求。

HTTP请求的关键部分是请求行,它包含了网络应用程序要采取行动的重要信息。这些信息是方法令牌、目标路径和HTTP协议版本号。方法令牌是来自服务器的请求类型,两个常见的类型是GET(获取数据)和POST(发布数据以在服务器上进行更改)。正如你所期望的,方法令牌将代码引向不同的路径,这取决于请求的内容。目标是请求的来源路径。例如,服务器从一个请求信息的用户那里得到的目标路径,www.mirth.com/baked-brownies/123baked-brownies/12 。最后,版本号是网络浏览器正在使用的HTTP版本,最常见的是HTTP/1.1.

获取请求的请求行

一旦服务器接受了客户端的套接字,我们就会读取一个请求的请求行。与我们之前使用的#gets 方法不同,如果没有更多的输入可以获取,#readline 会返回一个错误。

分解请求的请求行

我们将HTTP请求分解成几个部分,即method_token, targetversion_number 。所有这些部分对于服务器来说都是如何响应请求的基本信息。这段代码简单地打印了我们从HTTP请求中得到的信息,并将其返回到HTTP请求的正文中,显示在网页浏览器的页面上。

运行代码以发出请求

在GitHub上查看完整的代码片段,mirth-2.rb :

  1. 在这个练习中,运行ruby mirth-2.rb
  2. 我们不是从另一个套接字发出请求,而是直接从浏览器发出请求。打开你的浏览器并输入localhost:1337/cakes-and/pies 。你的浏览器不会有任何显示,因为服务器还没有回应。相反,在你的输出中记下记录的字符串。

对请求的响应

我们下一个代码片段的关键部分是http_response 变量,其中包含:

  • 起始行
  • 标题字段
  • 一个空白处
  • 主体,包含将由浏览器呈现的信息。

HTTP响应的第一部分是代表起始行的状态代码。在我们的例子中,我们想确保浏览器一切正常,所以我们把状态代码定为201 OK 。如果服务器有任何问题,浏览器就能将这些信息转达给用户,这就解释了当你访问一个糟糕的网页时最常见的404 Error 状态代码。

HTTP响应中还有其他值得注意的部分,比如头字段。头部字段是由键值对组成的行,在我们的例子中,Content-Type: text/html 。你可以在RFC文档页面找到其他头的列表。

头域的类型在请求和响应之间可能有所不同,但对于响应来说,头域是你输入关于响应的任何额外信息的地方,比如重定向的路径、缓存和cookies,以及与安全相关的细节。浏览器读取头字段并采取相应的行动,如重定向到适当的页面,存储适当的用户设置,或确保网页的安全。

主体是放置HTML代码的地方,由网络浏览器渲染。

客户端和服务器之间的HTTP请求和响应消息规范。

给网络应用程序增加复杂性

好了,让我们给我们的网络应用程序增加一些复杂性,用我们自己的更好的HTTP响应来回复网络浏览器。我们将添加一些要显示的模拟数据(本例中是出生日期),以及两个HTTP响应来显示数据并通过新的用户输入来更新数据。

添加一些模拟的出生日期

让我们首先创建一些默认数据。所有的生日数据都在一个哈希值中,在服务器被初始化后就可以找到。注意,这些数据不是持久性的,这意味着一旦服务器重新启动,任何添加到这个哈希中的对象都会消失。

为所有的端点创建一个案例语句

接下来,我们创建一个case语句,以便将不同的方法令牌和路径目标分开。这让我们对我们的小网络应用的三个端点有了一个清晰的轮廓。这三个端点是,GET /show/birthday, POST /add/birthday 和任何其他通用路径。如果请求与任何一个端点相匹配,块内的代码就会运行。

添加GET端点

因为每个端点将产生不同的HTTP响应,我们为每个端点定义以下变量,response_status_code, content_typeresponse_message

添加POST方法

至于POST方法,我们为我们的生日数据哈希添加一些用户输入。现在,它不会是持久性的,但你仍然能够在同一服务器运行时添加到列表中。

POST端点看起来会与GET端点有一些不同。这次的状态码不会是“200 OK” ,因为我们不只是通知浏览器请求已经成功,而是可以更有帮助,使用“303 See Other” 状态码将用户重定向到/show/birthdates 页面。

接下来,从请求中需要一个重要的头域,即Content-Length 头。内容长度头是HTTP请求中正文信息的大小,单位是字节。一旦我们知道了消息正文的字节数,我们就可以准确地从客户端套接字的IO中读取正文。最后,我们使用Ruby的内置解码器库uri将请求的正文解码成Ruby哈希值,然后追加到我们的生日列表的末尾。

构建HTTP响应

一旦我们成功地确定了基于每个端点的HTTP响应的所有组件,我们就可以重建响应了。响应是按照HTTP规范构建的:响应起始行包含版本号和响应状态代码,然后是头信息、空行和响应体。在接下来的练习中,我们接受来自网络浏览器的GET和POST响应。

运行接受GET和POST响应的代码

查看完整的代码片段,mirth-3.rb on GitHub:

  1. 让我们运行该代码,ruby mirth-3.rb
  2. 与之前的练习类似,我们打开浏览器进入URL,但这次我们进入localhost:1337/show/birthdays ,那是显示出生日期的URL。
  3. 在表格中添加一个新的生日信息,页面就会随着新的数据更新。
  4. 重新启动服务器,按Control-C键关闭服务器,然后再次用ruby mirth-3.rb 。重启会导致服务器以代码中提供的默认数据启动。

让我们在下一节中尝试解决数据持久化的问题。

持久性数据存储

如果没有存储数据的能力,什么是网络应用?我们很容易收集并显示一些用户提交的数据,但是一旦套接字关闭,这些数据就不会再存在了。我们需要一种方法来存储数据,以便在下一次服务器重新启动时,我们仍然可以使用以前保存的同一组数据。但我们要怎么做呢?

很明显,答案是存储在数据库中。数据库在物理磁盘或服务器上存储信息。但是,假设我们生活在一个不存在数据库的时代。坚持一些信息的最原始的方式是什么?我们把数据保存到一个文件中。

我们可以使用一个纯文本文件,但我们必须实现我们自己的格式来表示纯文本的数据。好消息是,Ruby有一个内置的库,PStore,可以将Ruby对象翻译成二进制并存储在文件中。另外,PStore也能够从文件中读取并将二进制文件翻译成Ruby对象。序列化和反序列化是将Ruby对象编入二进制的过程,反之亦然。

然而,PStore将文件存储为二进制文件,而我们人类是无法读取的。因此,我们提高了抽象度,并使用YAML::Store ,它是PStore的一个实现。YAML::Store ,而不是将Ruby对象序列化为二进制,而是序列化为人类可读的YAML文件格式。

YAML文件的结构是什么样子的。

运行代码,用YAML创建持久性数据存储

在GitHub上查看完整的代码片段,mirth-4.rb :

  1. 确保你把mirth-4.rb 和YAML文件mirth.yml 放在同一个文件夹里。
  2. 运行ruby mirth-4.rb ,并在浏览器上转到localhost:1337/show/birthdays 。你会看到默认的数据显示与之前的运行完全一样。
  3. 输入一些数据并重新启动应用程序。你应该还能看到你之前输入的数据!YAML文件将被更新以反映新的输入。

断点,你现在已经有了一个工作的Web应用程序了

唷,这有很多东西需要接受。如果你有兴趣了解更多的主题,请看文章末尾的附加信息部分。

但是等等--我们有一个完全用Ruby库编写的工作网络应用。我们花了很多时间才走到这一步。我们需要了解TCP如何运行的所有低级细节,什么是HTTP请求和响应,以及如何通过将数据存储在一个文件中来保持数据。我的意思是......肯定有一个更好的方法来写一个Ruby网络应用程序,但是怎么做呢?是的--用更多的抽象!

Rack (n' Roll) 规范

实现TCP套接字以创建网络连接的代码在所有类型的网络应用中通常是相似的--服务器创建一个套接字来接受客户端。HTTP请求和响应在所有类型的网络应用中也是标准化的。正如我们在上面看到的,重写解析HTTP的功能需要大量的时间和精力。一个网络开发者必须了解关于网络协议的复杂细节。

现在有机会把这些功能抽象出来,让那些只想做一个网络应用的开发者去做,减少开销的工作。我们可以使用一个应用服务器,而不是每次要启动一个新的网络应用时都要创建一个网络连接和编写HTTP解析逻辑。

应用服务器是一个代表网络应用程序处理HTTP的程序,它接受和解析请求,然后生成和发送响应。就像HTTP有其规范一样,应用服务器也有一个关于服务器如何与我们的代码对话的协议。

Rack是Ruby应用服务器的规范。如果我们的Web应用实现了Rack规范,那么我们就可以运行任何支持Rack规范的应用服务器。根据你的Web应用的需要,你可以使用许多应用服务器,在本教程中我们将使用 [puma](https://github.com/puma/puma)作为本教程中的应用服务器。

Rack请求

一个Rack应用(或遵循Rack规范的网络应用)是任何响应#call消息的Ruby对象,它接受一个称为环境的哈希参数,并返回一个包含三个元素的数组:

  • 一个状态代码
  • 响应头的哈希值
  • 响应主体的数组

这类似于HTTP消息的样子,但它们不是字符串格式,而是Ruby对象。

想象一下你的网络应用是一个盒子,它接收HTTP请求并输出HTTP响应。Rack是Web应用之上的一层,它使应用服务器Puma能够代表Web应用拦截HTTP请求,并返回三元素数组。

一旦你把你的应用变成了Rack应用,你就可以访问Rack提供的更多功能。但首先,让我们看看Rack应用程序是如何集成到我们现有的应用程序中的。

创建一个应用服务器和新的Rack::Request 对象

我们没有实例化我们自己的TCP套接字,而是通过遵循Rack规范将其交给Puma。我们的应用程序要做的第一件事是创建一个 rack::request对象,并提供环境。在这之后,我们不必担心接受和分割HTTP请求的请求线,因为它是在请求对象中提供的。

使用内置的Rack::Request 方法

这段代码之前是我们的case语句,它决定了要把请求引向哪个端点。现在我们使用内置的请求方法,如#get?#path

另一个值得注意的内置方法是#params ,该方法返回请求的正文和查询字符串,并将其解码为一个参数的哈希值。我们在POST端点中使用这个方法,我们从请求中获取参数,也就是用户的输入,并将其存储在我们的YAML文件中。

返回三元素数组

正如Rack规范中所述,我们需要返回一个三元素数组,其中包括状态、头信息和正文。请注意,status必须是一个200或更大的整数,headers必须是一个Hash,body必须是一个响应#each 的对象。为了简单起见,本例中的body对象是一个Ruby数组。

使用Puma运行应用程序

最后,我们要确保使用Rack::Handler::Puma 来运行应用程序,这是运行Rack应用程序的Puma API。Puma继续创建一个TCP套接字,并代表Web应用程序处理网络问题。

运行代码,在我们的例子中整合Rack::Request

在GitHub上查看完整的代码片段,mirth-5.rb :

  1. 运行ruby mirth-5.rb 。Mirth的行为应该与之前运行的相同。请注意,代码中的许多细节已经被删除,取而代之的是rack::request 库中的方法。

机架响应

现在,Mirth是一个使用rack::request 的Rack应用程序,我们现在可以用 [rack::response](https://www.rubydoc.info/gems/rack/Rack/Response)来包装HTTP响应对象。类似于rack::request 的工作方式,我们使用rack::response 对象来封装应用程序的响应逻辑。一旦HTTP响应对象被创建并准备好,Rack将为应用程序发送响应。

创建一个新的Rack响应对象

与创建rack::request 对象的方式类似,我们创建一个新的rack::response 对象来处理响应。

使用内置的Rack::Response 方法

同样,类似于你可以向rack::request 关于请求的问题,现在你可以根据不同的端点,按照你的喜好修改响应对象。例如,在GET端点中,我们将使用#write ,将一个字符串追加到响应的正文中。现在这将在幕后的rack::response 库中完成。

rack::response 也负责设置默认值。你不必为每个端点声明状态为200,因为它被假定为200是默认代码。内容类型被设置为 "text/plain"。如果你想让你的响应看起来不同,你可以修改它。

对于页面重定向,你现在可以使用rack::response’s #write 方法来设置重定向的状态和要重定向的路径。以前用两行代码完成的工作,现在只用一行就完成了。

将响应标记为完成

一旦每个端点都完成了对响应对象的修改,你就可以调用#finish ,以便通过Puma发送响应。

运行代码,在我们的例子中整合Rack::Response

在GitHub上查看完整的代码片断,mirth-6.rb

1.运行ruby mirth-6.rb 。注意代码看起来有多大的不同--为了确保正确的HTTP响应而进行的大部分手动变量分配现在已经消失了,由rack::response 库来处理。

真正的持久性数据存储

很明显,将我们的数据存储在YAML文件中并不能随着更多的请求而扩展。此外,一旦你的应用程序需要存储更多复杂的数据模型,而且它也会这样做,YAML文件的存储就会出现问题。数以百万计的数据项在每一个请求中都需要花费太多的时间和内存来处理和加载。在另一个抽象中,我们使用一个数据库,这样我们就不必担心优化我们的查询和更新。

SQLite是一个轻量级的关系型数据库,它不使用MySQL和PostgreSQL等替代品的传统网络化客户端-服务器模型。相反,SQLite数据库引擎在应用程序中运行,数据存在于一个本地文件中,由Web应用程序直接读取。SQLite对于我们要完成的任务是非常好的,因为它意味着我们不必处理运行一个单独的数据库服务器的额外复杂性。

不出所料,有一个Ruby gem叫做 [sqlite3](https://github.com/sparklemotion/sqlite3-ruby) 的API,我们可以方便地使用它。我们现在通过gem的API,用SQLite数据库的读写操作来取代YAML::Store 的读写操作。

创建一个名为 "生日 "的SQLite3表

创建一个带有默认值的名为生日的表

第一步是让我们创建一个具有适当列的SQLite3表。对我们来说,这将是一个名字列和一个出生日期列。为了保持简单,这两列都是字符串类型,我们插入一些默认值,就像以前的Mirth实现一样。

创建一个SQLite3数据库对象

创建完表后,我们在代码中通过sqlite3 gem创建一个对它的引用。我们通过创建一个带有表名的SQLite3::Database.new 实例来访问数据库表。感谢SQLite3,我们还添加了一个可选的设置,使用results_as_hash: 关键字参数以Ruby哈希的形式返回所有结果。

获取所有生日数据

当我们使用YAML::Store时,我们创建了一个事务,以便从YAML文件中获取数据。使用SQLite3 gem,我们直接在数据库表上执行一个查询。对于我们的GET端点,我们想显示存储在数据库中的所有生日信息,我们执行SELECT * FROM birthdays ,在Ruby哈希中返回所有的生日信息。SELECT * 是一个有风险的查询,因为它对性能有影响,但对于我们的目的,它工作得很好。在这一点上,我们就像使用YAML::Store一样访问Hash。

为新的用户输入的行创建一个查询

对于我们创建新生日数据的POST请求端点,我们执行一个INSERT查询,INSERT INTO birthdays (name, date)VALUES(?, ?) 与新的名字和生日。SQLite3将处理新行添加到表中的过程。

运行代码,用SQLite3创建持久的数据存储

在GitHub上查看完整的代码片段,mirth-7.rb

  1. 在运行ruby文件之前,我们必须创建一个数据库和表。在终端上运行下面的程序来创建数据库和表。
`sqlite3 mirth.sqlite3`

`CREATE TABLE birthdays (name TEXT, date TEXT);`

`INSERT INTO birthdays VALUES("Gma", "2021-01-01");`

`INSERT INTO birthdays VALUES("Tom", "2021-01-02");`

`INSERT INTO birthdays VALUES("Sesame", "2021-01-03");`

`.quit`

2.运行ruby mirth-7.rb 。现在你正在运行带有数据库的Mirth,所有的读和写都将通过Sqlite3进行。

Rails库

在本教程的这一部分,我们用一些Rails库来代替我们的工作,而实际上并不需要Rails整体。这将使你更好地了解每个库可以单独做什么,从而为Rails的整体能力描绘一个更好的心理模型。

在我们开始之前,我想让你思考一下,我们的Mirth应用程序中是否有任何部分可以被抽象掉,也就是说,应用程序的哪些常见部分可以被删除,而仅仅用API来代替。

使用Active Record库

Active Record是Rails的主要组件之一,是一个对象-关系映射库,意味着它将Ruby对象映射到数据库中的行。对你来说,一个House 对象应该映射到house 表中的一行,这听起来很明显。但这是主动记录模式的一个实现,即 "一个包裹了数据库表或视图中的行的对象,封装了数据库访问,并在该数据上添加了领域逻辑"。使用Active Record库,我们通过面向对象的API直接与数据库进行交互,而不需要关注SQL数据库。

Active Record专门从事创建、读取、更新、删除(CRUD)操作。该库处理了手动执行这些CRUD操作的复杂性。例如,生日这个对象被映射到SQLite中的生日表。注意,Active Record模型的名字是单数,而表的名字是复数。

例如,在我们的GET请求端点中,我们可以使用Birthday.all来获得生日表中所有行的列表。返回的对象是一个Birthday对象的数组,该数组对每一列如姓名和生日都有帮助方法。

至于我们的POST请求端点,我们通过简单地使用Birthday.create(name:, date:) 来创建新的用户输入的行来使用Active Record。Active Record完成了在数据库表中创建和保存新记录的工作。

在我们使用Active Record库的这些内置方法之前,我们必须在Active Record和Sqlite3数据库之间建立一个连接,然后创建一个连接数据模型和数据库中表的类。Active Record库处理类名与数据库中表名的匹配。

使用动作调度库

我们在代码中看到的端点路由逻辑是我们可以抽象出来的另一件事。在这种情况下,Action Dispatch是我们使用的库。更具体地说。 ActionDispatch::Routing::RouteSet提供了一个路由器,可以处理传入的请求,并将它们路由到适当的代码路径。

目前,代码仍然通过检查每个请求方法和路径来决定采取哪种代码路径来处理请求。使用Action Dispatch,我们把我们的应用程序分解成多个小型Rack应用程序,并为每个终端分别配置一个路由。RouteSet#draw 方法是用来取一个带有几个辅助方法的块,以请求方法(即GET、POST)的形式来配置对特定端点的请求。

现在我们已经有了一些小型的Rack应用,以端点的形式通向一个更大的Rack应用。任何传入的请求都要经过大型Rack应用,由Action Dispatch引导到相应的端点。

然而,我们似乎可以做得比有三个小型Rack应用更好。让我们看看Action Controller能提供什么。

使用Action Controller库

Action Controller是Rails提供的另一个库。它遵循MVC模型,即给定一个请求,控制器将决定如何处理它,比如确定要查询什么数据和渲染哪个视图。正如你在MVC图中所看到的,控制器充当了视图和数据模型之间的中介实体。

一个通用的MVC模型,控制器渲染视图并根据用户输入更新数据模型。

在动作控制器中,ActionController::Base 类中的方法被称为 "动作"。首先,我们为我们的Active Record模型创建一个控制器,将其称为BirthdaysController 。(注意Active Record模型的复数版本,Birthday。)

在我们的例子中,这个动作可以被称为#show_all_birthdays ,因为这正是我们的动作要做的--路由GET端点来显示每个人的生日。这些动作本质上是以前在迷你Rail应用程序中的端点。

然而,由于我们希望我们的动作与Action View一起工作(正如你将在下一节看到的那样),我们必须遵循Rails的命名惯例,将动作#index ,即GET /birthdays#create ,即POST /birthdays 。Rails还有其他一些命名惯例,可以在Rails Routing API中找到。

使用Action Controller和Action Dispatch,我们将请求路由到ActionController的动作。Action Controller通过实例方法,如:#params,#render 和 ,使访问请求和生成响应更加方便。 #redirect_to.所有这些以前由Rack Request和Rack Response库提供给我们的方法现在都由Action Controller处理。因此,每个HTTP端点的代码要短得多。

除了控制请求在应用程序中的流动,Action Controller还有很多其他功能,比如HTTP认证、在会话中存储数据以及在整个应用程序的生命周期中引发异常。

使用动作视图库

在关于Action Controller的部分,你可能已经注意到,之前内联渲染的一大块HTML代码不见了。然而,这个网络应用程序仍然在工作动作视图,MVC模型的视图组件,与动作控制器一起工作,帮助我们通过ERB模板从嵌入式Ruby(ERB)代码中生成HTML代码。然后,HTML代码被传回给Action Controller,并被放入一个响应对象,返回给客户端。

这里需要注意的是,视图必须以符合Rails惯例的方式来存储。在我们的例子中,我们声明了目录birthdays/index.html.erb ,并确保控制器通过使用以下方法访问视图路径 #prepend_view_path方法。

在HTML-ERB文件本身中,有一些功能是被抽象出来的,其中大部分是模板代码,如表单和内容标签。Action View库为我们提供了替代模板代码的辅助方法。此外,任何来自控制器的实例变量都可以在视图中使用相同的名称进行访问。在我们的案例中,在控制器动作查询了所有的生日后,我们在index.html.erb 中读取实例变量@all_birthdays ,并显示它们。

概述了Rails组件在Mirth中的工作方式。

使用Rails库运行应用程序

在GitHub上查看完整的代码片断,mirth-final.rb

好了,现在你已经有了一个大致的概念,看看最终完成的使用Rails库的Mirth网络应用:

  1. 假设你已经在上一节中创建了一个SQLite3数据库,把index.html.erb 放在一个叫做birthdays/ 的文件夹中,然后运行ruby mirth-final.rb
  2. 进入网址localhost:1337/birthdays,可以看到同样的网络应用程序在运行,但现在是使用Rails库。

起点的终点

Rails的功能远远超过我们在本教程中所涉及的内容,每个部分都可以写得更详细。除了我们在本教程中了解到的主要库之外,Rails还有用于发送电子邮件、创建和管理后台工作以及为你的Web应用添加国际化的库。

只要稍微熟悉一下Rails框架,你就能建立一个Web应用--这就是Rails的魅力所在。然而,从头开始构建你自己的Ruby网络应用程序不仅具有教育意义--我认为这也是Ruby和Rails开发者职业生涯中的一个仪式!

开个玩笑。这很有趣,但如果你用Rails来满足你的网络开发需求,那可能是最好的。现在你了解了网络应用在Ruby中如何工作的低级细节,你就能够理解为什么Rails试图将所有的实现细节隐藏在引擎盖下。