浏览器静态资源的版本控制新思路.强制更新指定资源缓存.的探讨
此篇,探讨的是一种可以让脚本自己更新自己缓存副本的能力. 因为上一版本,绝大多是朋友,给我的反馈是看不懂,所以我争取在这个重写的版本中.详细把每个细节都介绍一二. 如果大多数细节,都是您了解的,则跳跃性阅读即可. thx.
另:本文讨论的 方案,在国内的网络环境.很难实施. 仅仅是一种探讨 . 此篇内容非常多. 感谢您的宝贵时间,希望能耐心看完.
关于缓存
在开始之前,不得不提到 "web 缓存".如果您对它有充分理解,请直接跳过.
我们可以简单的理解下什么是资源文件的缓存, 比如一个页面中引入了一个脚本 a.js ,这个文件的内容可能不会经常变化. 所以每次打开这个页面, 如果都去服务器端加载这个脚本,而它又没有任何变化,就会显得很多余.浪费带宽和时间. 所以浏览器可以把一些不常改变的东西,做本地缓存,即,当我再次打开这个页面时,使用的a.js,不再通过网络,去服务器下载,而是直接使用浏览器缓存的副本. 一般我们称这种资源为"静态资源". 我们可以借助HTTP协议,通过一些缓存控制头域和配套的缓存控制指令,来告知浏览器.某个文件,你可以缓存多久, 在这个允许的时间范围内,你无须到服务器下载,而可以直接使用你的缓存副本. 有趣的是, 类似的缓存机制,不仅仅浏览器具备,充斥在整个网络中的一些代理服务器,也有相似的功能.所以我们称这种代理服务器为 "缓存代理服务器" .
你可以把浏览器,缓存代理,服务器原站,想象成这样子:
浏览器 : 你的学习笔记
缓存代理: 你的学习成绩优异的同桌同学 A
原始服务器: 你的老师.
当你遇到一个,老师曾经教过,但是被你遗忘的问题,你就先去问了同学A ,他说:"这个问题我会,我告诉你". 然后你再把答案记在自己的学习笔记上. 下次再遇到这个问题,你就无需再问.你只要看看自己的学习笔记就行了. 这个过程,就是缓存代理的作用,当缓存代理有a.js的副本,并且确认这个副本没有过期时,就会把副本直接作为响应返回给浏览器. 然后浏览器再把响应作为副本,自己缓存起来.
假如同学 A 也不知道这个问题的答案,我们看看会发生什么. 他为了面子,就对你说:"稍等下,我要上个厕所,回来告诉你." . 然后这货,跑到老师那边,得到了答案.记在他自己的笔记上.然后又跑回来,把答案告诉了你. 这就是缓存代理的回源过程.缓存代理,自己没有副本,或者确认副本过期了以后,就会这样做.
假如有一天,教育局更改了教材,发现那个问题的答案,其实一直有需要改进的地方. 又怎么办呢. 老实会紧急在课堂说对所有的同学们说:"对于问题A,老的答案可能有些问题,现在我们来重新学习一下这个问题....." . 然后同学们会如何呢? 会重新记一份笔记.把新的答案记下来.
后来,老师发现,来问问题的同学越来越多,严重耽误了他干别的事情, 他决定请一个助教,专门来应对来问问题的同学们.他把常见问题的答案都告诉给这个助教.让他在教研室门口,拦住所有来问问题的学生,并把他们想要的答案,告知他们. 这位助教扮演的角色,就是我们耳熟能详的"反向代理",又因为他也有一份答案保留.并不是每次都代替学生来问问题.所以,它也是缓存代理. 而反向代理和非反向代理的区别,就是, 同学A,是你主动去问的. 而助教是被动存在的.他是老师安排的. 所以从你的角度来说,助教就是反向代理.因为他是服务提供者安排并控制的.
再后来,由于教委的人抽风,经常要更新答案. 无论是同学,还是老师,都有些崩溃. 同学也发现,刚从其他同学 或助教那里得来的答案,往往和老师去教委开会后,获得的最新答案有出入. 有些细心的学生,逐渐开始不信任别的同学,甚至是助教的答案.他每次像问问题,被助教拦和其他同学拦下来时,都会大喊一句:"别想糊弄我,你给我去问老师". 迫于淫威,他们可能就真的不自作主张的把自己的答案告诉你.而去很抱歉的,打扰老师了. 这是一种传递性的情况. 也是我们后面要经常提到的 端对端重载的概念.. 但是现实和理想总是有些偏差, 你对同学A发威, 同学A买账了.结果他对拦在门口的助教说,这个问题,我们想知道老师怎么说.但是助教可能并不买账.因为老师交代过他.任何问题,你都别来烦我.直接把你的答案给他们就是了... 所以.往往你还是拿不到最新的答案. 这就是协议的遵守和违约. 有时候违约并不是单向的. 比如同学A,他很有自信,他为了不没事就往办公室跑. 即使老师交代他, 某个问题的答案最近可能有改动.如果明天有同学还问你某个问题.你一定要来我这里确认一下,拿到新的内容在做答复. 但是同学A,完全没当一回事,自作主张的把自己的答案直接交给了 来问的同学.并且还骗他说,这就是最新的.相信我吧... 这种人,就是我们后面可能要提到的,ISP,或一些内网的 流氓缓存代理所干的肮脏勾当.
前面的比喻,可能并不是特别恰当.但是基本上能说清楚这个过程了.
然后,我建议你可以详细的看看这个链接里的内容 : http://www.cnblogs.com/_franky/archive/2011/11/23/2260109.html
看完后,我们来提一下几个Http1.1 的Cache-Control头域的重要的伪指令, 你可以把这些指令看做是,老师的承诺,比如老师说,这个答案大家可以放心.在半年内,是不会有什么变化的. 指令也可以是学生发出的.比如学生对助教说, 你滚蛋,我要老师那里最新的答案..
如果,你看过我上面给出的链接的全部内容,那么下面这几个伪指令,你就不会陌生 :
1. max-age = x (单位秒) 缓存新鲜期.对所有代理有效,包括用户代理(也就是我们的浏览器)
2. s-maxage = x (单位秒) 缓存的新鲜期,仅对缓存代理有效(不包括我们的浏览器)
3. no-cache 请求头中出现这个是告知缓存代理不要拿你的缓存副本来骗我,请你回源. 说白了就是,我的期望是,服务器端直接返回给我一个最新鲜的版本.(对应名词:端对端重载)
请记住这几个东西,我们会在稍后的地方详细讲解对他们的使用. 现在,我们开始聊聊,我们到底要解决什么问题.
要解决的问题:
我们在日常开发中,引入的脚本资源大概长下面这个摸样:
<script src=http://www.a.com/a.js?v=20121212></script> <script src=http://www.a.com/20121212/a.js></script>
其中 20121212 部分我们是作为版本号来存在的.
当然,也有一种做法,并不是把日期作为版本号,比如拿文件的内容,用某种hash算法,获得一个字串,作为版本号.这样可以防止竞争对手枚举版本号方式变相攻击,导致缓存代理们,缓存错误的内容.但是不管怎么样.更新资源,我们总是需要改变资源的url. 这本身没有什么问题. 但假如我们没有办法修改url的资源就变的束手无策了. 这样的场景经常发生在第三方脚本身上. 比如我们的脚本作为第三方脚本,存在于某些渠道网站的页面中. 当文件更新时,我们可能没办法强迫哪些渠道修改脚本的url . 所以. 常见的做法有两种:
(1). 减少脚本的过期时间. 比如10分钟的过期时间(baidu联盟的脚本文件的过期时间是两个小时)
(2). 使用一个永不缓存的小加载器脚本.加载后面的主脚本. 通过修改加载器脚本中 主脚本的url来实现更新.
显然.两种方式,都是一些权衡的产物. 很难说哪个更好.也许只有更合适自身情况的方案.才是相对好的那一个. 而本文.主要想探讨的是第三种方案. 即脚本自更新技术.他是一种无版本号,且强制更新自身的方式. 但这个方案不是无懈可击的.至少通过我一个月的测试.发现他在中国的网络环境下.很难推广使用. 具体细节会在后面详细讨论.
让我们开始今天的正题: 假设我们有一种方法.可以在得知 xxx.js 已经有了新版本.需要客户端重新去服务器获取新版本时.立刻实现更新. 这个xxx.js.而且url并不发生变化. 那么是一件多么快意的事情啊. 我们来描述一下大概的场景.
A页面部署了 http://www.a.com/a.js . 这个脚本每次会动态的去服务器获取一些数据.填充到A页面. 这些数据是动态的.所以不涉及到缓存. 突然动态数据中有一个段数据是通知 a.js有了新版本要更新了. a.js控制浏览器,立刻向服务器重新发起一个到http://www.a.com/a.js 自身的请求.获取到新版本的内容.同时,浏览器更新本地缓存.并且.这次更新动作.不会再次解释执行a.js的内容. 这就是我们理想的更新模式. 有了这个模式. 我们可以把a.js的缓存时间设置为10年. 一但a.js很长时间都没内容更新.那么就不会有任何浪费性的请求到达服务器. 这时候,你可能会问. 用户刷新浏览器.是不会直接拿本地缓存的.而是会发起http请求. 至少也会有一个304的响应才对. 事实却是如此. 但是请记得. 还是有相当多的情况.用户只是不断的点链接跳来跳去.又或者在地址栏中直接输入网址,又或者从收藏夹中访问某个页面. 当这种情况下.如果a.js的缓存时间很短. 就会经常因a.js缓存过期,而导致浏览器发起http请求. 我们这个方案的最终目的之一.就是要消灭这部分额外的请求. 从而节省宝贵的流量. 流量的节省不仅仅体现在客户端, 还体现在 反向代理的回源花费的流量. 如果a.js的缓存时间很短. 非特殊设置下. 反向代理也同样会频繁的回源. 大多数CDN.可都是双向收费的. 减少回源.也是我们的目标之一. 现在我们简单总结下.这个方案的好处:
(1). 可以让静态资源拥有非常长的过期时间. 从而带来网络流量上的成本节省.
(2). 这个方案,可以做到需要更新静态资源时.立刻更新.并在用户下次访问时.使用最新的版本.
现在.我们就来讨论下这个方案的细节. 我们可能存在以下几个疑惑:
(1)如何保证浏览器发起一个对a.js的http请求.
(2)如何保证这次请求只更新缓存.而不解释执行a.js
对于问题(1) . 我们的答案是 . location.reload 这个BOM 方法 + ajax代理(少数浏览器需要). 我们需要借助一个iframe. 把要更新的资源放在iframe中. 然后调用iframe的 location.reload方法来做. 遗憾的是. 无论是location.reload 还是ajax 代理方式.各个浏览器的行为都不太一样. 这些行为差异.主要体现在是否会真的发起对a.js的请求. 以及发出的http请求头域的差异.
好在经过我不懈的测试.最终还是覆盖了所有主流浏览器,以及主流版本.
我们先来看看reload API 的一些情况.
下如果你不明白 Cache-Control, Pragma, if-None-Match, If-Modified-Since .以及直接走cache的含义.
我建议你先读一下: http://www.cnblogs.com/_franky/archive/2011/11/23/2260109.html
Browser | Cache-Control | Pragma | If-None-Match | If-Modified-Since | 是否仍然直接走cache |
IE6,IE7,IE8 | 无 | no-cache | 无 | 无 | 否 |
IE9,IE10 | no-cache | 无 | 无 | 无 | 否 |
Firefox | no-cache | no-cache | 无 | 无 | 否 |
Chrome | max-age=0 | 无 | 有 | 有 | 否 |
Safari | max-age=0 | no-cache | 无 | 无 | 否 |
Opera | no-cache | no-cache | 无 | 无 | 否 |
Browser | Cache-Control | Pragma | If-None-Match | If-Modified-Since | 是否仍然直接走cache |
IE6,IE7,IE8 | 无 | no-cache | 无 | 无 | 否 |
IE9,IE10 | no-cache | 无 | 无 | 无 | 否 |
Firefox | no-cache | no-cache | 无 | 无 | 否 |
Chrome | max-age=0 | 无 | 有 | 有 | 否 |
Safari3 | max-age=0 | no-cache | 无 | 无 | 否 |
Safari4+ | max-age=0 | no-cache | 有 | 有 | 否 |
Opera11.0- | - | - | - | - | 是 |
Opera11.64+ | no-cache | no-cache | 无 | 无 | 否 |
Browser | Cache-Control | Pragma | If-None-Match | If-Modified-Since | 是否仍然直接走cache |
IE6,IE7,IE8 | 无 | no-cache | 有 | 有 | 否 |
IE9,IE10 | 无 | 无 | 有 | 有 | 否 |
Firefox | max-age=0 | 无 | 有 | 有 | 否 |
Chrome | max-age=0 | 无 | 有 | 有 | 否 |
Safari | max-age=0 | no-cache | 无 | 无 | 否 |
Opera | no-cache | no-cache | 无 | 无 | 否 |
Browser | Cache-Control | Pragma | If-None-Match | If-Modified-Since | 是否仍然直接走cache |
IE6,IE7,IE8 | 无 | no-cache | 无 | 无 | 否 |
IE9,IE10 | 无 | 无 | 有 | 有 | 否 |
Firefox | max-age=0 | 无 | 无 | 有 | 否 |
Chrome | max-age=0 | 无 | 有 | 有 | 否 |
Safari3 | max-age=0 | no-cache | 无 | 无 | 否 |
Safari4+ | max-age=0 | no-cache | 有 | 有 | 否 |
Opera | - | - | - | - | 是 |
以上,四个表.其实我们要关心的是 硬编码外链脚本资源的情况.因为他们正是我们要更新的静态资源.
补充: 所谓硬编码即 直接写在html页面中的脚本.而不是借助dom api动态插入而加载的脚本.
在结果中.我们尤其要关注的是那些请求中带有 Cache-Control : no-cache 浏览器. 还记得前面我一再提到的端对端重载的概念么? 根据http1.1协议.这样做.会导致所有遵守http1.1协议的缓存代理服务器回源. 假如.所有的缓存代理都遵守http1.1的端对端重载. 那么这将给原站带来灾难. 瞬间带来海量的请求.如果我们当前的瞬时流量很大.压垮原站.那就是分分钟的事情.所以为了防止这种情况出现.我们必须强迫我们的反向代理(比如我们的CDN).不支持端对端重载. 然后在a.js有更新时,清除CDN对a.js的缓存(几乎所有的CDN,都会开发这种接口的.大公司自搭的更是如此). 这样做的结果.就是保证每个节点都只回源一次.压力可以忽略不计. 你一定会问.那就用reload(false) 呗. 这样就没有端对端重载了. 但是我们不要忘记 其他缓存代理的影响. 我们仍然还是希望用户和 反向代理之间的那些 缓存代理可以遵守端对端重载,从而正确的回源. (但实际情况,我自己测试下来,发现大部分缓存代理都不支持.端对端重载... 所以这里true,也好,false也罢.真的无所谓了. 只要你发起请求.我们就谢天谢地了.)
细心的你,一定发现Opera11.0- 似乎不能用reload . 没关系.我们可以用ajax 代理方式来做. 我们可以创建一个永久缓存的 html页面.根据url中的hash 部分.来传入要更新资源. 该html页面所在的域,与要更新的资源同域.即可有效的更新了. 你也许问.为啥其他浏览器不能这样做. 下面这个表,会给我们答案:
Browser | Cache-Control | 是否自动加入Pragma : no-cache | If-None-Match | If-Modified-Since | 是否仍然直接走cache |
IE | - | 否 | - | - | 是 |
Firefox | no-cache | 否 | 无 | 无 | 否 |
Chrome6- | max-age=0 | 否 | 无 | 无 | 否 |
Chrome7+ | no-cache | 否 | 无 | 无 | 否 |
Safari5.0- | max-age=0 | 是 | 无 | 无 | 否 |
Safari5.1+ | no-cache | 否 | 无 | 无 | 否 |
Opera10.7- | 无 | 否 | 无 | 无 | 否 |
Opera11+ | no-cache | 否 | 无 | 无 | 否 |
我们看到.IE.是不发起http请求的. 而其他浏览器虽然可以.但是我们为了节省对那个代理html的请求. 还是使用reload好了. 这时候你也许会说. reload ? ireload难道不也需要一个iframe 页面作为容器么? 答案是 no . 不需要. 我们只需要 iframe.src = about:blank 或 配合 DataURI 来做就好了. 这样就只有在 opera浏览器下.我们需要使用 ajax代理 访问额外的html页面了.
然后,我们来看看如何让资源加载,而不执行.或不渲染(对于.css来说. 是的.这个方案可不仅仅只对.js)
加载资源不执行不渲染的方式:
其实,针对不同浏览器,方法还是很多的. 参考地址:http://www.cnblogs.com/_franky/archive/2010/12/09/1901720.html 。有兴趣可以仔细看下.
我所推荐的方式是:
IE 全系, Opera全系, Safari5-, Chrome7-: 使用 <script type="text/c" src="xxx"></script> 方式加载资源
ps: 对于Safari5.0-,和Chrome6-,应注意加载资源的方式,.js采用 script,.css采用下面的link 方式.原因会在后面 非IE的 iframe,处理方案中详细解析.
Chrome8+, Safari5.1+, Firefox全系建议使用Link标签来做: <link rel="stylesheet" media="c" href="xxx" />
我那份关于资源预加载的帖子里描述的其他方法,或多或少都有其他一些缺陷,所以请记得,上面两种方式,就是你需要的.加载资源,不执行,不渲染的方式了.
原理就是 script的 type给个不认识的值, 以及 link stylesheet 的media 给个不认识的值. 就让所有主流的A级浏览器,达到了只加载资源的目的.
一切看起来还不错. 但是这引出了后面我们要说的一些恶心问题. 其中任何一问题,都一度让我想要放弃. 但最终我坚持了下载.并一一解决了. 仅作为一个 偏执狂的心酸历程.记录下来好了:
到此,原理方面,讨论结束. 我们来点实施上的经验:
显然,我们在更新一些资源的时候,他们要有iframe作为容器, 但是我们显然不希望这个iframe,部署一个真正的实体页面来做. 否则reload这个额外的iframe,也很苦恼.如何解决?
1. IE下 用 一个 src 为 about:blank的 iframe,通过动态写入文档流的方式,组织其html.来加载这些资源.
爽的地方就在这里, IE下,reload 一个动态写入的文档流.刷新后还是记得这些文档流内的内容. 就搞定问题了. 又因为 about:blank 被视为同parent页面同域. 所以似乎 iframe.contentWindow.document.write 就满足一切需求了.
但是很不幸, IE全系,至今都有一个同源策略的bug. 即 在我们创建一个 src 为about:blank的iframe前,parent 页面修改过document.domain. 则该iframe就被视为同parent页,跨域.也就是说 我们的iframe.contentWindow.document 是没权限的.
解决办法:
使用javascript: 伪协议. 即 设置iframe的url 为 javascript:document.write(xxx)的方式来写入文档流.实现同样的功能. 记住,write的机会只有一次.所以我们务必要一次性把内容写入. 但是当我们决定这样做的那一刻起,注定我们要面对IE系对我们设置的重重考验.因为这样做可能带来的问题如下:
(1). 当parent页存在 base标签,且target 为_blank时, 使用伪协议方式,会导致IE系弹窗(并可能会被浏览器拦截).
解决办法: 扫描parent页面的所有base标签,看是不是target为_blank.如果有,就暂时把它的target改为 _self. 后面对iframe写入伪协议结束后.再改回去.
ps: 务必迭代所有base标签,网上有资料说, 多个base标签 只有第一个生效是错误的. 实际上只要有target=_blank的base,无论他在哪,优先级都高于其他.
且,务必不要移除base标签,因为你可能遇到 IE6 base单闭合引发的bug.导致base后面的节点都被IE6解析成base的子节点.移除它,再恢复是很可怕的.
(2). 避免过早的获取document.contentWindow.document,导致拿不到iframe的document.
解决办法: iframe的url设置为about:blank后在iframe.onload中获取其document对象.
(3). 不恰当的写入方式,造成parent页的document.readyState永远卡在inneractive状态.进一步导致parent页面的onload事件,永远不会被触发.
解决办法: 不要对iframe.立刻写伪协议,而是在iframe.onload后,再次setTimeout 去写入伪协议. 注意这两个条件都是必要的.
ps: 记得onload注册要在 iframe的url 设置为'about:blank' 之前.
(4). IE8- iframe.onload,被认定跨域,注册无效问题.
解决办法: 请使用iframe.attachEvent('onload', callback)的方式.
(5). iframe.src = "about:blank" ,导致IE全系,后退按钮bug.(体现为,parent页后退按钮失效.可以无限按后退,并导致iframe内部的资源无限重载)
解决办法: 用iframe.contentWindow.location.replace('about:blank') 代替 iframe.src = "about:blank"方式. 前者,跳转的好处是,不会使iframe,产生记录.
ps: 此bug的本质原因就是iframe.src= xxx 产生历史记录.再写入伪协议.造成了这个恶心的问题.
另外, 请别担心location.replace会受同源策略限制. 如果你写过反iframe嵌套的脚本,就知道,这个api,完全不受限制.
需要说明的是,新创建的iframe,是没这个问题的.只有你打算复用某个已经有src属性的iframe时才要注意这个问题.
(6). 不恰当的使用伪协议,写入文档流,IE,出现,并永远卡在loading状态.(不停的转啊转...)
解决办法: 无需理会,因为我们的需求,先天上,帮我们修正了这个bug.即iframe内部产生一个网络请求(cache的不算).就会修正这个bug.
小结: 其实IE,或非IE,在使用伪协议的时候,bug远不止这么多,只不过我们的需求,先天上,绕过了他们而已.所以我就不再一一列出了.
(7). 写入文档流,接口不一致的隐患问题
本质上来说,这并不是一个bug. 而是一个隐患. 因为我们直接使用doc.write 和使用伪协议写入doc.write,有一个区别:
伪协议 : str = 'javascript:document.write("' + xxx + '")'; iframe.src = str;
直接写文档流: 则是iframe.contentWindow.document.write(xxx);
这就产生了一个接口的不一致性, 那么我们的脚本在大多数没有修改document.domain的页面,因为直接写文档流,没有权限限制,而被通过.则规避了xxx 内容中单引号或双引号的转意问题. 而用伪协议方式,因为我们要拼接一个外部包裹的字符串,就必须考虑内部的单双引号的转意.虽然技术上,我们可以借助单双引号的交错嵌套,避免转意序列\' 或\"的使用. 但是,我们也应该人为的保持接口的一致性.因为此部分功能,可能被你用在别的地方,而被写入文档流的数据,可能是不确定的. 那么保持一致性,可以做到要出错,两个分支都出错,要么就都通过. 避免了测试用例不全.带来的隐患.导致线上事故的发生. 这是我们一个血的教训.所以觉得,十分有必要补充进来的.
解决办法: iframe.contentWindow.document.write(eval('"' + str +'"'));
例子:
var str = '<script>alert(\\"\'franky\'\\")<\/script>';
document.write(eval('"' + str +'"'));//打印 'franky'
当然,就这一点上,可能我们还有另外一个选择,就是先用伪协议,写一段'<script>document.domain = 父页面.document.domain</script>进iframe的文档流. 然后,iframe 和parent就同域了.按道理就可以继续转回使用iframe.contentWindow.document.write了.(但我实测,还是会无法写入.所以我还是选择了伪协议方式) 不过,请务必要记得.不要这样:iframe.src = 'javacript:document.domain = xxx'; .这样做是会被ie抛出异常的.所以你要:
iframe.src = 'javascript:document.open();document.write("<script>document.domain = \'xxx\'<\/script>");undefined';
另外值得一提的就是, 后面将要介绍的dataURI方式.以及普通document.write处理<\/script> 时,不能要写成<\\\/script>(早期webkit 版本存在 bug. 新的则会自动修正html错误) .虽然你那样写,IE和其他非IE较新版本的浏览器会自动修正.但是在运行时,会有一些微妙差异.尤其是在你的更新脚本.可以执行的时候.此处不是重点.就不详细解释了. 保持接口一致性,需要费一番心思.(尤其是,复用iframe,以及这个接口,不仅仅用于资源更新,还有其他用途的时候.).我是自己写了些中间函数来处理这一系列恼人的问题...
还有一点就是webkit下 dataURI的iframe,会被视为跨域,而Firefox,Opera,则没问题.所以reload可以在外面做.而不必需要在iframe里面做.
2.关于非IE的方案:
我只能很遗憾的说, 方案1仅对IE有效,其他浏览器不可行. FF表现为,资源不被加载, Chrome,Safari,Opera,表现为,iframe,嵌套性的reload parent页面.导致无限嵌套的iframe出现.并不停的reload. 原因也无需解释.
但其实, DataURI 的方式可以解决这个问题,,比如 src="data:text/html;charset=utf-8," + encodeURIComponent的html内容.
最初,我在测试这个方式的时候,是失败的. 后来我发现aoao的blog,记录非IE使用DataURI方式. 在与aoao请教后,发现了我的悲剧. 我使用dataURI时, 内部加载资源,用了自适应协议方式,即. //www.a.com/a.js 的方式. 导致其被自适应为 data://www.a.com/a.js 而不是我们期望的http://www.a.com/a.js . 所以失败了... 所以在使用dataURI时,切忌写全协议头.
那么我们看看,哪些浏览器可以毫无顾忌的使用DataURI方式.
Firefox1.0+ (FF0.8版本,我没测.无视它吧)
Chrome10+
Safari5.1+
Opera11.6+
而其他低版本的非IE,存在的问题,并不直接和DataURI有关.
大多数情况下,他们和加载资源的方式有关系, 比如.js 使用link href的方式加载. .css使用 script方式加载等. 这样的本质问题是,浏览器会有一个会话级的缓存.它会记住第一次加载某资源的类型,如果它记住某个本来是.js的资源却使用link href方式加载,则再次加载该资源时,即使你使用script src 方式常规加载. 也会导致这个脚本不被执行. .只有重启浏览器,才能修正这个问题.
Chrome3-Chrome7 (Chrome1-2不存在这个类型差异的bug. )注意加载资源方式最好互相对应. (会话级缓存,该资源的类型,导致不渲染或不执行.)
Safari5.0- 注意加载资源方式,最好相互对应. (会话级缓存,该资源类型,导致reload,更新指定资源,不会为不同类型的资源提供缓存.即link方式加载.js. .js再加载仍然是老的)
后面是比较悲剧的情况了:
Chrome8 和Chrome9 ,我们无法使用script 方式加载而保证不执行. 只能使用link 或Object 或其他方式. 但是他们仍然存在不同类型,不执行或不渲染的问题.
我在纯技巧的角度上,始终没有想出解决办法, 那么我们唯一能做的是,假如 我要更新的资源,我确保它在我试图更新它之前,它已经被正确的方式加载过. 那么我就可以随便使用script 或link方式,去强制更新他们了. 因为浏览器已经正确的记住了他们的类型. 所以,我们需要评估使用环境,是否可以用. 又或者我们不考虑 比较低版本的问题浏览器,那么一切都不是问题了.
我们来总结一下:
我们可以在
IE全系
使用about:blank方式创建iframe,并使用恰当的方式(或者直接写文档流,或借助javascript伪协议)写入文档流,并配合<script type="text/c" src=xxx>来实现更新指定资源.
FF全系,Chrome10 +, Opera11.6+,Safari5.1+
使用dataURI方式创建iframe.并使用<link rel="stylesheet" media="c" href="xxx" />方式,更新指定资源.
Chrome1-2
使用dataURI方式创建iframe,并使用<script type="text/c" src=xxx>来实现更新指定资源
Chrome3-7, Safari5.0-
使用dataURI方式创建iframe,并使用根据资源类型,切换使用对应的link 或script方式来实现更新指定资源
Chrome8-9
使用dataURI方式创建iframe,并在确保资源在一次会话中,已经被正确方式加载过. 再使用<link rel="stylesheet" media="c" href="xxx" />方式,更新指定资源.
Opera11-
目前,reload方式无解. 我仍然没有想出应对的办法. 但是考虑到Opera11-,千分之一都不到的占有率.也许我们会欣慰很多. 更何况,我们可以使用ajax来搞.
最后的话
感谢你,花了这么长时间看到这里. 但是我仍然要说一下这个方案最终没有被采纳的原因. 还记得我一再说的 流氓缓存代理么. 是的.我们的很多ISP,都有流氓缓存代理存在.
他们会无视 Cache-Control : no-cache请求头域, 无视 Cache-Control: s-maxage=0 响应头域. (仅对缓存代理有效的一个响应头.),甚至无视Expires 和 max-age= xxx.自顾自的去缓存任何资源. 在我的测试中.部分ISP的缓存代理.甚至还会缓存 .php 的 动态资源(即no-cache的资源). 而有的缓存代理则有扩展名策略. 即,假设某资源是一个常见的 动态资源文件的扩展名.比如 .php .aspx 之类的东西.他们会和 .js .css等常见静态资源的缓存策略存在差异(即使,他们的响应头域完全一致). 遗憾的是.这样稍微有点良心的流氓ISP.最多只占一半..
我在实际测试中.遇到的最多的 无法更新的用户.大多数来自教育网. 不死心的我. 最终出了必杀技. 我们启用了在教育网内部的布置了双线接入 CDN节点. . 试图拦截部分来自教育网的请求到该节点上. 然后当该节点回源时.走另外的出口.而不走教育网统一的出口.以绕过教育网公共出口的流氓缓存代理. 但是效果也十分不理想. 一部分可能是因为 相当多的用户的local dns 解析到了错误的节点上. 一部分则可能是各个院校自己也存在类似的缓存代理.用于节省其流量成本.
还记得前阵子,方舟子 污蔑360浏览器窜号的问题么? 是的.就是污蔑. 因为窜号就是类似的流氓ISP的流氓缓存代理导致的. 导致其缓存了 setCookie: sessionID的响应.. 这种问题诬陷给浏览器.不得不说 Mr Know all 方舟子先生.实在是太能胡扯了.
好吧.似乎跑题了. 我要说的就是这些了. 我在这个方案上.大概花了两个月的时间. 一个月.写代码.并不断完善. 另一个月做实际测试. 最终得到的结果很让人伤心.. 特更新此文记录之.
最后补充一点数据:
类似流氓缓存代理.即更新失败的情况大约占8%左右.
Cache-Control: s-maxage=0响应头域的使用.是有一定效果的. 包括教育网内的CDN节点.也能起到一定作用. 我最高的时候.看到的某一天的数据.达到96%的更新成功率. 说以说.最理想的状态下.也有4%的用户会更新失败. 这是难以接受的情况.