如何让浏览器不缓存文件
前言
最近在项目开发中遇到一个需求:项目打包后,可以根据修改配置文件,进而动态替换页面上的文本。由于项目基本不涉及到后端,因此不考虑通过接口来修改。这就需要前端项目打包后需要暴露一个配置文件,每次页面刷新时会获取到最新的配置,达到动态替换页面文本的目的。
本文重点总结下如何可以让浏览器不缓存静态资源,保证每次获取的都是最新的资源。
浏览器缓存
想知道如何不缓存文件,就需要先了解浏览器是怎么判断是否要缓存文件的。这里要引出一个概念,那就是浏览器缓存。
浏览器缓存(Brower Caching)是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档。
「浏览器缓存的优点有:」
浏览器缓存主要有两类:缓存协商和彻底缓存,也有称之为 「协商缓存」 和 「强缓存」 。
浏览器在第一次请求发生后,再次请求时:
- 浏览器会先获取该资源缓存的header信息,根据其中的 Expires 和 Cache-control 判断是否命中强缓存,若命中则直接从缓存中获取资源,包括缓存的header信息,本次请求不会与服务器进行通信;
- 如果没有命中强缓存,浏览器会发送请求到服务器,该请求会携带第一次请求返回的有关缓存的header字段信息( Last-Modified/IF-Modified-Since 、 Etag/IF-None-Match ),由服务器根据请求中的相关header信息来对比结果是否命中协商缓存,若命中,则服务器返回新的响应header信息更新缓存中的对应header信息,但是并不返回资源内容,它会告知浏览器可以直接从缓存获取;否则返回最新的资源内容。
强缓存
与强缓存相关的头部有两个,分别是 Cache-Control 与 Expires 。
这里重点介绍下 Cache-Control 。
「Cache-Control」
Cache-Control 是 http1.1 时出现的 header 信息,主要是利用该字段的 max-age 值来进行判断,它是一个相对时间,例如
- Cache-Control:max-age=3600 ,代表着资源的有效期是3600秒。
Cache-control 除了该字段外,还有下面几个比较常用的设置值:
- no-cache :不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
- no-store :直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
- public :可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
- private :只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
下次当遇到面试官问 no-cache 和 no-store 的区别时,应该知道怎么回答了吧。
注意, Cache-Control 与 Expires 可以在服务端配置同时启用,同时启用的时候 Cache-Control 优先级高。建议使用 Cache-Control 。
协商缓存
协商缓存有个特点,就是响应头和请求头是成双成对出现的。第一次请求资源时,浏览器会返回响应头;再次请求资源时,浏览器会添加相应的请求头。具体来说,是 「Last-Modify/If-Modify-Since」 和 「ETag/If-None-Match」 。
被浏览器缓存的文件会有不同的缓存来源,包括 from memory cache 和 from disk cache ,前者指缓存来自内存,后者指缓存来自硬盘。决定缓存到内存还是硬盘的正是 Etag 字段。如果响应头有 Etag 字段,那么浏览器就会将本次缓存写入硬盘中。
「Last-Modify/If-Modify-Since」
浏览器第一次请求一个资源的时候,服务器返回的header中会加上 Last-Modify , Last-modify 是一个时间标识该资源的最后修改时间,例如Last-Modify: Thu,31 Dec 2037 23:59:59 GMT。
当浏览器再次请求该资源时,request的请求头中会包含 If-Modify-Since ,该值为缓存之前返回的 Last-Modify 。服务器收到 If-Modify-Since 后,根据资源的最后修改时间判断是否命中缓存。
如果命中缓存,则返回304,并且不会返回资源内容,并且不会返回 Last-Modify 。
需要注意的是, If-Modified-Since 只可以用在 GET 或 HEAD 请求中。
「ETag/If-None-Match」
与 Last-Modify/If-Modify-Since 不同的是, Etag/If-None-Match 返回的是一个校验码。 ETag 可以保证每一个资源是唯一的,资源变化都会导致 ETag 变化。服务器根据浏览器上送的 If-None-Match 值来判断是否命中缓存。
ETag HTTP响应头是资源的特定版本的标识符。这可以让缓存更高效,并节省带宽,因为如果内容没有改变,Web服务器不需要发送完整的响应。而如果内容发生了变化,使用 ETag 有助于防止资源的同时更新相互覆盖。
与 Last-Modified 不一样的是,当服务器返回 304 Not Modified 的响应时,由于 ETag 重新生成过,response header 中还会把这个 ETag 返回,即使这个 ETag 跟之前的没有变化。
If-None-Match 是一个条件式请求首部。对于 GET 和 HEAD 请求方法来说,当且仅当服务器上没有任何资源的 ETag 属性值与这个首部中列出的相匹配的时候,服务器端才会返回所请求的资源,响应码为 200 。对于其他方法来说,当且仅当最终确认没有已存在的资源的 ETag 属性值与这个首部中所列出的相匹配的时候,才会对请求进行相应的处理。
当与 If-Modified-Since 一同使用的时候, If-None-Match 优先级更高(假如服务器支持的话)。
「强缓存与协商缓存的区别」
| 缓存类型 | 获取资源形式 | 状态码 | 发送请求到服务器 |
|---|---|---|---|
| 强缓存 | 从缓存取 | 200(from cache) | 否,直接从缓存取 |
| 协商缓存 | 从缓存取 | 304(Not Modified) | 否,通过服务器来告知缓存是否可用 |
「用户行为对缓存的影响」
| 用户操作 | Expires/Cache-Control | Last-Modied/Etag |
|---|---|---|
| 地址栏回车 | 有效 | 有效 |
| 页面链接跳转 | 有效 | 有效 |
| 新开窗口 | 有效 | 有效 |
| 前进回退 | 有效 | 有效 |
| F5刷新 | 无效 | 有效 |
| Ctrl+F5强制刷新 | 无效 | 无效 |
参考链接: https://zxpsuper.github.io/Demo/advanced_front_end/browser/cache.html
不缓存
no-store
上面介绍了一下浏览器缓存文件的方式,其中提到强制缓存的 Cache-control 的指令 no-store ,作用是不存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。
需要注意, Cache-Control 是通用消息头字段,既可以用于请求头,也可以用于响应头。
发送如下响应头可以关闭缓存:
Cache-Control: no-store
这里额外引用MDN里的几个示例,说明下其他场景该如何配置。
「缓存静态资源」
对于应用程序中不会改变的文件,你通常可以在发送响应头前添加积极缓存。这包括例如由应用程序提供的静态文件,例如图像,css文件和JavaScript文件。
Cache-Control:public, max-age=31536000
「需要重新验证」
指定 no-cache 或 max-age=0, must-revalidate 表示客户端可以缓存资源,每次使用缓存资源前都必须重新验证其有效性。这意味着每次都会发起 HTTP 请求,但当缓存内容仍有效时可以跳过 HTTP 响应体的下载。
Cache-Control: no-cache
Cache-Control: max-age=0, must-revalidate
「注意」: 如果服务器关闭或失去连接,下面的指令可能会造成使用缓存。
Cache-Control: max-age=0
增加版本号
这种方法不需要依赖服务端,纯前端便可实现。该方法流行于前端工程化诞生之前,弊端是需要手动增加版本号,人为干预较多。
<script type="text/javascript" src="../js/jquery.min.js?version=1.7.2" ></script>
使用随机数
既然在文件后面添加指纹可以让浏览器重新获取资源,那么我们可以在后面拼接随机数或者时间戳,这样也可以达到相同的目的,还省去了手动更改版本号的步骤。
具体来说,可以在 index.html 增加一段脚本,用来动态生成一个script标签,并引入静态资源,拼接时间戳。
var script = document.createElement("script");
script.src = "/resource/options/myjs.js?randomId=" + new Date().getTime();
document.body.appendChild(script);
这样浏览器每次刷新后,便会动态生成一个包含时间戳的静态资源。浏览器发现文件名有更改,会重新获取静态资源,达到了不缓存文件的目的。
使用HTML禁用缓存
HTML也可以禁用缓存, 即在页面的head标签中加入meta标签。例:
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"/>
说明:虽能禁用缓存,但只有部分浏览器支持,而且由于代理不解析HTML文档,故代理服务器也不支持这种方式。该方法不适用于特定文件不缓存的要求。
应用
掌握了以上缓存与不缓存的方式,接下来该进行实战了。
正在开发的项目使用的 Vite , Vite 使用 .env 文件来保存额外的环境变量:
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略
一份用于指定模式的文件(例如 .env.production )会比通用形式的优先级更高(例如 .env )。 .env 类文件会在 Vite 启动一开始时被加载,而改动会在重启服务器后生效。
那么可以考虑将需要动态替换的文本配置放入 .env 文件,并在打包的时候,将 .env 文件的配置暴露出去成为JS文件,这样就可以打包后进行修改JS文件,让配置实时生效。
但是配置文件会很庞大,不适合放在 .env 文件中,所以该方案放弃。
Vite 针对静态资源的处理,提供了 public 指定目录。可以将资源放在指定的 public 目录中,它应位于你的项目根目录。该目录中的资源在开发时能直接通过 / 根路径访问到,并且打包时会被完整复制到目标目录的根目录下。
请注意:
public public/icon.png /icon.png- public 中的资源不应该被 JavaScript 文件引用。
尝试使用 import 语法引入到JS文件中, Vite 会报错。提示你需要将资源使用 script 或者 link 的方式在 html 文件里引入。
public 目录可以看作是 webpack 下的 static 目录,会完整的将整个目录复制到最终的打包文件中。那么可以将配置文件放入到 public 目录下。打包后可以修改配置文件里的值,并且确保浏览器不会对该文件进行缓存后,刷新浏览器便可以得到最新的替换文本。这里我采用了使用随机数的方式来让浏览器不缓存文件。
具体做法是在 index.html 中写入:
<script>
(() => {
var script = document.createElement("script");
script.src = "/resource/options/myjs.js?randomId=" + new Date().getTime();
document.body.appendChild(script);
})();
</script>
然后 myjs.js 文件里将配置对象赋值给 window 全局对象自定义属性 __DynamicTextOptions__ ,尽量确保不会与现有属性冲突,并且不会被覆盖。
为了确保属性不被意外改写,这里还做了一些额外的处理。
window.__DynamicTextOptions__ = {}; // some options
Object.freeze(window.__DynamicTextOptions__);
Object.defineProperty('window', '__DynamicTextOptions__', { configurable: false, writable: false })
然后在业务代码中,直接获取 window.__DynamicTextOptions__ 上的对象内容即可。
至此,就实现了可以根据配置文件动态替换文本的需求。
总结
本文是由项目上遇到的一个小问题而诞生。探索了如何不需要重新打包,只修改打包后暴露的配置文件,进而替换页面上的文字。
总结了一下浏览器的强缓存和协商缓存。
- 与强缓存相关的头部包括 Cache-control 和 Expries 。
- 与协商缓存相关的头部包括 Last-Modify/If-Modify-Since 和 ETag/If-None-Match 。
也总结了如何让浏览器不缓存文件,方式包括:
- Cache-control: no-store
静态资源文件增加版本号
静态资源文件增加随机数
- 使用 meta 标签禁用缓存
最终使用了静态资源文件后面拼接时间戳的方式来达到不缓存文件的目的。
本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!