ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ruby on Rails 与 Rack 深度整合指南:中间件栈的组成、配置与自定义

Ruby on Rails 与 Rack 深度整合指南:中间件栈的组成、配置与自定义 Ruby on Rails 与 Rack 深度整合指南中间件栈的组成、配置与自定义【免费下载链接】railsRuby on Rails项目地址: https://gitcode.com/GitHub_Trending/rai/railsRails 是构建在 Rack 之上的 Ruby Web 框架所有 HTTP 请求最终都会以统一约定的方式流经一套由中间件middleware串联起来的调用栈。本文以 Rails 官方文档中 Rails on Rack 一讲为主体结合本仓库Ruby on Rails 完整源码中actionpack、railties的真实实现讲清 Rack 的基本模型、Rails.application这一主 Rack 对象、Action Dispatch 内置中间件栈的每个成员及其用途并给出通过config.middleware增删改排序、编写自定义中间件以及直接在控制器中使用 Rack 底层 API 的完整实战方案。读完你将能独立看懂bin/rails middleware的每一行输出并在自己的应用里精准地调整请求/响应处理管线。读完本文你将掌握Rack 应用与 Rack 中间件的标准协议call、三元组响应、useRails 为什么选择 Rack以及Rails.application如何成为主 Rack 对象Action Dispatch 中间件栈与Rack::Builder的异同用config.middleware.use / insert_before / insert_after / swap / move_after / move_before / delete操作中间件栈Rails 内置中间件栈中每个组件的作用与触发条件自定义中间件的正确落地位置lib/autoload_lib(ignore:)与注册方式在控制器里读取 Rackenv、直接写 Rack 响应、把路由指向 Rack 应用。Rack 简介为什么 Rails 需要它Rack 为用 Ruby 开发 Web 应用提供了一套模块化接口。它把 HTTP 请求与响应包裹进一种约定俗成的结构从而把 Web 服务器、Web 框架以及它们之间各类软件也就是中间件的 API 统一成一次方法调用。这种约定带来的直接好处是只要符合 Rack 规范的 Web 服务器例如 Puma 或 Falcon都可以与任何基于 Rack 的 Web 框架比如 Rails互换使用框架不需要关心底层服务器是哪一个。在深入探讨 Rails 如何与 Rack 集成之前先看看 Rack 本身。一个最简 Rack 应用一个 Rack 应用就是实现了call方法的对象。它接收一个被称作 Rack 环境Rack environment的env哈希。下面是一个最朴素的 Rack 应用class App def call(env) [200, { content-type text/plain }, [Hello World]] end end run App.new当 HTTP 请求到达时符合 Rack 规范的 Web 服务器会把请求解析成env哈希然后用env调用应用。call方法必须返回一个恰好包含三个元素的数组代表 HTTP 响应HTTP 响应状态码上例中的200一个哈希内含我们想要发送的所有 HTTP 响应头一个可枚举对象逐段产出字符串形式的响应体。Rack 应用一般通过 Web 服务器自带的命令行程序启动应用的入口保存在config.ru文件中$ cat config.ru APP rack_app lambda do |env| [200, { content-type text/plain }, [Hello World]] end run rack_app APP $ gem install puma $ puma此时应用应该已经可以通过 http://localhost:9292 访问$ curl localhost:9292 Hello WorldRack 中间件Rack 应用可以被称作中间件的组件包裹起来。中间件既可以在请求到达主应用之前对它做处理也可以在应用返回响应之后再次处理它。中间件通常被用来完成日志记录、缓存、鉴权、性能测量等横切任务。一个 Rack 中间件必须提供new方法new接收 Rack 应用以及任何用于配置中间件的参数new的返回值必须是一个能响应call的 Rack 应用。典型情况下Rack 中间件是类中间件类的每个实例都包裹着对被关联应用的访问class MyMiddleware def initialize(app) app app end def call(env) # Operations before the request hits the main application # ------------------------------------------------------- # Propagate the request down the middleware stack status, headers, body app.call(env) # --------------------------------------- # Operations after the request comes back # Propagate the response up the middleware stack [status, headers, body] end end中间的app.call(env)是关键一环它把请求继续向下传播返回值三元组再被逐层向上回传因此每个中间件都能在请求下行与响应上行两个阶段插入自己的逻辑。中间件还可以短路short-circuit整条栈完全跳过app.call由自己直接返回响应。这意味着请求永远不会到达主应用也不会触达栈中剩余的其他中间件。一个做请求鉴权的中间件就可以利用这种手法未通过时直接返回 401class AuthenticateRequest def initialize(app) app app end def call(env) if authenticated?(env[HTTP_AUTHORIZATION]) app.call(env) else [401, { content-type text/plain }, [Authentication failed]] end end def authenticated?(token) # ... end end把中间件挂到 Rack 应用上使用的是useclass AuthenticateRequest # ... end class App def call(env) [200, { content-type text/plain }, [Hello World]] end end use AuthenticateRequest run App.new上面这套用来组装 Rack 应用的 DSL 由Rack::Builder提供。想进一步了解 Rack可查阅 Rack 规范Rack SPEC与 Rack 官方网站。需要说明的是Rack 本体不在本仓库源码范围内属于 Rails 依赖的外部 gem。Rails on Rack主 Rack 对象与服务器启动Rails.applicationRails 应用的主 Rack 对象Rails.application是 Rails 应用的主 Rack 应用对象primary Rack application object。任何符合 Rack 规范的 Web 服务器都应该通过Rails.application来伺服一个 Rails 应用。换句话说一个 Rails 应用本质上就是一个包着一大堆中间件的 Rack 应用而最内层run的最终应用就是路由分发器见下文bin/rails middleware输出的最后一行。启动 Rails 服务器Rails 通过子类化Rackup::Server创建了Rails::Server。执行bin/rails server时会实例化一个Rails::Server对象并启动 Web 服务器Rails::Server.new.tap do |server| require APP_PATH Dir.chdir(Rails.application.root) server.start end这段代码在 服务器命令实现 中可以看到完整形态Rails::Command::ServerCommand#perform先调用Rails::Server.new(server_options)再require APP_PATH加载应用、Dir.chdir(Rails.application.root)切换目录最后在检测到服务器可运行时print_boot_information并server.start。从该实现还能读到一些实用的默认值可用于理解启动参数行为默认端口是3000DEFAULT_PORT 3000可由-p/ENV[PORT]覆盖默认绑定地址在 development 环境为localhost其他环境为0.0.0.0可用-b/ENV[BINDING]覆盖development 环境默认 PID 文件是tmp/pids/server.pidRails::Server#start内部还会create_tmp_directoriestmp/cache、tmp/pids、tmp/sockets并处理开发环境缓存开关。服务器是如何一步步初始化启动的更多细节可参考 初始化流程指南。Action Dispatch 中间件栈ActionDispatch::MiddlewareStack相当于 Rails 版的Rack::Builder。它在满足 Rails 需求的前提下被构建得更加灵活、功能更丰富其核心实现位于 actionpack/lib/action_dispatch/middleware/stack.rb。Rails::Application使用ActionDispatch::MiddlewareStack把内部的与外部的中间件组合起来最终构建出用 Rails 组成的一个完整 Rack 应用。从源码看ActionDispatch::MiddlewareStack相比Rack::Builder额外提供了按类名定位、按位置插入/调换/删除的能力每个入栈条目都被封装成Middleware对象保存klass、args、kwargs、block真正的实例化发生在build阶段——middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) }即从后向前逐层包裹最终得到洋葱模型。另外它还支持process_middleware.action_dispatch通知事件当有监听者时会用InstrumentationProxy透明地包装每个中间件以便做性能埋点。查看中间件栈运行下面的命令查看完整的中间件栈$ bin/rails middleware该命令的实现见 railties/lib/rails/commands/middleware/middleware_command.rb它逐条打印use 中间件名最后打印一行run 应用名.routes。下面是一个新生成 Rails 应用的示例输出use ActionDispatch::HostAuthorization use Rack::Sendfile use ActionDispatch::Static use Propshaft::Server use ActionDispatch::Executor use ActionDispatch::ServerTiming use ActiveSupport::Cache::Strategy::LocalCache::Middleware use Rack::Runtime use Rack::MethodOverride use ActionDispatch::RequestId use ActionDispatch::RemoteIp use Propshaft::QuietAssets use Rails::Rack::Logger use ActionDispatch::ShowExceptions use WebConsole::Middleware use ActionDispatch::DebugExceptions use ActionDispatch::ActionableExceptions use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::Migration::CheckPending use ActionDispatch::Cookies use ActionDispatch::Session::CookieStore use ActionDispatch::Flash use ActionDispatch::ContentSecurityPolicy::Middleware use Rack::Head use Rack::ConditionalGet use Rack::ETag use Rack::TempfileReaper run MyApp::Application.routes阅读这段输出时要注意打印顺序越靠上越外层越先接触到原始请求越靠下越接近最终路由应用而run一行才是整条栈的底也就是真正处理业务请求的 Rails 路由分发器。如果你想探究这套默认序列是怎么被搭出来的可以阅读 railties/lib/rails/application/default_middleware_stack.rb。其中大量中间件是根据配置条件性加入的例如仅当config.hosts非空时才use ActionDispatch::HostAuthorizationconfig.public_file_server.enabled为真才use ActionDispatch::Staticconfig.server_timing开启才加ActionDispatch::ServerTimingAPI only 应用不加Rack::MethodOverride、ActionDispatch::Cookies、ActionDispatch::Flash与Rack::TempfileReaper等。这也解释了为什么文档里的示例输出与你的应用实际输出可能会略有差别。配置中间件栈Rails 通过配置接口config.middleware来增加、删除、修改中间件栈。你可以在application.rb或环境专属配置文件environments/environment.rb中使用它。值得补充的底层机制是config.middleware实际是Rails::Configuration::MiddlewareStackProxy的实例见 railties/lib/rails/configuration.rb。它并不立即改动真正的栈而是把每次操作记录成一段延迟执行的 lambdaoperations与delete_operations等到组装时再一次性merge_into真实的ActionDispatch::MiddlewareStack。正因为如此你在引擎或初始化器里按任意顺序调用这些方法都是安全的最终都会正确地落到栈中。添加中间件向栈中添加新中间件有三种方法config.middleware.use(new_middleware, args)把新中间件追加到中间件栈的底部最内层最接近路由应用的位置config.middleware.insert_before(existing_middleware, new_middleware, args)把新中间件插到指定的已有中间件之前config.middleware.insert_after(existing_middleware, new_middleware, args)把新中间件插到指定的已有中间件之后。示例# config/application.rb # 把 Rack::BounceFavicon 追加到栈底 config.middleware.use Rack::BounceFavicon # 在 ActionDispatch::Executor 之后添加 Lifo::Cache # 并把 { page_cache: false } 作为参数传给 Lifo::Cache。 config.middleware.insert_after ActionDispatch::Executor, Lifo::Cache, page_cache: false中间件类构造函数的额外参数都可以在这里透传位置参数与关键字参数均可对应到 stack.rb 中的klass.new(app, *args, **kwargs, block)。替换中间件使用config.middleware.swap替换中间件# config/application.rb # 用 Lifo::ShowExceptions 替换 ActionDispatch::ShowExceptions config.middleware.swap ActionDispatch::ShowExceptions, Lifo::ShowExceptionsswap在 stack.rb 中的语义是先定位到目标中间件的位置把新中间件插进去再删除同位置多出来的那个旧条目。移动中间件使用config.middleware.move_before或config.middleware.move_after移动栈中已有的中间件组件# config/application.rb # 把 ActionDispatch::ShowExceptions 移动到 Lifo::ShowExceptions 之前 config.middleware.move_before Lifo::ShowExceptions, ActionDispatch::ShowExceptions# config/application.rb # 把 ActionDispatch::ShowExceptions 移动到 Lifo::ShowExceptions 之后 config.middleware.move_after Lifo::ShowExceptions, ActionDispatch::ShowExceptions在 stack.rb 中move的实现是先从源位置delete_at取出中间件再插入到目标位置move_after则额外把目标索引加一。另外move_before/move_after同insert_before/insert_after一样被MiddlewareStackProxy记录为删除类操作从而保证先执行完所有添加操作后、再按删除操作调整位置时语义依然正确。删除中间件使用config.middleware.delete删除中间件# config/application.rb config.middleware.delete Rack::Runtime如果目标中间件不存在delete会静默忽略其实现是对条目做reject!。而使用delete!时若该中间件组件不存在则会抛出错误# config/application.rb config.middleware.delete! Some::NonExistentMiddlewaredelete!在 stack.rb 中若找不到目标会直接抛出RuntimeError: No such middleware to remove。相关行为在 actionpack/test/dispatch/middleware_stack_test.rb 中有对应的单元测试覆盖例如delete找不到时返回 nil、delete!找不到时抛异常。中间件栈的重载时机中间件栈只被加载一次并不会被监控自动热更新。因此每次修改完中间件栈配置后请重启你的服务器使其生效。Rails 内置中间件栈Action Controller 的大量功能正是通过中间件实现的。下面逐一说明上面示例栈中各个组件的用途其中部分条目只有在满足特定配置条件时才会出现。ActionDispatch::ActionableExceptions若请求来自本机ActionDispatch::ActionableExceptions提供了一种从 Rails 错误页面直接分发执行对应动作的途径。实现在 actionpack/lib/action_dispatch/middleware/actionable_exceptions.rb。ActionDispatch::CallbacksActionDispatch::Callbacks提供在请求分发前后执行的回调。实现在 actionpack/lib/action_dispatch/middleware/callbacks.rb。ActionDispatch::ContentSecurityPolicy::MiddlewareActionDispatch::ContentSecurityPolicy::Middleware提供了一套 DSL 用来配置Content-Security-Policy响应头。更多信息参见 Securing Rails Applications安全指南。ActionDispatch::CookiesActionDispatch::Cookies负责从请求中读取 cookie 数据并在响应上写入 cookie 数据。实现在 actionpack/lib/action_dispatch/middleware/cookies.rb。ActionDispatch::DebugExceptionsActionDispatch::DebugExceptions负责记录异常日志当请求来自本机时展示调试用的错误页面。实现在 actionpack/lib/action_dispatch/middleware/debug_exceptions.rb。ActionDispatch::ExecutorActionDispatch::Executor在开发环境下确保线程安全的代码重载。实现在 actionpack/lib/action_dispatch/middleware/executor.rb。ActionDispatch::FlashActionDispatch::Flash负责设置 flash 键。仅当配置了会话存储见 config.session_store 配置项时它才会出现在栈中。实现在 actionpack/lib/action_dispatch/middleware/flash.rb。ActionDispatch::HostAuthorizationActionDispatch::HostAuthorization通过限制请求可以发送到的主机host防止 DNS rebindingDNS 重绑定攻击。配置方法参见 configuring配置指南。实现在 actionpack/lib/action_dispatch/middleware/host_authorization.rb。ActionDispatch::ReloaderActionDispatch::Reloader提供 prepare 与 cleanup 回调目的是在开发环境下辅助代码重载。实现在 actionpack/lib/action_dispatch/middleware/reloader.rb。ActionDispatch::RemoteIpActionDispatch::RemoteIp负责检测 IP 欺骗攻击。实现在 actionpack/lib/action_dispatch/middleware/remote_ip.rb。ActionDispatch::RequestIdActionDispatch::RequestId为请求生成唯一的X-Request-Id头并让ActionDispatch::Request#request_id方法可用。这个唯一的请求 ID 可以用来端到端地追踪一次请求通常会成为栈中多个环节日志的一部分。实现在 actionpack/lib/action_dispatch/middleware/request_id.rb。ActionDispatch::ServerTimingActionDispatch::ServerTiming会设置一个包含本次请求性能指标的Server-Timing响应头。实现在 actionpack/lib/action_dispatch/middleware/server_timing.rb。ActionDispatch::Session::CookieStoreActionDispatch::Session::CookieStore负责把会话数据存储到 cookie 中。其实现位于 actionpack/lib/action_dispatch/middleware/session/ 目录下。ActionDispatch::ShowExceptionsActionDispatch::ShowExceptions会接住应用抛出的任何异常并调用一个专门的 exceptions app把它包装成对最终用户友好的响应格式。实现在 actionpack/lib/action_dispatch/middleware/show_exceptions.rb。ActionDispatch::StaticActionDispatch::Static负责从public目录伺服静态文件。当config.public_file_server.enabled为false时它会被禁用。实现在 actionpack/lib/action_dispatch/middleware/static.rb。ActiveRecord::Migration::CheckPendingActiveRecord::Migration::CheckPending会检查是否存在待执行的迁移当config.action_dispatch.x_sendfile_header被设置为:page_load时若发现有待执行迁移会抛出ActiveRecord::PendingMigrationError。ActiveSupport::Cache::Strategy::LocalCache::MiddlewareActiveSupport::Cache::Strategy::LocalCache::Middleware是内存本地缓存in-memory local cache的中间件。该缓存不是线程安全的只被设计为单个线程的临时内存缓存。Propshaft::QuietAssetsPropshaft::QuietAssets会抑制suppress对静态资源请求的日志输出。它由 PropshaftRails 默认的资源管线 gem提供因此不出现在本仓库的actionpack/railties源码中。Rack::ConditionalGetRack::ConditionalGet基于if-none-match与if-modified-since支持条件GET请求。如果请求的页面没有变化则返回304 Not Modified和空响应体。Rack::ETagRack::ETag为所有 String 类型的响应体添加ETag头。ETag 被用来校验缓存从而促成上面提到的条件GET请求。参见 Caching with Rails缓存指南 中关于条件 GET 的说明。Rack::HeadRack::Head对所有HEAD请求返回空响应体其它请求则原样放行。Rack::LockRack::Lock会把每个请求锁进一个 mutex从而让每个请求都被串行同步执行。只有当config.allow_concurrency被显式设置为false时即应用代码非线程安全它才会出现在栈中。Rack::MethodOverrideRack::MethodOverride允许在params[:_method]被设置时覆写 HTTP 方法。这正是 Rails 支持浏览器原生不具备的PUT、PATCH、DELETE方法的底层机制表单通常通过隐藏的_method字段来模拟。Rack::RuntimeRack::Runtime设置X-Runtime响应头其中包含执行本次请求所消耗的时间单位秒。Rack::SendfileRack::Sendfile会设置一个服务器专属的X-Sendfile头。如果你使用了类似 Apache 或 Nginx 这样的反向代理服务器它可以用于加速文件发送——例如对 Apache 可以配置为X-Sendfile。通过config.action_dispatch.x_sendfile_header配置项进行设置。Rack::TempfileReaperRack::TempfileReaper负责清理用于缓冲 multipart 请求的临时文件tempfile。Rails::Rack::LoggerRails::Rack::Logger会通知日志请求已开始请求结束后冲刷flush所有日志。实现在 railties/lib/rails/rack/logger.rb。提示TIP以上任何中间件都可以直接复用在你自己搭建的自定义 Rack 栈中。自定义中间件你可以编写自己的中间件并把它接入 Rails 应用。创建中间件自定义中间件文件应当放在lib/目录下并手动require因为中间件不会被自动重载这也是上文中修改后必须重启的另一层原因。下面的示例从 URL 参数中读取locale值将其存入 Rackenv随后从查询参数中删除它这样当请求最终进入控制器时locale不会混入params哈希保持参数干净# lib/middleware/extract_locale.rb module RackMiddleware class ExtractLocale def initialize(app) app app end def call(env) request ActionDispatch::Request.new(env) if request.params[locale].present? env[myapp.locale] env[action_dispatch.request.query_parameters][locale] env[action_dispatch.request.query_parameters].delete(locale) env[action_dispatch.request.parameters].delete(locale) end app.call(env) end end endRails 默认并不会创建lib/middleware/目录所以需要你自己创建。建议把该目录从自动加载autoload路径中排除以避免自动加载带来的问题# config/application.rb module MyApp class Application Rails::Application # ... config.autoload_lib(ignore: %w[assets tasks middleware]) # ... end endconfig.autoload_lib(ignore: ...)是Rails::Application提供的便捷方法它把lib目录加入 autoload 路径同时通过ignore指定其中不希望参与自动加载的子目录中间件是典型的必须手动require的类型。把自定义中间件加入栈自定义中间件可以在application.rb中添加# config/application.rb # ... require_relative ../lib/middleware/extract_locale module MyApp class Application Rails::Application # ... config.middleware.use RackMiddleware::ExtractLocale # ... end end也可以在独立的初始化器initializer中添加# config/initializers/extract_locale.rb require #{Rails.root.join(lib, middleware, extract_locale)} Rails.application.config.middleware.use RackMiddleware::ExtractLocale两条路径等价最终都会通过MiddlewareStackProxy把use操作应用到真实的中间件栈上。放在独立初始化器里的好处是你可以把中间件的注册与被加载的应用上下文解耦便于按环境条件判断后决定是否挂载。在 Rails 中访问 Rack 底层 APIRack 的底层 API 可以在 Rails 控制器里直接使用。读取 Rackenv在 Rails 控制器中可以通过request.env拿到 Rackenv哈希class HomeController def index user_agent request.env[HTTP_USER_AGENT] # ... end end常见的做法是在控制器里读取由前置中间件例如上面自定义的ExtractLocale写入env的自定义键实现跨层数据传递。直接编写 Rack 响应你也可以在 Rails 控制器中直接写出一个 Rack 响应class HomeController def index self.response Rack::Response[200, {}, [Im Home!]] end end注意这里使用的是 Rack 3 风格的Rack::Response[status, headers, body]类方法构造形式。这样写相当于绕过视图渲染把响应主体完全交给自定义三元组。当然Rails 主推的仍然是正常的render机制这种写法适合对响应做最底层控制的少数场景。把路由指向 Rack 应用你可以在config/routes.rb中把请求路由到任意 Rack 应用。详细说明参见 路由指南 中关于把路由指向 Rack 应用routing to Rack applications的小节。这也是 Rails 与 Rack 生态诸如挂载 WebSocket 端点、Rack 中间件型独立服务衔接的标准入口。小结Rails 与 Rack 的关系可以浓缩成三句话Rails.application是一个可以被任何 Rack 服务器驱动的 Rack 应用这个应用由ActionDispatch::MiddlewareStack相对Rack::Builder功能更强组装而成整条栈可以用bin/rails middleware一览无余并通过config.middleware系列 API 精确调整。当默认的 20 多个内置中间件无法满足需求时把自定义中间件放进lib/并手动require再以use或精确位置插入的方式注册即可在保证请求处理逻辑可复用、可测试的前提下扩展 Rails 的能力边界。文中所涉及的核心实现均可直接在仓库源码中继续研读中间件栈引擎见 actionpack/lib/action_dispatch/middleware/stack.rb默认栈的组装逻辑见 railties/lib/rails/application/default_middleware_stack.rb配置代理见 railties/lib/rails/configuration.rb对应的行为测试见 actionpack/test/dispatch/middleware_stack_test.rb。【免费下载链接】railsRuby on Rails项目地址: https://gitcode.com/GitHub_Trending/rai/rails创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进