Hanami(原Lotus)是一个面向对象的Ruby Web框架,它的缓存模块设计得非常精细,其中Hanami::Action::Cache::MaxAge::Expires::Format负责将开发者设置的缓存时间转换为符合HTTP规范的响应头格式。很多初学者在使用Hanami的缓存功能时,会因为格式参数写法不对而导致缓存完全不生效,页面每次请求都回源到服务器。本文将深入分析这个内部格式化类的实现原理,并给出在实际项目中正确设置缓存过期时间的完整方案。

一、HTTP缓存基础:max-age与Expires的区别
在讨论Hanami的实现之前,必须先理解HTTP协议中两种缓存过期机制的本质差异。Cache-Control头中的max-age=N表示资源从响应生成那一刻起,N秒之内被认为是新鲜的,这是一个相对时间;而Expires头指定一个绝对的时间点,例如Expires: Wed, 21 Oct 2025 07:28:00 GMT,到达该时间后缓存即过期。
两者的优先级关系是:当Cache-Control与Expires同时存在时,现代浏览器以Cache-Control为准。之所以仍然保留Expires,主要是为了兼容HTTP/1.0时代的旧代理和客户端。Hanami内部正是通过Format类把开发者传入的秒数同时转换为这两种头部,达到新旧客户端通吃的兼容效果。
另外需要注意,绝对时间依赖客户端时钟的准确性。如果用户设备时间偏差较大,Expires的判断就会出错,这也是RFC规范推荐优先使用max-age的根本原因。理解了这一点,就能明白为什么Hanami把核心API设计成接收秒数而不是日期字符串。
二、Hanami中设置缓存过期时间的方法
Hanami提供了简洁的DSL来控制action的缓存行为,核心方法是cache_control和expires。前者用于设置Cache-Control头,后者用于设置Expires头。下面是一个典型的使用示例:
class ShowArticle
include Web::Action
# 设置Cache-Control: max-age=3600, public
cache_control public: true, max_age: 3600
# 同时设置Expires头,Expires接受秒数,内部自动换算为HTTP日期格式
expires 1800
def call(params)
# 业务逻辑
end
end这段代码中,max_age: 3600表示缓存一小时,public: true表示响应可以被共享缓存(如CDN、代理服务器)存储。expires 1800传入的是相对秒数,Hanami会在内部调用Format模块,把1800秒加到当前时间上,再格式化为标准的RFC 1123日期字符串写入Expires头。
还可以根据请求的HTTP方法动态调整缓存策略,例如只对GET请求启用缓存:
class ShowProduct
include Web::Action
def call(params)
if request.get?
# 针对GET请求设置缓存
headers.merge!(
'Cache-Control' => 'max-age=600, must-revalidate'
)
else
headers.merge!(
'Cache-Control' => 'no-store'
)
end
end
endmust-revalidate指令告诉缓存,一旦资源过期,必须向源服务器重新验证,不能直接使用过期副本。对于包含敏感数据的接口,使用no-store彻底禁止缓存是最安全的做法。
三、Format类的格式化原理与常见误区
Hanami源码中,Format类使用了Ruby标准库的Time#httpdate方法来生成符合HTTP规范的日期字符串。它的核心逻辑可以简化理解为:取当前时间,加上指定的秒数,再格式化输出。正是因为它接收的是秒数而非日期字符串,所以像expires '2025-01-01'这样直接传日期的写法是错误的,会导致类型转换异常或头部格式非法。
第一个常见误区是单位混淆。max_age的单位是秒,如果想设置24小时应该写86400,写成24会导致缓存几乎立即过期。建议在代码中使用具名常量提高可读性:
class ShowFeed include Web::Action HOUR = 3600 DAY = HOUR * 24 cache_control public: true, max_age: DAY expires DAY def call(params) end end
第二个常见误区是误以为设置了缓存头就一定生效。实际上缓存头只是建议,最终是否缓存还取决于请求方法、响应状态码以及中间是否有代理改写头部。POST、PUT等非幂等请求的响应默认不会被缓存;带Set-Cookie头的响应也不应被共享缓存存储。排查缓存不生效时,应先用curl命令检查实际下发的响应头:
curl -I https://your-app.ipipp.com/articles/1 # 观察输出中的Cache-Control和Expires字段是否符合预期
四、动态内容与静态资源的差异化缓存策略
对于纯静态资源如图片、CSS、JavaScript文件,推荐设置较长的max-age并配合文件指纹(如app-a3f8d2.js)实现永久缓存,即使一年也没问题;对于HTML页面和API响应,则应设置较短的缓存时间,或者使用ETag配合条件请求实现协商缓存。Hanami中可以这样组合:
class ShowProfile
include Web::Action
cache_control private: true, max_age: 0, must_revalidate: true
def call(params)
body = render_profile(params[:id])
# 基于内容生成ETag,内容未变时返回304,节省带宽
etag = Digest::MD5.hexdigest(body)
if request.env['HTTP_IF_NONE_MATCH'] == etag
self.status = 304
self.body = ''
else
headers.merge!('ETag' => etag)
self.body = body
end
end
end协商缓存的好处是客户端每次仍会发起请求,但服务器只在内容变化时才传输完整响应体,304响应只有头部信息,体积极小。这种策略特别适合频繁更新的动态页面,比盲目调大max-age更可靠。
总结来看,Hanami::Action::Cache::MaxAge::Expires::Format虽然是一个内部类,但它承载了从Ruby秒数到HTTP标准日期格式的关键转换。掌握max-age与Expires的语义差异、坚持用秒数而非日期字符串传参、结合ETag实现协商缓存,就能在Hanami项目中构建出高效且可控的缓存体系。遇到缓存异常时,优先用curl验证实际响应头,再反推配置问题,这是最快定位问题的路径。
Hanami缓存Hanami::ActionMaxAge设置修改时间:2026-09-01 21:14:35