在Ruby的轻量级Web框架Scorched中,路由系统本身已经足够灵活,但当项目规模扩大,我们会遇到这样的场景:同一个路径在不同子域名下要交给不同的控制器,或者带有特定请求头的客户端才能访问某组接口。如果把这些判断都塞进action方法里,代码会变得又臭又长。Scorched::Plugins::RoutingConstraints正是为解决这类问题而设计的插件,它把“请求是否满足某条件”这件事提前到路由匹配阶段,只有符合条件的请求才会被对应的路由捕获。

插件的基本工作机制
Scorched::Plugins::RoutingConstraints的核心思路是给路由附加一个“约束器”。约束器可以是一个返回布尔值的代码块,也可以是一个响应matches?方法的对象。在路由定义时,通过插件提供的方式把约束器与路径绑定,框架在收到请求后会依次检查已注册路由的约束,只有约束通过才会将请求分派给该路由的处理器。这种设计让路由表本身具备了描述能力,开发者读路由文件就能知道什么请求走哪条路。
从底层看,插件在Scorched的路由匹配流程中插入了一个前置过滤。原生Scorched根据HTTP方法、路径模式进行匹配,插件在此基础上增加了一层条件断言。由于约束逻辑在路径匹配之后、处理器执行之前运行,因此它不会干扰框架原有的快速路径匹配算法,只是在候选路由集合中再做一次筛选。这也意味着约束写得越轻量,对整体吞吐的影响越小。
下面是一段最小可用的约束路由示例,展示了如何用代码块限制只有带特定请求头的请求才命中:
require 'scorched'
require 'scorched/plugins/routing_constraints'
class App < Scorched::Controller
plugin Scorched::Plugins::RoutingConstraints
constrain -> (req) { req.env['HTTP_X_API_VERSION'] == '2' } do
get '/users' do
'来自API v2的用户列表'
end
end
get '/users' do
'默认版本的用户列表'
end
end
约束器的多种定义方式
除了直接用代码块,插件还支持用对象封装约束逻辑,这在多个路由需要复用同一套规则时特别有用。我们可以定义一个普通类,实现matches?实例方法,方法接收请求对象并返回布尔值。这样做把条件判断从路由文件里抽离出来,便于单元测试,也符合单一职责原则。例如一个限制内网IP的约束对象,可以在不同模块里被反复引用。
另一种常见做法是组合多个约束。插件通常允许在一个constrain块里嵌套多个条件,或者连续调用约束方法。框架的语义一般是“全部满足才算通过”,但如果你需要“满足其一即可”的语义,就应该在代码块内部自己用或逻辑处理。下面的例子演示了用对象约束子域名,并在其内再嵌套一个请求方法约束:
class SubdomainConstraint
def initialize(subdomain)
@subdomain = subdomain
end
def matches?(req)
req.host.split('.').first == @subdomain
end
end
class App < Scorched::Controller
plugin Scorched::Plugins::RoutingConstraints
constrain SubdomainConstraint.new('admin') do
get '/dashboard' do
'管理员面板'
end
end
constrain -> (req) { req.request_method == 'POST' } do
post '/webhook' do
'仅接受POST的webhook'
end
end
end
对比在action里写if req.host.start_with?('admin')这种散落判断,约束器方式让路由注册表即文档。新成员看代码时,不需要追踪每个方法内部的前置判断,就能理解流量分布。同时,当某个约束条件变更,比如子域名规则调整,只需改约束类一处,所有关联路由自动生效。
在真实项目中的落地与注意点
在多租户SaaS场景中,我们常根据租户域名加载不同配置。借助RoutingConstraints,可以把租户识别放到路由层,未识别的域名直接落到一个统一的404或引导页路由,而不是进入业务action后再渲染错误。这降低了业务代码的防御性编程负担。例如把租户查找写成约束器,查找失败就返回false,请求自然流向默认路由,由默认路由返回友好的入驻引导页。
使用插件时也要注意约束器的执行顺序。Scorched按照路由定义的先后做匹配,前面的路由若约束宽松,可能“吃掉”本该留给后面严格约束路由的请求。因此建议把约束更具体、更严格的路由写在前面,宽泛的兜底路由放在最后。此外,约束块里不要做重IO操作,如同步调用远程鉴权服务,否则会阻塞请求线程,拖慢整个节点的响应。若必须做外部校验,应考虑在反向代理或中间件层完成,只把轻量本地判断留给路由约束。
最后看一个综合示例:同时按子域名和请求头版本号路由,并保留一个无约束的健康检查路径。它体现了约束路由与普通路由和平共处的写法:
class ApiVersionConstraint
def initialize(version)
@version = version
end
def matches?(req)
req.env['HTTP_X_API_VERSION'] == @version
end
end
class App < Scorched::Controller
plugin Scorched::Plugins::RoutingConstraints
get '/health' do
'OK'
end
constrain ApiVersionConstraint.new('1') do
get '/orders' do
'v1订单接口'
end
end
constrain ApiVersionConstraint.new('2') do
get '/orders' do
'v2订单接口,字段更丰富'
end
end
get '/orders' do
'未带版本头的默认订单接口'
end
end
通过上述安排,我们让路由层承担了原本混杂在控制器中的分流职责。Scorched::Plugins::RoutingConstraints的价值不在于多出什么神奇功能,而是提供了一种清晰的边界:路径与条件的组合在注册时一目了然,运行时高效筛选,维护时改动集中。对于追求简洁又不愿引入重型框架的团队,这种插件化的约束路由是值得纳入工具箱的 practica 方案。
ScorchedRoutingConstraints路由约束修改时间:2026-08-17 18:06:33