所谓条件请求,就是 Server 根据请求头的条件的不同而返回不同的响应。
这里所说的条件即验证器,有两种:
最后修改时间 (last-modified) 和 内容指纹 (etag), 可以单独使用或者联合使用。
验证的过程区分 强验证
还是 弱验证
.
强验证
要求内容完成相同。
弱验证
只要求主体内容相同,允许部分片段有差异,具体规则自定义。
验证类型 x 验证器 | 最后修改时间 | 内容指纹 |
---|---|---|
强验证 | none | 逐 bit 比较,要求完全相同 |
弱验证 | none | 内容主体相同,允许部分变化 |
Request 无特殊 Header.
Response 返回内容的同时,在 Header 中设置了 ETag
和 Cache-Control
.
Cache-Control
中,
max-age=0
表示 0 秒后资源过期,也就是每次都要检查缓存,如果缓存还新鲜就用缓存.
no-cache
也要求很次都检查缓存,但强制要求重新请求,不使用缓存,即使缓存可用。
private
表示仅允许 user-agent 缓存该请求,不允许代理服务器缓存.
相对应的 public
允许 user-agent 和代理服务器缓存该请求。
must-revalidate
表示需要在使用缓存前校验缓存状态,不允许使用过期的缓存。
Request 携带了上一次的 ETag
值,放在 If-None_Match
中。
Response 取请求头中的内容指纹,跟准备返回的内容的指纹做比对,如果内容一致就响应 304
.
指纹中 W/
表示当前使用的是 弱验证,W
大小写敏感。
Request 依然携带上一次的 ETag 值。
Response 判断内容有更新 (指纹不一致), 响应 200
, 发送新的内容。
# GET /products/1
# GET /products/1.json
def show
end
刚才 GET 请求对应的 show 方法如上,也就是说这些处理过程都是自动完成的 :)
正常的处理完成,并且是 200
或 201
的响应,并且需要检查缓存时,
Rack 计算 body 的 SHA256, 设置 ETag
和 Cache-Control
.
rack/etag
def skip_caching?(headers)
(headers[CACHE_CONTROL] && headers[CACHE_CONTROL].include?('no-cache')) ||
headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
end
def digest_body(body)
parts = []
digest = nil
body.each do |part|
parts << part
(digest ||= Digest::SHA256.new) << part unless part.empty?
end
[digest && digest.hexdigest.byteslice(0, 32), parts]
end
默认的验证规则实际上是强验证,因为此时并没有定义弱验证规则,是对 body 整体的 hash.
如果我们需要自定义弱验证规则,可以使用 Rails 的 stale?
和 fresh_when
.
例如 product 的 GET 请求,如果我们定义只有产品的价格有变化才算是有效变化,其他变化都可以忽略。
def show
fresh_when(@product.price)
end
只修改 product 描述的时候,server 响应 304
, 浏览器依然使用旧的缓存;
修改价格后,响应 200
并返回新的内容。
actionpack-5.1.6/lib/action_controller/metal/conditional_get.rb
def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, template: nil)
weak_etag ||= etag || object unless strong_etag
last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
if strong_etag
response.strong_etag = combine_etags strong_etag,
last_modified: last_modified, public: public, template: template
elsif weak_etag || template
response.weak_etag = combine_etags weak_etag,
last_modified: last_modified, public: public, template: template
end
response.last_modified = last_modified if last_modified
response.cache_control[:public] = true if public
head :not_modified if request.fresh?(response)
end
在这个例子中,object 是 @product.price
, 它没有 updated_at
方法,Server 便不会设置 Last-Modified
头。
浏览器收不到 Last-Modified
就不会在下一次 GET 请求中携带 If-Modified-Since
, 也就是说这种情况下,只通过内容指纹进行验证。
如果 object 有 updated_at
方法,Server 会自动设置 Last-Modified
.
当然也可以手动设置,如 fresh_when(@product.price, last_modified: Time.now)
,这样也会令每次的缓存都失效。
还可以把 etag 声明到控制器中:
class ProductsController < ApplicationController
# sth...
etag { @product.try :price }
def show
fresh_when @product
end
# sth...
end
需要注意的是在方法中仍要使用 fresh_when
或 stale?
,否则使用默认的策略。
actionpack-5.1.6/lib/action_controller/metal/conditional_get.rb
def etag(&etagger)
self.etaggers += [etagger]
end
private
def combine_etags(validator, options)
[validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
end
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Conditional_requests
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/ETag
https://ruby-china.github.io/rails-guides/caching_with_rails.html#conditional-get-support