Rails对HTTP条件GET请求的支持

 

原文地址

对条件 GET 请求的支持

条件 GET 请求是 HTTP 规范的一个特性,以此告诉 Web 浏览器,GET 请求的响应自上次请求之后没有变化,可以放心从浏览器的缓存中读取。

为此,要传递HTTP_IF_NONE_MATCHHTTP_IF_MODIFIED_SINCE首部,其值分别为唯一的内容标识符和上一次改动时的时间戳。浏览器发送的请求,如果内容标识符(etag)或上一次修改的时间戳与服务器中的版本匹配,那么服务器只需返回一个空响应,把状态设为未修改。

服务器(也就是我们自己)要负责查看最后修改时间戳和HTTP_IF_NONE_MATCH首部,判断要不要返回完整的响应。既然 Rails 支持条件 GET 请求,那么这个任务就非常简单:

class ProductsController < ApplicationController

  def show
    @product = Product.find(params[:id])

    # 如果根据指定的时间戳和 etag 值判断请求的内容过期了
    # (即需要重新处理)执行这个块
    if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key)
      respond_to do |wants|
        # ... 正常处理响应
      end
    end

    # 如果请求的内容还新鲜(即未修改),无需做任何事
    # render 默认使用前面 stale? 中的参数做检查,会自动发送 :not_modified 响应
    # 就这样,工作结束
  end
end

除了散列,还可以传入模型。Rails 会使用updated_atcache_key方法设定last_modifiedetag

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])

    if stale?(@product)
      respond_to do |wants|
        # ... 正常处理响应
      end
    end
  end
end

如果无需特殊处理响应,而且使用默认的渲染机制(即不使用respond_to,或者不自己调用render),可以使用 fresh_when简化这个过程:

class ProductsController < ApplicationController

  # 如果请求的内容是新鲜的,自动返回 :not_modified
  # 否则渲染默认的模板(product.*)

  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, etag: @product
  end
end

有时,我们需要缓存响应,例如永不过期的静态页面。为此,可以使用 http_cache_forever辅助方法,让浏览器和代理无限期缓存。

默认情况下,缓存的响应是私有的,只在用户的 Web 浏览器中缓存。如果想让代理缓存响应,设定public: true,让代理把缓存的响应提供给所有用户。

使用这个辅助方法时,last_modified首部的值被设为Time.new(2011, 1, 1).utcexpires首部的值被设为 100 年。

使用这个方法时要小心,因为浏览器和代理不会作废缓存的响应,除非强制清除浏览器缓存。

class HomeController < ApplicationController
  def index
    http_cache_forever(public: true) do
      render
    end
  end
end

强 Etag 与弱 Etag

Rails 默认生成弱 ETag。这种 Etag 允许语义等效但主体不完全匹配的响应具有相同的 Etag。如果响应主体有微小改动,而不想重新渲染页面,可以使用这种 Etag。

为了与强 Etag 区别,弱 Etag 前面有W/

W/"618bbc92e2d35ea1945008b42799b0e7" # => 弱 ETag
"618bbc92e2d35ea1945008b42799b0e7"   # => 强 ETag

与弱 Etag 不同,强 Etag 要求响应完全一样,不能有一个字节的差异。在大型视频或 PDF 文件内部做 Range 查询时用得到。有些 CDN,如 Akamai,只支持强 Etag。如果确实想生成强 Etag,可以这么做:

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, strong_etag: @product
  end
end

也可以直接在响应上设定强 Etag:

response.strong_etag = response.body
# => "618bbc92e2d35ea1945008b42799b0e7"