什么是 Ruby 中的 Rack 中间件?对于“中间件”的含义,我找不到任何好的解释。
9 回答
机架即设计
Rack 中间件不仅仅是“一种过滤请求和响应的方法”——它是使用Rack的 Web 服务器的管道设计模式的实现。
它非常清晰地分离了处理请求的不同阶段——关注点分离是所有设计良好的软件产品的关键目标。
例如,使用 Rack 我可以有管道的单独阶段:
身份验证:当请求到达时,用户的登录信息是否正确?如何验证此 OAuth、HTTP 基本身份验证、名称/密码?
授权:“用户是否有权执行此特定任务?”,即基于角色的安全性。
缓存:我已经处理了这个请求,我可以返回一个缓存的结果吗?
装饰:如何增强请求以使下游处理更好?
性能和使用监控:我可以从请求和响应中获得哪些统计信息?
执行:实际处理请求并提供响应。
能够分离不同的阶段(并且可以选择包括它们)对于开发结构良好的应用程序有很大帮助。
社区
还有一个围绕机架中间件开发的很棒的生态系统——您应该能够找到预先构建的机架组件来完成上述所有步骤以及更多。有关中间件列表,请参阅Rack GitHub wiki 。
什么是中间件?
中间件是一个可怕的术语,它指的是任何帮助但不直接参与执行某些任务的软件组件/库。非常常见的示例是日志记录、身份验证和其他常见的水平处理组件。这些往往是每个人都需要跨多个应用程序的东西,但没有太多人有兴趣(或应该)构建自己。
更多信息
关于它是一种过滤请求的方法的评论可能来自RailsCast 第 151 集:机架中间件屏幕截图。
Rack 中间件是从 Rack 演变而来的,在Introduction to Rack middleware中有很好的介绍。
此处有 Wikipedia 上的中间件介绍。
首先,Rack 就是两个东西:
- 网络服务器接口约定
- 一颗宝石
Rack - 网络服务器界面
机架的基础是一个简单的约定。每个机架兼容的网络服务器总是会在你给他的对象上调用一个调用方法,并提供该方法的结果。Rack 准确地指定了这个调用方法的外观,以及它必须返回的内容。那是机架。
让我们尝试一下。我将使用 WEBrick 作为机架兼容的网络服务器,但它们中的任何一个都可以。让我们创建一个返回 JSON 字符串的简单 Web 应用程序。为此,我们将创建一个名为 config.ru 的文件。config.ru 将由 rack gem 的命令 rackup 自动调用,该命令将简单地在机架兼容的网络服务器中运行 config.ru 的内容。因此,让我们将以下内容添加到 config.ru 文件中:
class JSONServer
def call(env)
[200, {"Content-Type" => "application/json"}, ['{ "message" : "Hello!" }']]
end
end
map '/hello.json' do
run JSONServer.new
end
正如约定所指定的,我们的服务器有一个名为 call 的方法,该方法接受环境散列并返回一个格式为 [status, headers, body] 的数组以供 web 服务器服务。让我们通过调用 rackup 来尝试一下。一个默认的机架兼容服务器,可能是 WEBrick 或 Mongrel 将启动并立即等待请求服务。
$ rackup
[2012-02-19 22:39:26] INFO WEBrick 1.3.1
[2012-02-19 22:39:26] INFO ruby 1.9.3 (2012-01-17) [x86_64-darwin11.2.0]
[2012-02-19 22:39:26] INFO WEBrick::HTTPServer#start: pid=16121 port=9292
让我们通过 curl 或访问 url 来测试我们的新 JSON 服务器http://localhost:9292/hello.json
,瞧:
$ curl http://localhost:9292/hello.json
{ message: "Hello!" }
有用。伟大的!这是每个 Web 框架的基础,无论是 Rails 还是 Sinatra。在某些时候,他们实现了一个调用方法,遍历所有框架代码,最后以典型的 [status, headers, body] 形式返回响应。
例如,在 Ruby on Rails 中,机架请求会命中如下ActionDispatch::Routing.Mapper
所示的类:
module ActionDispatch
module Routing
class Mapper
...
def initialize(app, constraints, request)
@app, @constraints, @request = app, constraints, request
end
def matches?(env)
req = @request.new(env)
...
return true
end
def call(env)
matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ]
end
...
end
end
所以基本上 Rails 检查,如果有任何路由匹配,则依赖于 env 哈希。如果是这样,它会将 env 哈希传递给应用程序以计算响应,否则它会立即以 404 响应。因此,任何符合机架接口约定的 Web 服务器都能够为完全成熟的 Rails 应用程序提供服务。
中间件
Rack 还支持创建中间件层。他们基本上拦截一个请求,用它做一些事情并传递它。这对于多功能任务非常有用。
假设我们想向我们的 JSON 服务器添加日志记录,该服务器还测量请求需要多长时间。我们可以简单地创建一个中间件记录器来执行此操作:
class RackLogger
def initialize(app)
@app = app
end
def call(env)
@start = Time.now
@status, @headers, @body = @app.call(env)
@duration = ((Time.now - @start).to_f * 1000).round(2)
puts "#{env['REQUEST_METHOD']} #{env['REQUEST_PATH']} - Took: #{@duration} ms"
[@status, @headers, @body]
end
end
当它被创建时,它会为自己保存一份实际机架应用程序的副本。在我们的例子中,这是我们的 JSONServer 的一个实例。Rack 自动调用中间件上的 call 方法并期望返回一个[status, headers, body]
数组,就像我们的 JSONServer 返回一样。
所以在这个中间件中,开始点,然后对 JSONServer 的实际调用是用 进行的@app.call(env)
,然后记录器输出日志条目,最后返回响应为[@status, @headers, @body]
。
为了让我们的小 rackup.ru 使用这个中间件,像这样添加一个 use RackLogger 到它:
class JSONServer
def call(env)
[200, {"Content-Type" => "application/json"}, ['{ "message" : "Hello!" }']]
end
end
class RackLogger
def initialize(app)
@app = app
end
def call(env)
@start = Time.now
@status, @headers, @body = @app.call(env)
@duration = ((Time.now - @start).to_f * 1000).round(2)
puts "#{env['REQUEST_METHOD']} #{env['REQUEST_PATH']} - Took: #{@duration} ms"
[@status, @headers, @body]
end
end
use RackLogger
map '/hello.json' do
run JSONServer.new
end
重新启动服务器,瞧,它会在每个请求上输出一个日志。Rack 允许您添加多个按添加顺序调用的中间件。这只是在不更改机架应用程序核心的情况下添加功能的好方法。
机架 - 宝石
尽管 rack - 首先 - 是一种约定,但它也是提供强大功能的宝石。其中之一我们已经用于我们的 JSON 服务器,rackup 命令。但还有更多!rack gem 为许多用例提供了很少的应用程序,例如提供静态文件甚至整个目录。让我们看看我们如何提供一个简单的文件,例如一个位于 htmls/index.html 的非常基本的 HTML 文件:
<!DOCTYPE HTML>
<html>
<head>
<title>The Index</title>
</head>
<body>
<p>Index Page</p>
</body>
</html>
我们可能想从网站根目录提供这个文件,所以让我们将以下内容添加到我们的 config.ru:
map '/' do
run Rack::File.new "htmls/index.html"
end
如果我们访问http://localhost:9292
,我们会看到我们的 html 文件完美呈现。这很容易,对吧?
让我们通过在 /javascripts 下创建一些 javascript 文件并将以下内容添加到 config.ru 来添加整个 javascript 文件目录:
map '/javascripts' do
run Rack::Directory.new "javascripts"
end
重新启动服务器并访问http://localhost:9292/javascript
,您将看到现在可以从任何地方直接包含的所有 javascript 文件的列表。
在很长一段时间内,我都无法理解 Rack 自己。在自己制作了这个微型 Ruby Web 服务器之后,我才完全理解了它。我在我的博客上分享了我对 Rack 的了解(以故事的形式):http: //blog.gauravchande.com/what-is-rack-in-ruby-rails
反馈非常受欢迎。
什么是机架?
Rack 在支持 Ruby 和 Ruby 框架的网络服务器之间提供了一个最小的接口。
使用 Rack 你可以编写一个 Rack 应用程序。
Rack 会将环境散列(一个散列,包含在来自客户端的 HTTP 请求中,由类似 CGI 的标头组成)传递给你的 Rack 应用程序,它可以使用此散列中包含的内容来做任何它想做的事情。
什么是机架应用程序?
#call
要使用 Rack,您必须提供一个“应用程序”——一个以环境哈希作为参数(通常定义为)响应方法的对象env
。#call
必须返回一个恰好包含三个值的数组:
- 状态码(例如“ 200”),
- 标头哈希,
- 响应体(它必须响应 Ruby 方法,
each
)。
您可以编写一个返回此类数组的 Rack 应用程序 - 这将通过 Rack 在Response中发送回您的客户端(这实际上是Class [单击转到文档]的一个实例)。Rack::Response
一个非常简单的机架应用程序:
gem install rack
- 创建一个
config.ru
文件 - Rack 知道要查找它。
我们将创建一个微型 Rack 应用程序,它返回一个 Response(的实例Rack::Response
),其 Response Body 是一个包含 String: 的数组"Hello, World!"
。
我们将使用命令启动本地服务器rackup
。
在我们的浏览器中访问相关端口时,我们将看到“Hello, World!” 在视口中渲染。
#./message_app.rb
class MessageApp
def call(env)
[200, {}, ['Hello, World!']]
end
end
#./config.ru
require_relative './message_app'
run MessageApp.new
使用localhost:9292启动本地服务器rackup
并访问,您应该会看到“Hello,World!” 呈现。
这不是一个全面的解释,但本质上这里发生的是客户端(浏览器)通过本地服务器向 Rack 发送 HTTP 请求,Rack 实例化MessageApp
并运行call
,将 Environment Hash 作为参数传递给方法(env
论据)。
Rack 获取返回值(数组)并使用它创建一个实例Rack::Response
并将其发送回客户端。浏览器使用魔法打印“Hello, World!” 到屏幕。
顺便说一句,如果您想查看环境哈希的样子,只需puts env
将def call(env)
.
尽管它是最小的,但您在这里编写的是一个 Rack 应用程序!
使机架应用程序与传入环境哈希交互
在我们的小 Rack 应用程序中,我们可以与env
散列进行交互(有关环境散列的更多信息,请参见此处)。
我们将实现用户将自己的查询字符串输入到 URL 中的功能,因此,该字符串将出现在 HTTP 请求中,封装为环境哈希的键/值对之一中的值。
我们的 Rack 应用程序将从 Environment 哈希中访问该查询字符串,并通过响应中的 Body 将其发送回客户端(在本例中为我们的浏览器)。
来自 Environment Hash 的 Rack 文档: “QUERY_STRING:请求 URL 中 ? 之后的部分,如果有的话。可能为空,但始终是必需的!”
#./message_app.rb
class MessageApp
def call(env)
message = env['QUERY_STRING']
[200, {}, [message]]
end
end
现在,rackup
访问localhost:9292?hello
(?hello
作为查询字符串),您应该会在视口中看到“hello”。
机架中间件
我们会:
- 在我们的代码库中插入一个 Rack Middleware - 一个类:
MessageSetter
, - Environment hash 将首先命中此类,并将作为参数传入:
env
, MessageSetter
将在 env 哈希中插入一个'MESSAGE'
键,其值为'Hello, World!'
ifenv['QUERY_STRING']
为空;env['QUERY_STRING']
如果不,- 最后,它将返回
@app.call(env)
-@app
成为“堆栈”中的下一个应用程序:MessageApp
。
首先,“长手”版本:
#./middleware/message_setter.rb
class MessageSetter
def initialize(app)
@app = app
end
def call(env)
if env['QUERY_STRING'].empty?
env['MESSAGE'] = 'Hello, World!'
else
env['MESSAGE'] = env['QUERY_STRING']
end
@app.call(env)
end
end
#./message_app.rb (same as before)
class MessageApp
def call(env)
message = env['QUERY_STRING']
[200, {}, [message]]
end
end
#config.ru
require_relative './message_app'
require_relative './middleware/message_setter'
app = Rack::Builder.new do
use MessageSetter
run MessageApp.new
end
run app
从Rack::Builder 文档中,我们看到它Rack::Builder
实现了一个小的 DSL 来迭代地构建 Rack 应用程序。这基本上意味着您可以构建一个由一个或多个中间件和一个“底层”应用程序组成的“堆栈”来调度。通过您的底层应用程序的所有请求将首先由您的中间件处理。
#use
指定要在堆栈中使用的中间件。它将中间件作为参数。
机架中间件必须:
- 有一个构造函数,它将堆栈中的下一个应用程序作为参数。
- 响应将
call
Environment 哈希作为参数的方法。
在我们的例子中,“中间件”是MessageSetter
,“构造函数”是 MessageSetter 的initialize
方法,堆栈中的“下一个应用程序”是MessageApp
。
所以在这里,由于Rack::Builder
引擎盖下的作用, '方法的app
参数是.MessageSetter
initialize
MessageApp
(在继续之前先了解一下上面的内容)
因此,每个中间件本质上都将现有的环境哈希“传递”给链中的下一个应用程序——因此您有机会在中间件中改变该环境哈希,然后再将其传递给堆栈中的下一个应用程序。
#run
接受一个参数,该参数是一个响应#call
并返回机架响应( 的实例Rack::Response
)的对象。
结论
使用Rack::Builder
您可以构建中间件链,并且对您的应用程序的任何请求将依次由每个中间件处理,然后最终由堆栈中的最后一个部分(在我们的例子中,MessageApp
)处理。这非常有用,因为它将处理请求的不同阶段分开。就“关注点分离”而言,它再干净不过了!
您可以构建一个由多个中间件组成的“请求管道”,这些中间件处理诸如:
- 验证
- 授权
- 缓存
- 装饰
- 性能和使用监控
- 执行(实际处理请求并提供响应)
(高于此线程另一个答案的要点)
您经常会在专业的 Sinatra 应用程序中看到这一点。Sinatra 使用 Rack!请参阅此处了解 Sinatra 的定义!
最后一点,我们config.ru
可以用简写风格编写,产生完全相同的功能(这是您通常会看到的):
require_relative './message_app'
require_relative './middleware/message_setter'
use MessageSetter
run MessageApp.new
为了更明确地展示MessageApp
正在做什么,这里是它的“长手”版本,它明确地表明#call
正在创建一个 的新实例Rack::Response
,并带有所需的三个参数。
class MessageApp
def call(env)
Rack::Response.new([env['MESSAGE']], 200, {})
end
end
有用的链接
config.ru
最小可运行示例
app = Proc.new do |env|
[
200,
{
'Content-Type' => 'text/plain'
},
["main\n"]
]
end
class Middleware
def initialize(app)
@app = app
end
def call(env)
@status, @headers, @body = @app.call(env)
[@status, @headers, @body << "Middleware\n"]
end
end
use(Middleware)
run(app)
运行rackup
并访问localhost:9292
。输出是:
main
Middleware
所以很明显,Middleware
包装并调用了主应用程序。因此,它能够对请求进行预处理,并以任何方式对响应进行后处理。
正如在:httpconfig.middleware.use
://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack 中解释的那样,Rails 使用 Rack 中间件来实现它的很多功能,您也可以使用家庭方法添加自己的中间件。
在中间件中实现功能的优点是您可以在任何 Rack 框架上重用它,因此可以在所有主要的 Ruby 框架上重用它,而不仅仅是 Rails。
Rack 是一个 gem,它提供了一个简单的接口来抽象 HTTP 请求/响应。Rack 位于 web 框架(Rails、Sinatra 等)和 web 服务器(unicorn、puma)之间,作为适配器。从上图中,这使 unicorn 服务器完全独立于了解 rails 和 rails 不了解 unicorn。这是松散耦合、关注点分离的一个很好的例子。
上图来自这个 Rails 会议上的演讲https://youtu.be/3PnUV9QzB0g我建议观看它以加深理解。
机架中间件是一种过滤进入应用程序的请求和响应的方法。中间件组件位于客户端和服务器之间,处理入站请求和出站响应,但它不仅仅是可用于与 Web 服务器对话的接口。它用于对模块(通常是 Ruby 类)进行分组和排序,并指定它们之间的依赖关系。机架中间件模块必须: – 具有将堆栈中的下一个应用程序作为参数的构造函数 – 响应“调用”方法,该方法将环境哈希作为参数。此调用的返回值是一个数组:状态代码、环境哈希和响应正文。
我使用 Rack 中间件解决了几个问题:
- 使用自定义 Rack 中间件捕获 JSON 解析错误,并在客户端提交破坏的 JSON 时返回格式正确的错误消息
- 通过 Rack::Deflater 进行内容压缩
它在这两种情况下都提供了相当优雅的修复。
Rack - 黑白 Web 和应用服务器接口
Rack 是一个 Ruby 包,它为 Web 服务器提供与应用程序通信的接口。在 Web 服务器和应用程序之间添加中间件组件以修改请求/响应的行为方式很容易。中间件组件位于客户端和服务器之间,处理入站请求和出站响应。
用外行的话来说,它基本上只是一组关于服务器和 Rails 应用程序(或任何其他 Ruby Web 应用程序)应该如何相互通信的指南。
要使用 Rack,请提供一个“app”:一个响应调用方法的对象,将环境哈希作为参数,并返回一个包含三个元素的 Array:
- HTTP 响应代码
- 标头哈希
- 响应正文,必须响应每个请求。
有关更多说明,您可以点击以下链接。
1. https://rack.github.io/
2. https://redpanthers.co/rack-middleware/
3. https://blog.engineyard.com/2015/understanding-rack-apps-and-middleware
4. https://guides.rubyonrails.org/rails_on_rack.html#resources
在 Rails 中,我们有 config.ru 作为机架文件,您可以使用rackup
命令运行任何机架文件。而这个的默认端口是9292
. 要对此进行测试,您可以简单地rackup
在您的 rails 目录中运行并查看结果。您还可以分配要在其上运行它的端口。在任何特定端口上运行机架文件的命令是
rackup -p PORT_NUMBER