Ruby-china 上有几篇讨论 Etag 的帖子,比如
要了解 Etag 的使用,推荐先看看上面的文章和讨论。但是我好像没有看到过介绍 rails 中使用 Weak Etag 的。
在讨论 Weak Etag 前,先解释一下 rails 自带的中间件 Rack::ETag 与自己写 fresh_when/is_stale 的区别。使用 Rack::ETag 能够节省网络传输时间,并不节省服务器端的计算量;而使用 fresh_when 则能节省页面渲染+网络传输时间,复杂一些的 Rails 的页面渲染一般需要 10ms 以上,这是 fresh_when 相比 Rack::ETag 能够节省下来的。我的做法是重要的页面自己使用 fresh_when 计算 Etag,其它统统让 Rack::ETag 处理。
但是用 Rails 生成 Etag 配合 Nginx 有个坑,就是使用 Nginx 的压缩的时候,Nginx 会简单的把 Etag 去掉,理由是压缩后的内容变化了,Etag 无效了。Quakewang 的帖子中提到了这个问题,并给出了两种解决办法:1.用 Rack 中间件进行 gzip 压缩,2.修改 nginx 源代码。其实这两种方案都不能让人满意。这个坑就引出了本文的主角 Weak Etag,对 Etag 详细的说明见下面的链接:
http://en.wikipedia.org/wiki/HTTP_ETag
简单来说,Weak Etag 和 (strong) ETag 的区别在于:Etag 保证文件的内容是完全相同的,而 Weak Etag 只保证文件的内容是语义上相同的。区分 ETag 和 Weak Etag 对一些 byte by byte 的操作有影响,比如断点续传等。
Weak Etag 就是在 Etag 前面增加了两个字符 W/,比如:
"123456789" -- A strong ETag validator W/"123456789" -- A weak ETag validator
从 Nginx1.7.3 版本开始,其 gzip 模块能正确的处理 Weak Etag 了。源代码在下面两个文件中: src/http/modules/ngx_http_gzip_filter_module.c src/http/ngx_http_core_module.c
如果 response 中带 Weak Etag,那么 Nginx 的 gzip 模块不处理;如果 response 中带 strong Etag,那么 Nginx 会把它转化为 Weak Etag。
所以如果使用了 Nginx1.7.3 以后版本,让 rails 应用支持 etag 很简单。让 rails 生成的 Etag 成为 Weak Etag 即可,代码如下:
class WeakEtagMiddleware
def initialize(app)
@app = app
end
def call(env)
# make request etags "strong"
etag = env['HTTP_IF_NONE_MATCH']
if etag && etag =~ /^W\//
env['HTTP_IF_NONE_MATCH'] = etag[2..-1]
end
status, headers, body = @app.call(env)
# make response etags "weak"
etag = headers['ETag']
if etag && etag !~ /^W\//
headers['ETag'] = "W/#{etag}"
end
[status, headers, body]
end
end
也可以使用 gem 'rails_weak_etags', 代码拷贝自 http://stackoverflow.com/questions/18693718/weak-etags-in-rails
此外,Rack::ETag 在 1.6.0.beta2 以后,默认也生成 Weak Etag。 https://github.com/rack/rack/commit/12528d4567d8e6c1c7e9422fee6cd8b43c4389bf 如果你使用的 Rails 版本足够新,那么不需要做任何事情就能正确处理 Etag 了。我目前还是使用的 Rack 1.5 的版本,所以还是需要上面的解决方案。