这两天写Jedi时涉及到一个小问题。Jedi的可能特性之一是可为不同的UA自动输出不同的HTML/CSS等(比如当遇到html5新标签,对IE6~8输出带namespace的xml标签)。
但是如果不考虑一开始就根据UA来redirect到不同的URL,那么就意味着get同一个URL会产生不同的response,这会影响cache机制。
熟悉HTTP协议的同学可能很快想到可以发出Vary头。不过印象中Vary似乎得到支持有不少问题,因此特别去google了一下。果然,找到一篇IE团队的人写的blog:Vary with Care。
根据该文,IE背后的WinINET引擎压根不保存请求头,也就是说IE(以及所有用WinINET的应用)只能根据URL得到Cache,根本无法支持Vary——Vary的本质是根据“URL + 指定的headers”得到Cache。
那么IE如果看到带有Vary头的response,会怎样?它就会认为该response是不可缓存的!也就是发Vary头就会导致IE的client cache失效。
不过还有一点点特别处理。
第一,IE会认 Vary: User-Agent 。
所以说IE团队还是有聪明人的。因为对于一个浏览器来说UA总是永远一样的(废话嘛),所以不存在不同的cache的可能。尽管我个人怀疑这是否导致其他问题,比如是否影响其他用WinINET的应用(因为按理说它们的UA并不是IE嘛)——不过先不担心这些了,反正这真是好消息,因为回到我的最初需求,就是针对不同的UA输出不同的内容。所以我确实可以通过Vary: User-Agent来保证Cache的正确。不过问题是,如果还有其他Vary因素呢?
第二,对于压缩过的响应,IE6在解码后会抛弃整个Vary头——这真是一个毫无道理的做法,唯一的解释是程序员认为Vary只有一个可能是Vary: Accept-Encoding。IE7开始则只去掉Accept-Encoding头。看上去这比IE6正常了点,不过其实不管IE6还是IE7都有极大问题——那就是IE只针对压缩响应扔掉AE头,如果是未压缩响应则不会。
这其实说明IE程序员没搞懂Vary: Accept-Encoding的用法。实际上Server对于某个URL只要支持压缩,就应该总是输出Vary:AE,即使该响应并没有压缩(比如对于不支持解压的client发送的响应)。因为如果不输出Vary:AE的话,可能导致链路上的Proxy看到一次未压缩的响应,就只cache该响应(因为没有Vary头告诉Proxy实际上我还支持压缩版本)。而那些支持解压的client就无法得到压缩的好处了。
然而如果IE收到一个未经压缩但是也带有Vary:AE的响应,Vary:AE头不会被干掉,那么会怎样?就会回到我们最初那个情况,导致Cache失效!我勒个去!于是IE6会很坑爹的重发请求,而IE7稍微聪明点,如果有ETag的话,会发If-Modified-Since,那么至少可能拿个304回来。
一直到IE9,才有比较正常的做法:IE9会忽略Vary中的以下内容及其任意组合:Accept-Encoding、User-Agent、Host(本来在Vary中写Host就是比较蛋疼的事情,因为不同host的请求肯定是vary的,但真这么写了,你也不要跟写的人一样智商嘛,所以IE9的智商确实提高了)。
总结一下:
为了确保IE6~8不至于客户端cache完全失效,可用 Vary: User-Agent, Accept-Encoding 并开启压缩。
对于IE6,在解压后整个Vary头被丢弃,就OK。
对于IE7和8,解压后AE被去掉,剩下的UA则会被忽略。
如果有其他的Vary需求,可用不同的URL,比如Vary:Accept就用*.html、*.json之类,而Vary:Accept-Language就用*.en、*.zh之类的。这就是Apache的content negoation模块做的事情。
最后对于不压缩的(比如以压缩传输的脚本,在没打过patch的IE6上会有问题),不能加AE,否则会导致IE的cache失效。但为了保证Proxy不被未加AE的响应导致只缓存未压缩版本,可为此响应加上Cache-control: private。
注:以上是根据前述blog和相关comments做出的结论。更多的问题也许需要实测。