Camping是Ruby世界里出了名的“小而美”框架,整个框架源码不到两千行,却把MVC的核心思路都装了进去。它的路由设计比较特别:不是像Rails那样用独立的routes文件声明式地写路由表,而是把路由信息直接嵌在Controller类的继承结构里。这套机制全部由Camping::Controllers模块提供,理解了它,基本就理解了Camping的一半。

一、Controller与R路由块的基本原理
在Camping中,每一个Controller类对应一组URL。路由匹配靠的是类内部的R路由块,写法是R '/路径/:参数'这种形式。Camping启动时会扫描所有Controller,把R块里的路径模式编译成正则表达式,请求进来后按定义顺序逐个匹配,命中哪个Controller就实例化哪个。
下面是一个最简单的例子,包含两个Controller,分别处理首页和文章详情页:
module Blog::Controllers
class Index
# 匹配根路径 /
R '/'
def get
"欢迎来到我的博客"
end
end
class Show
# 匹配 /posts/1、/posts/abc 这类路径
R '/posts/(\d+)'
def get(id)
"这是第 #{id} 篇文章"
end
end
end注意路径里那个(\d+),它是一个正则捕获组,匹配到的内容会作为参数传给对应的动作方法,所以get方法需要声明一个id形参来接收。这一点和Rails的:id符号参数不同,Camping底层就是纯正则,捕获组按顺序对应方法参数,理解了这一点写路由就不会混乱。
还有一个容易忽略的细节:路由匹配的优先级与类定义顺序无关,而与模块内类名的字母排序有关。如果你的两个路由模式有重叠,比如一个匹配/posts/(\d+),另一个匹配/posts/new,一定要想清楚哪一个类名排在前面,否则可能出现请求被意外接管的情况。这是Camping最经典的坑之一。
二、RESTful动作的定义与参数处理
Camping对RESTful的支持非常直接:Controller里的public方法名就是HTTP方法名。get处理GET请求,post处理POST请求,put、delete同理。如果请求方法找不到对应方法,Camping会尝试调用method_missing相关的默认逻辑,最终返回405状态码。
表单参数和URL参数的接收方式也很朴素,全部通过input这个HashLike对象:
module Blog::Controllers
class Create
R '/posts'
def post
# 接收表单或JSON提交的参数
title = input.title
content = input.content
Post.create(title: title, content: content)
redirect Index
end
def get
# GET /posts 返回文章列表JSON
@posts = Post.all
render :list
end
end
class Delete
R '/posts/(\d+)'
def delete(id)
Post.find(id).destroy
redirect Index
end
end
end代码里的redirect Index也是Camping的特色写法,直接传Controller类就能跳转到该类的默认路由,不用手写URL字符串,重构路径时不容易漏改。
对于PUT和DELETE请求,由于HTML表单原生不支持,可以通过带_method隐藏字段的POST来模拟,Camping会自动识别并转发到对应动作。另外,环境相关信息可以从env和request中拿到,比如@env['HTTP_USER_AGENT'],写接口日志或做UA判断时用得上。
三、过滤器的编写与执行时机
过滤器在Camping里体现为几个约定方法:initialize、service和respond。整个请求生命周期是:先创建Controller实例,调用initialize做初始化,接着调用service做路由分发,service内部调用具体的动作方法,最后调用respond包装输出。覆写这三个方法中的任意一个,就相当于插入了一个过滤器。
不过直接在每个Controller里覆写太啰嗦,Camping提供了before方法,可以配合一个基类实现全局拦截:
module Blog::Controllers
# 所有Controller的公共基类
class Base < R
def service(*a)
# 请求日志过滤器
start = Time.now
r = super(*a)
log.info "请求 #{@env['PATH_INFO']} 耗时 #{Time.now - start}"
r
end
end
class Admin < Base
R '/admin'
# 鉴权过滤器
def before
# 也可以在Base中统一处理
@user = User.find(cookie.user_id) rescue nil
unless @user && @user.admin?
redirect '/login'
end
end
def get
"管理后台,欢迎 #{@user.name}"
end
end
end上面代码用了两层过滤:基类的service负责打点日志,子类的before负责登录校验。注意service里必须调用super并返回其结果,否则请求会在这里被“吞掉”,直接返回空响应。这类bug很隐蔽,页面不报错只是空白,排查时优先检查过滤器有没有正确传递返回值。
过滤器的执行顺序是固定:initialize、before、具体动作、respond。如果before里调用了redirect或throw,动作方法不会执行。利用这个特性可以把权限校验全部收口到过滤器层,让动作方法保持纯粹的业务逻辑。
四、实战中的一些经验总结
路由数量多了以后,建议按资源拆分Controller,一个资源一个类,类名用资源名单数形式,方便推断排序优先级。共享逻辑抽到Base类,避免过滤器代码到处复制。
调试路由时可以在项目根目录跑camping blog.rb启动服务,配合终端输出的请求日志快速确认请求到底进了哪个Controller。如果发现404,先检查R块的正则是否真的能匹配目标路径,用"/posts/123".match(%r{/posts/(\d+)})这样的单行代码在irb里验证正则,比反复重启服务快得多。
最后提醒一点,Camping的会话和Cookie默认通过cookies和state方法访问,state存在签名Cookie里,适合放登录态这类轻量数据,不要往里塞大对象。把路由、动作、过滤器这三块吃透,用Camping搭一个小型API服务或工具页面,代码量往往不到Rails项目的十分之一,维护起来相当轻松。