Rails 在 Rails 中使用 HTTP 缓存

heroyct · June 03, 2018 · 4936 hits
Topic has been selected as the excellent topic by the admin.

为了改善页面的响应速度,在 rails 里面使用了 HTTP 缓存,做下总结

什么是 HTTP 缓存

这里讲的很清楚了,就不多说了
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching_FAQ

rails 的 HTTP 缓存

默认缓存是开启的

随便打开一个页面,在 chrome 的 Network 里面查看 request 和 response,可以看到下面的内容

response header

Cache-Control: max-age=0, private, must-revalidate
ETag: W/"25200a77a0ae51ac9511df4ac1e9a0d5"

request header

If-None-Match: W/"64a30229a112c1819d754ef8a7ad8876"

大概意思是,进行 (私有) 浏览器缓存,每次都会验证请求的状态
比较他们的 etag 的值,如果是一样的就返回 304

手动指定 Cache 的时间

可以看到默认的 max-age 是 0,我们可以手动指定这个时间

expires_in 1.hour

在 chrome 里面确认,这个时候要注意的是你如果直接更新页面的话无法确认结果

新打开一个 tab, 在 URL 栏里面点击 enter 进入才可以确认

image

这种比较适合静态页面或者是一段时间不更新也没有任何问题的页面

如何强制更新

比如上面设置了 1 个小时的更新,但是你想立刻更新页面的时候
可以在 URL 后面加上时期之类的来更新,比如?20180810120000

etag 是如何生成的

headers['ETag'] = Digest::MD5.hexdigest(body)

可以看出是根据每次返回的 response body 来生成的,如果你每次返回的 body 都一样,ETAG 也会一样

API

一个 API 返回 json 的时候,如果每次返回的值都一样,就会返回 304

HTML 页面

默认启用了 csrf, csrf-token 的值每次都会变化,所以基本都是 200

自定义 ETAG

每次都从 response body 来生成 ETAG,明显不是一个高效的方法
因为还是必须去 render html,生成 response body,
虽然可以减少传送的内容,但是服务器端的 response time 没有太大改善

实际应用中,我们很可能需要的是某个或者多个数据未改变,不用去 render html,直接返回 304
这个时候就可以使用 fresh_when

fresh_when

比如有个 book mode,我们希望 book 没有更新对话就返回 304,可以在 controller 里面这样写

def index
  fresh_when last_modified: @book.updated_at.utc, etag: @book
  fresh_when @book
end

这样的话 book 没有更新的话,就会返回 304

多个变量生成 ETAG

def index
  fresh_when [@book, current_user.try(:id)]
end

OR

class TopController < ApplicationController
  etag { current_user.try(:id) }
  fresh_when @book
end

book 没有更新并且是相同用户的话,才会返回 304

从 model 如何生成 etag

headers[Etag] = Digest::MD5.hexdigest(@book.cache_key)

model 的 cache_key 类似这样books/13-20180517145513000000000

<model name>/<id>-<updated_at>

从自定义的类如何生成 etag

def etag=(etag)
  key = ActiveSupport::Cache.expand_cache_key(etag)
  @etag = self[ETAG] = %("#{Digest::MD5.hexdigest(key)}")
end

def retrieve_cache_key(key)
  case
  when key.respond_to?(:cache_key) then key.cache_key
  when key.is_a?(Array)            then key.map { |element| retrieve_cache_key(element) }.to_param
  when key.respond_to?(:to_a)      then retrieve_cache_key(key.to_a)
  else                                  key.to_param
  end.to_s
end

可以看出最简单的实现自定义类的 etag 生成的方法就是实现 cache_key 方法
比如你有一个类来管理看过的书,看过的书有增加了就生成新的 etag

class BookHistory
  def cache_key
    # return read book ids
  end
end

禁止缓存

有时候我们需要禁止缓存,可以这样

def index
  expires_now
end

禁止缓存的情况感觉不多,感觉一般没必要用
我在 React(flux + immutable js) 的项目中,API 如果返回 304 的话
在 safari 里面有时候会取不到返回值,暂时采取了 safari 的话就禁止缓存

使用 fresh_when 需要注意的地方

影响页面变动的变量比较少的时候,我会采用 fresh_when
但是影响页面变动的变量多的话,用 fresh_when 会有一些问题

  1. 维护困难,必须搞清楚这个页面的所有变量,把他们都传到 fresh_when 里面去
    • 比如你可能在页面里面你使用了 book.author.name 来显示相关书的作者
    • book 的 author 变化了你需要更新页面,但你可能忘了把 author 也传给 fresh_when
  2. 上线后不容易发现问题,因为你看到没问题,某个用户可能看到的画面就有可能有问题

不知道大家使用时有没有什么好的方法来解决这些问题

总结

  1. 使用 HTTP 缓存可以很好的提高服务器的响应速度
  2. 如果是在 RestAPI 中,因为返回的内容基本是某个 model 的内容,比较推荐使用
  3. 在一般的 html 页面中,影响页面的变量少的时候可以使用 fresh_when,变量多的时候慎重使用

参考文章

huacnlee mark as excellent topic. 04 Jun 09:44
You need to Sign in before reply, if you don't have an account, please Sign up first.