前端资源缓存方案

5,787 阅读8分钟

讲前端资源缓存之前,我们先看两个场景:

场景一:

某业务沟通群
内部员工A:技术老师们,XX系统没法访问了
产品同学B:@技术同学C,麻烦技术同学帮忙看一下
技术同学C:@测试同学D,测试同学帮忙复现一下
测试同学D一阵忙碌...
测试同学D:@内部员工A,我这边没有复现呢,你这边能刷新一下页面试试吗?
内部员工A:试过了,还是不行
测试同学D:强刷呢?
内部员工A:强刷是什么?
测试同学D:就是Ctrl + F5
内部员工A:Ctrl + F5是什么?
测试同学D:……
半天后,内部员工A终于学会了强刷页面...
内部员工A:现在正常了,谢谢老师

场景二:

新需求上线,产品同学A生产验收中...发现页面空白无法访问...
沟通群里
产品同学A:@测试同学B 我这没办法访问呢?
测试同学B一顿操作后...
测试同学B:@产品同学A 我这没问题呀,你强刷一下试试呢
产品同学A很娴熟的强刷了一遍缓存...
产品同学A:@测试同学B,现在正常了
过了一会...
产品同学B:@测试同学B,我这边访问不了
测试同学B:@产品同学B 你强刷一下试试呢
产品同学B:强刷过了,也没用
测试挠头中...
前端挠头中...
a few moment later...
产品同学B:现在突然好了...
测试继续挠头中...
前端继续挠头中...

从上面两个场景,我们可以发现一个关键词【缓存】,发生故障了,可以通过清除缓存来解决问题。那是不是可以理解为是缓存导致了故障?既然如此能不能不要缓存?

那么缓存的意义到底是什么?

1、加快用户访问速度,提高用户留存率,进而促进转化率的提升
2、节省服务器带宽成本/cdn服务流量成本

既然缓存是非常必要的,那么为什么会出现上面两类场景的问题呢?是缓存的问题?还是我们对于缓存的使用不当?下面我们就来看看缓存到底应该怎么用。

一般情况下哪些地方存在缓存?

1、浏览器
2、cdn服务器
3、源站服务器(构建缓存)

一般会缓存哪些资源?

1、html文件
2、css文件
3、js文件
4、font文件
5、image文件
以上5种比较常见,实际情况肯定不止。

这些文件是如何被缓存到浏览器的

1、浏览器自发行为
2、浏览器根据http网络协议响应首部属性进行判断是否缓存

浏览器自发缓存行为比较少见,所以我们着重看一下基于http网络协议的缓存,下面是缓存的属性:
响应报文首部的缓存属性:

属性名优先级http版本说明
ExpiresDate1.0资源过期时间,依赖客户端时间,容易出现偏差
Cache-controlmax-age1.1缓存时长
——s-maxage1.1cdn缓存时长,优先级高于max-age与expires
——public1.1允许cdn缓存
——private1.1禁止cdn缓存
——no-cache1.1浏览器会缓存资源,但每次都会向服务器确认资源是否发生改变
——no-store1.1绝对禁止缓存资源
——must-revalidate1.1如果资源过期,则向服务器获取新资源
Pragmano-cache-1.0用来向后兼容只支持 HTTP/1.0 协议的缓存服务器,它的行为与 Cache-Control: no-cache 一致
Last-modifiedDate--资源上次修改时间
Etagstring--资源的标识,一般为md5或者hash值

http网络协议请求报文首部的缓存属性:

属性名优先级http版本说明
Cache-controlmax-age1.1缓存时长
———no-cache1.1浏览器会缓存资源,但每次都会向服务器确认资源是否发生改变
Pragmano-cache-1.0用来向后兼容只支持 HTTP/1.0 协议的缓存服务器,它的行为与 Cache-Control: no-cache 一致
If-Modified-SinceDate--客户端保留的资源上次的修改时间
If-None-Matchstring--客户端保留的资源标识

浏览器缓存的过程是怎样的呢?

1624344549523.jpg

1、浏览器强制刷新/禁用资源缓存是怎么做到的?

在请求头部配置cache-control: no-cache 或者 Pragma: no-cache

2、如何设置浏览器的强缓存?

在资源响应头部配置以下任一属性:
1)Expires: Mon, 10 Aug 2020 06:26:14 GMT
2)Cache-Control: max-age=604800

3、协商缓存怎么进行协商?

请求头携带以下属性:
1)If-Modified-Since: Tue, 21 Jul 2020 17:21:36 GMT
2)If-None-Match: W/"5f172420-cd9a2"

响应头返回以下属性:
1)Last-modified: Tue, 21 Jul 2020 17:21:36 GMT
2)Etag: W/"5f172420-cd9a2"

如果If-Modified-Since的时间等于Last-modified的时间,并且If-None-Match的值等于Etag的值,则说明服务器资源未发生变更,可以从本地读取缓存资源,反之则说明服务器资源发生变更,需要重新从服务器拉取新资源。

到这里我们大概了解了浏览器缓存是怎样一个过程,那我们再回过头来看一下场景1,我们猜测场景1可能是存在强缓存且强缓存未失效,但服务器资源和服务器接口已经更新,用户访问了旧资源,在旧资源请求了新的接口,导致故障出现。所以用户强刷缓存以后请求到了最新的服务器资源,该问题得以修复。但是我们不禁要问,这个问题到底应该怎么解决?我们明明已经发布了新资源,为什么浏览器没有请求新资源?

我们再来看一个更细的资源请求流程: image

如果我们请求头没有发生任何变更,在缓存期间无论我们发布多少次,用户都无法访问到新资源。有的同学可能会想到给请求加上版本号或者时间戳这种方案。我们再看看如果用版本号的方案是怎样的。见下图:
image

看完上图,我们大概知道了html是不是不应该被缓存?我们再来看看如果不缓存html的情况:

image

显然,只要不对html进行缓存,再配合版本号请求,是可以解决资源缓存无法更新的问题的,该方案也是很多传统项目所采用的方案。但是这个方案是最佳方案吗?显然不是,采用版本号方案有两个比较明显的问题:
1)每次发布需要手动调整版本号(有同学说,那我可以采用时间戳呀,但时间戳也会存在问题2)
2)每次发布都会导致全量缓存失效,意味着1000个前端静态资源,你只要改了其中1个,其余999个缓存全部失效,显然这不是我们想要的。

那我们还能怎么改进方案呢?这里我们不得不夸奖一下webpack的强大,因为借助webpack,我们可以更好的发挥缓存的作用。

webpack的三种hash值

1、hash

基于整个项目构建结果生成的hash值,只要项目内任何一处发生变化,hash值都会变化;

2、chunkhash

基于chunk构建结果生成的hash值,只有chunk内的内容发生变化,hash值才会变化;
例如:

// 修改前
// a.vue => a.fda123fd.js
<template>
    <div class='red'>hello world!</div>
</template>
// a.vue => a.fda123fd.css
<style>
    .red {
        color: red;
    }
</style>

// 修改后
// a.vue => a.f123klnk.js
<template>
    <div class='red'>hello world!</div>
</template>
// a.vue => a.f123klnk.css
<style>
    .red {
        color: #f00;
    }
</style>
3、contenthash

基于构建结果文件的内容生成的hash值,只要文件内容不变化,hash值不变;
例如:

// 修改前
// a.vue => a.fda123fd.js
<template>
    <div class='red'>hello world!</div>
</template>
// a.vue => a.45h6j7k8.css
<style>
    .red {
        color: red;
    }
</style>

// 修改后
// a.vue => a.fda123fd.js
<template>
    <div class='red'>hello world!</div>
</template>
// a.vue => a.3df4g56j.css
<style>
    .red {
        color: #f00;
    }
</style>

tips: 需要注意的是,如果style存在scope属性,即使使用了contenthash,只调整template或者script,没有调整样式内容,contenthash也会在每次编译后发生变化。

了解了以上三种hash值,显然contenthash最符合我们的需求,只有当编译文件的结果发生变化,才会生成新的文件,这样我们就能充分发挥缓存的作用了。调整后的方案:
image

相信看完上面的例子以后,大家对于场景1的情况大概心里有数了,那场景2呢?这又是怎么回事?下面就得讲到另一个缓存的地方,CDN服务器缓存。

CDN服务器缓存

为了让我们的网站资源更快的送达到用户身边,所以有了CDN服务器,相信大部分的面向用户的网站都会接入CDN服务器,如何保证CDN服务器资源的正确性就显得非常重要。

CDN示意图:
image

CDN如何缓存源站的资源

image

CDN是否会存在错误缓存

如果采取不当的发布方式,可能会导致CDN缓存错误。例如:先拷贝了html页面,后拷贝了js等资源,则会导致html请求的js不存在。所以最稳妥的方式是先拷贝js等资源,最后拷贝html页面。

如果CDN出现了错误缓存怎么办

可以采取刷新CDN缓存的方式更新缓存

CDN的回源策略有哪些?

1、主动回源:刷新CDN
2、被动回源:用户请求,缓存过期/新请求

讲到这里,场景2大家可能就知道大概是怎么回事了,为什么有的人访问正常,有的人访问不正常?可能是因为部分CDN节点存在缓存错误。