详解前端缓存,解决前端换包之后环境中仍会出现旧版效果

  前端项目修改了很多东西:比如bug啊,样式啊。当你把前端项目打包之后满心欢喜的在 Nginx(测试环境)换上它,然后在 Jira 上修改bug状态@测试人员复测。然后测试人员开始找你battle了,你的bug怎么还是没修改啊,但是你明明换上了最新的版本,中间到底出现了什么问题。打开控制台的 network,显示如图所示。

  问题就出在 from disk cache 这玩意上,也就是浏览器缓存,浏览器读取的还是缓存中旧版的资源,渲染出来的还是旧版的效果。除了 disk cache 外,还有其他几类浏览器缓存,总的来说,浏览器缓存大致分为4种,而这4种方式是有优先级顺序的,只有依次查找缓存且都没有命中的时候,才会去请求网络:

    1. Service Worker:是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,可以帮我们实现离线缓存、消息推送和网络代理等功能。

    2. Memory Cache:存在内存中的缓存。包括当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。因为存储在内存中,MemoryCache 是响应速度最快的一种缓存,但由于同样的原因,缓存持续性很短,会随着进程的释放而释放,一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。

    3. Disk Cache:Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。

    4. Push Cache:Push Cache 是 HTTP2 在 server push 阶段存在的缓存,当以上三种缓存都没有命中时,它才会被使用,Push Cache 是一种存在于会话阶段的缓存,当 session 终止时,缓存也随之释放。不同的页面只要共享了同一个 HTTP2 连接,那么它们就可以共享同一个 Push Cache。

  其实常见的情况下只有 disk cache 和 memory cache,如下图博客园首页请求所示:

  至于什么情况下是存在内存,还是存在硬盘。由于计算机内存有限,而且比硬盘容量小很多,浏览器会根据计算机具体情况来决定缓存放在内存中还是硬盘中。一般情况下,较大的 css 文件、js 文件和 jpg 图片这类大文件会被存入硬盘;当前系统内存使用率高的话,文件也会被优先存储进硬盘;而 Base64 格式的图片,几乎永远可以被塞进内存。

  那为什么需要缓存呢,对前端来说,因为读缓存不需要发起请求,也就不需要考虑请求环境和速度,提高访问速度,用户体验大大提升;对于后端而言,也缓解了服务器的压力,减少网络 IO 消耗,减少带宽消耗。

  但是什么时候需要缓存,什么时候不需要缓存。很明显,我这种换版操作肯定是需要重新获取新的资源的。最简单的解决办法就是 Ctrl+F5 强制刷新,强制告诉浏览器不获取缓存,必须重新去获取新的资源,但是强制刷新这种手动触发还是对用户体验不太友好。特别是我们做的后台管理系统,在图片很少的情况下,有没有办法每次换版之后都获取最新的资源。这个时候就要涉及浏览器的缓存策略了,常见的缓存策略有强缓存和协商缓存。其实浏览器的缓存策略都是通过设置 HTTP Header 来实现的。

强缓存

  不会向服务器发送请求,直接从缓存中读取资源。状态码:200,显示 from disk cache 或 from memory cache。通过设置两种 HTTP Header 实现:Expires和Cache-Control。

  1.Expires:值是一个时间戳,表示本地时间到这个设置的时间缓存就失效。这样一来 Expires 就是有问题的,受限于本地时间,我直接手动去把电脑端的时间改掉,都能导致缓存失效,所以更推荐使用 Cache-Control,或者二者搭配使用。

    在 Nginx中配置写法如下:

# gif、jpg、jpeg、png、bmp、ico这类的资源会在30天后失效
location ~ \.(gif|jpg|jpeg|png|bmp|ico)$ {
    root /XXXX/xxxx;
    expires 30d;
}

  2.Cache-Control:优先级比 Expires 高,同时设置 Expires 和 Cache-Control 则后者生效。可以在请求头或者响应头中设置,并且可以组合使用多种指令:

    1. private(默认):只能在浏览器中缓存, 只有在第一次请求的时候才访问服务器,若有 max-age,则缓存期间不访问服务器
    2. public:可以被任何缓存区缓存,如:浏览器、服务器、代理服务器等
    3. no-cache:可以缓存,但每次都应该去服务器验证缓存是否可用,进入协商缓存阶段,若有 max-age,则缓存期间不访问服务器,
    4. no-store:不仅不能缓存,连暂存也不可以(即:临时文件夹中不能暂存该资源)
    5. max-age=<seconds>:以秒为单位的缓存时间,max-age=60,表示缓存60秒后失效,60秒内再次访问该资源,均使用本地的缓存,不再向服务器发起请求
    6. s-maxage=<seconds>:同 max-age 作用一样,只在代理服务器中生效(比如CDN缓存),s-maxage 优先级高于 max-age,只对 public 缓存有效。设置了 s-maxage,没设置 public,代理服务器也可以缓存这个资源
    7. must-revalidate:可缓存但必须再向源服务器进确认
    8. proxy-revalidate:要求中间缓存服务器对缓存的响应有效性再进行确认

    在 Nginx 中配置写法如下,随便举一个指令:

location ~ .*\.(css|js|swf|php|htm|html )$ {
    add_header Cache-Control no-store;
}

协商缓存

  当 Cache-Control 和 Expires 过期或者它的属性设置为 no-cache 时(即不走强缓存),那么浏览器第二次请求时就会与服务器进行协商,服务器端会对比判断资源是否进行了修改更新,对比结果无非是以下两种:

    • 如果服务器端的资源没有修改(Not Modified),那么就会返回304状态码,告诉浏览器可以使用缓存中的数据。
    • 如果数据有更新就会返回200状态码,服务器就会返回更新后的资源并且将缓存信息一起返回。

  至于浏览器是怎么和服务器交互,主要是依靠跟协商缓存相关的header头属性:Last-Modified/If-Modified-Since、ETag/If-None-Match。这些属性在请求头和响应头是成对出现的。

  1.Last-Modified/If-Modified-Since:

    浏览器在第一次访问资源,或缓存过期后访问,服务器返回资源的同时,在 response header 中添加 Last-Modified 的 header,值是这个资源在服务器上的最后修改时间,浏览器接收缓存文件和header信息。随后我们每次请求时,浏览器会自动带上一个叫If-Modified-Since 的时间戳字段给服务器,它的值正是上一次 response 返回给它的 Last-modified 值,然后服务器会根据 If-Modified-Since 的值对比资源的最后修改时间判断资源是否进行了修改更新。

  2.ETag/If-None-Match :

    Etag是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的,因此 Etag 能够精准地感知文件的变化。Etag 和 Last-Modified 类似,当首次请求时,我们会在响应头里获取到一个最初的标识符字符串。那么下一次请求时,浏览器就会在请求头里就会带上一个值相同的、名为 if-None-Match 的字符串供服务器比对。Etag 的优先级会比 Last-Modified 高,但是Etag因为要生成,也会更消耗服务器性能。

   查看 Nginx 更新日志可知,在2014年6月26日就默认开启 Etag,对应的版本为1.7.3,也就是说1.7.3及以上的版本的 Nginx 默认开启 Etag。不过需要注意的是,如果 Nginx 有开启 Gzip,可能会与 Etag 有冲突。

  然后就是各家的 Etag 生成情况都不太一样,取决于服务器的类型或配置的算法。以下是简书首页随便的一个请求,这个不是什么大问题,顺便提一嘴。

  说了这么多,前端缓存最理想的效果就是希望能尽可能多的命中强缓存,对于频繁变动的资源,多使用协商缓存,同时,能在更新版本的时候让浏览器的缓存失效。这就要求了我们对资源进行 Nginx 配置的时候,对资源失效时间有个自己的衡量和把握。

  最后,还有一种情况是浏览器在几次刷新过程中会出现新版效果,也会出现旧版效果,新旧交替。那就得考虑前端项目是否布置了多节点,并使用 Nginx 配置负载均衡了,如果是这个问题的话,只要全部 Nginx 节点环境都换上新打的前端包问题就迎刃而解了。

posted @ 2023-01-01 18:48  他好像一条狗啊  阅读(2477)  评论(0编辑  收藏  举报