前言
对于前端开发者来说,最头疼的事情莫过于。当你兴冲冲的给项目打包以后,上传到 nginx 等静态资源服务器发布以后,自己本地验证没问题,以为万事大吉,结果测试人员、用户反馈系统没更新,让你顿时感觉如同吃了苍蝇般难受。
没办法,作为一个专业的开发人员,用户反馈程序出问题,第一时间就要找到问题出现的原因。
于是你开始进行检查确认工作,先是打开浏览器访问系统发现没问题,确实是更新以后的版本。然后你开始对自己更新的过程产生了怀疑,上服务器,检查一下更新以后的资源文件,再三的详细比对,发现确实没问题后,你忽然恍然大悟,原来代码确实没问题,有问题的是浏览器缓存。
解决问题思路
前端打包后的文件,其实就是一些静态资源,我们一般情况下,是直接放在静态资源服务器提供服务的。
一般,解决发布资源缓存这个问题,有几种思路:
1. 浏览器设置每次都重新下载页面的所有的资源。
这个方案有个优点,每次拿到的资源都是重新拉去的最新的资源,所以不存在会缓存的问题。
但是这个方案的缺点非常明显,它直接让缓存失去了自身的意义。没有缓存,增加了服务器的负担,对服务器不太友好,同时也让用户使用网页的体验降低了。
而且,默认情况下,没有浏览器会这么设置。如果需要用这个方式,就需要引导用户这么设置,同时也对用户要求太高。
所以这个方案,不用考虑,直接就可以 pass 掉了。
2. 在静态资源服务器上配置,强制不让浏览器进行缓存。
常见的静态资源服务器,像 nginx、apache 等,确实可以配置让客户端永远不缓存静态资源。
虽然与第一个方案相比,它存在一些优点,比如不需要引导用户对浏览器进行配置。
但是弊病和第一种思路一样,对用户和服务器都极其不友好,所以这个方案也可以被 pass 掉了。
3. 最好的解决方案
其实有一种比较好的解决方案,需要前端和静态资源服务器配合起来达到。
前端资源其实分为几类:
- 内容会变化,但是文件名不会变,用户也只能通过某个路径访问到,最典型的就是页面入口的
index.html
文件 - 内容会变化,文件名也可以变化,比如每次打包生成的 js文件、css 文件、图片资源等
- 内容不会变化,文件名也不会变化,比如直接在 html 中引入的第三方包
针对这几种不同的解决方案,可以针对性的作优化。
第一类,可以采用 Etag 的缓存策略。
如果没了解过这个名词的童鞋,可以 mdn 上了解下相关知识:ETag - HTTP | MDN。
简而言之,如果采取了 Etag 策略,那么每次请求某个资源的时候,服务器会在响的的请求头里返回 Etag 标签:
下次,如果再请求同样的资源的时候,浏览器就会在请求头里带上这个 Etag,让服务器进行对比,如果 Etag 能够匹配上,就返回 304,浏览器就明白,不用重新下载资源,直接用本地缓存就行;否则浏览器会返回200,并返回完整的请求资源。
比如,想要在 nginx 中配置 etag,可以参考下面的链接:
直接这样配置即可,同时要在 header 中加上 cache control 控制,让浏览器对 html 文件应用 no-cache
规则。
location ~ .*\.(?:htm|html)$ {
...
add_header Cache-Control "no-cache";
etag on;
...
}
不了解 cache-control 的童鞋,可以参考下 mdn 的文档:
Cache-Control - HTTP | MDN
这样配置了以后,对于所有的 html 文件,就达到了当文件没更新的时候,返回 304,当文件更新了,自动拉取最新的资源的目的。
第二类,直接采用强制浏览器缓存的方法。
对于第二类资源,我们可以通过添加 hash 的方式保证,即使前端版本更新了,文件名也不一样,那么我们就不需要像第一种方式一样,通过 Etag 方式去缓存了。
因为即使是 Etag 策略,其实也是会消耗请求资源的。对于这种发布了就不会更改的内容,直接可以通知浏览器长期缓存即可。
想达到这种效果,直接通过配置 cache control header 即可。
location ~ .*\.(?:js|css|jpg|jpeg|gif|png)$ {
...
add_header Cache-Control "public, max-age=31536000";
add_header Last-Modified "";
etag off;
...
}
直接配置对于第二类资源,关闭 Etag,同时设置强制缓存,同时设置上一年的缓存期。
这样设置了以后,当浏览器再次请求该资源时候,会查看浏览器之前缓存的该资源是否还在有效期以内,如果在,便会直接应用该资源,不会向浏览器再次发起请求了;否则,会重新发起请求,请求该资源。
第三类资源的解决方案,其实跟第二类一样,通过添加 hash 值或者 timeStamp,用以区分资源,达到每次更新版本后,请求的资源不一致。
对于这两类资源的处理,在 nginx 上的配置,也并无差别,可以等同对待。
对于前端来说,就比较麻烦一些。
下面列举一些,在常用的前端框架的脚手架中,该如何配置,以达到自动添加 timeStamp 的效果吧。
- create-react-app
在 react 这个官方脚手架中,支持从环境变量传值: Adding Custom Environment Variables | Create React App
所以,我们直接在模板中,替换调传入的值即可。
/** .env 文件 */
REACT_APP_WEBSITE_NAME=测试标题
REACT_APP_TIMESTAMP=1639814656911
/** html 模板文件,通过传值,替换掉模板中的内容 */
<title>%REACT_APP_WEBSITE_NAME%</title>
<script src="%PUBLIC_URL%/config.js?timeStamp=%REACT_APP_TIMESTAMP%"></script>
- vue-cli
在 vue 脚手架中,可以通过重载 webpack 配置,来设置变量。
/**
* @type {import('@vue/cli-service').ProjectOptions}
*/
module.exports = {
chainWebpack: (config) => {
config
.plugin('html')
.tap((args) => {
args[0].timeStamp = Date.now();
return args;
});
},
};
然后通过在 index.html 模板中,采用 lodash template 的方式,应用变量。
<script src="<%= BASE_URL %>config.js?timeStamp=<%= htmlWebpackPlugin.options.timeStamp %>"></script>
详细用法可以参考官方文档:HTML and Static Assets | Vue CLI
后记
其实对于同一个问题,往往会有多种不同的解决方案。有时候多研究一下,多思考一下,综合考量,就可以找出一种比较好的解决方案。