简介
在前端工程化中,前端代码打包之后的生成的静态资源就要发布到静态服务器上,这时候就要做对这些静态资源做一些运维配置,其中,gzip
和设置缓存是必不可少的。这两项是最直接影响到网站性能和用户体验的。
浏览器缓存主要有以下几个优点:
- 减少重复数据请求,避免通过网络再次加载资源,节省流量。
- 降低服务器的压力,提升网站性能。
- 加快客户端加载网页的速度, 提升用户体验。
浏览器缓存分为强缓存和协商缓存,两者有两个比较明显的区别:
- 强缓存在资源过期之前,不会向服务器发送网络请求,直接从本地缓存读取资源,响应码为
200 (from memory cache)
或者200 (from disk cache)
; - 协商缓存会向服务器发送一次请求,服务器会根据这个请求的
Request Headers
的一些参数来判断是否命中协商缓存,如果命中,则返回304
状态码并带上新的Response Headers
通知浏览器从缓存中读取资源;
强缓存
强缓存是根据返回头中的expires
(http 1.0 规范)或者Cache-Control
(http 1.1 规范)两个字段来控制的,都是表示资源的缓存有效时间。
expires
的值是一个GMT
格式的时间,这个时间代表资源的过期时间,在这个时间之前请求该资源,将直接命中强缓存。但是这个时间有个缺点,它是一个绝对时间,如果本地时间被修改,则将会导致缓存失效,所以expires
还是有些缺陷的。为了解决上述问题,http 1.1协议重新给了一个参数
Cache-Control
,这个值也是设置资源的过期时间,但是这个参数的值是一个相对时间,比如cache-control: max-age=3600
,浏览器会根据这个相对时间结合响应头的date
参数,得出资源的过期时间是date
的时间加上3600秒,需要注意的是,并不是每次请求都进行计算生效时间,只有当当前请求是200,或者304的时候,才会进行计算,否则每次计算,每次都加3600秒,那资源永远都不会过期了。Cache-Control
还有一些其他的值可以设置no-cache
:不使用强缓存,直接使用协商缓存。no-store
:直接禁止浏览器缓存数据,每次请求资源都会向服务器要完整的资源, 类似于network
中的disabled cache
。public
:可以被所有用户缓存,包括终端用户和 cdn 等中间件代理服务器。private
:只能被终端用户的浏览器缓存。
如果 Cache-Control
与 expires
同时存在的话, Cache-Control
的优先级高于 expires
协商缓存
协商缓存是由服务器来确定缓存资源是否可用。 主要涉及到两对属性字段,都是成对出现的,即第一次请求的响应头带上某个字, last-modified
或者 etag
,则后续请求则会带上对应的请求字段 if-modified-since
或者 if-none-match
,若响应头没有 last-modified
或者 etag
字段,则请求头也不会有对应的字段。
last-modified/if-modified-since
,值是GMT
格式的时间字符串,last-modified
标记最后文件修改时间, 下一次请求时,请求头中会带上的if-modified-since
值就是last-modified
,告诉服务器我本地缓存的文件最后修改的时间,在服务器上根据文件的最后修改时间判断资源是否有变化, 如果文件没有变更则返回304 Not Modified
,请求不会返回资源内容,浏览器直接使用本地缓存。如果有变化,则返回200
,返回最新的资源。etag/if-none-match
, 值是由服务器为每一个资源生成的唯一标识串,只要资源有变化就这个值就会改变。服务器根据文件本身算出一个唯一值并通过etag
字段返回给浏览器。当下次请求时,浏览器会将上次接收到的etag
值赋给if-none-match
字段,服务器通过比较两者是否一致来判定文件内容是否被改变。
etag
的优先级比last-modified
要高,先判断资源是否有更新
请求流程
浏览器在第一次请求后缓存资源,再次请求时,会进行下面两个步骤:
- 浏览器会获取该缓存资源的 header 中的信息,根据
Response Headers
中的expires
和cache-control
来判断是否命中强缓存,如果命中则直接从缓存中获取资源。 - 如果没有命中强缓存,浏览器就会发送请求到服务器,这次请求会带上
IF-Modified-Since
或者IF-None-Match
, 它们的值分别是第一次请求返回Last-Modified
或者Etag
,由服务器来对比这一对字段来判断是否命中。如果命中,则服务器返回 304 状态码,并且不会返回资源内容,浏览器会直接从缓存获取;否则服务器最终会返回资源的实际内容,并更新 header 中的相关缓存字段。
我们通过几个例子来实际看看会是什么效果,注意,设置缓存可以通过代理服务器设置,也可以直接使用代码设置,这里我们为了方便,直接使用nginx
进行设置,nginx
设置请求头需要ngx_http_headers_module
模块支持,通过yum
安装的nginx
默认不支持此模块,需要另行编译安装;
下面的例子,我们设置缓存事件都设置为30s,只是为了看效果,实际上不可能设置这么短的时间
设置expires
location ~* \.(js|css)$ {
expires 30s;
}
第一次请求直接返回200,可以看到虽然我们仅仅设置了expires
,但是响应头中也会带上cache-control
。expires
是一个绝对时间,该时间由date
的时间加上设置的30s
;
在过期时间内重复请求,都会命中强缓存,直接从浏览器本地读取资源
如果超过了过期时间,则不会命中强缓存,走协商缓存,返回304(因为资源没有发生变化),同时我们可以看到,date
和expires
都更新了,重新计算缓存过期时间
设置cache-control: max-age=30
location ~* \.(js|css)$ {
add_header Cache-Control max-age=30;
}
设置了cache-control
,此时可以看到响应头中已经不存在expires
了,其他的请求行为和设置expires
行为一样,这里不贴图了。
设置cache-control: no-cache
,不使用强缓存,使用协商缓存
location ~* \.(js|css)$ {
add_header Cache-Control no-cache;
}
第一次请求直接返回200,并设置cache-control
为no-cache
因为不使用强缓存,直接使用协商缓存,则之后每次都会返回304(因为资源没有发生变化)
注意每次请求都会讲上次响应头里的etag
值(通过if-none-match
请求头)和last-modified
(通过if-modified-since
请求头)发送给服务端
使用协商缓存,资源发生了改变(etag
会发生变化)
如果不使用强缓存,并且资源没有发生变化,则之后的每次请求都会返回304,但是如果我们的资源发生了变化,此时会将上次etag
值(通过if-none-match
请求头发送给服务器)和服务器上的最新资源etag
值进行比较,发现资源更新了,则返回响应码200,并返回最新的资源。
资源没有发生变化,多次请求返回304,etag
值为60ede052-5dd
修改config.js
文件,更新资源,此时etag
值会发生变化,此时etag
值为60ede48c-5eb
,响应码为200,请求了最新的资源。
使用协商缓存,资源没有改变,但是文件时间戳更新了(last-modified
会发生变化)
我们先将config.js
文件还原,避免混淆;
我们先看看当前文件的最后修改时间是多少,我们通过linux
的stat
命令查看,时间是2021-07-14 03:17:06.109141809 +0800
,注意是UTC
时间。
多次请求,返回304,注意此时last-modified
时间为Tue, 13 Jul 2021 19:17:06 GMT
,GMT
格式,加8个小时,正好是上面的UTC
时间
此时我们更新一下时间戳,通过linux
的touch
命令,更新config.js
文件的时间戳(修改时间也会被修改),此时修改时间为2021-07-14 03:25:30.121617196 +0800
再次请求资源,响应头中的last-modified
时间正好是我们更新时间戳后的时间,而上次的last-modified
时间通过请求头中的if-modified-since
传递给后端(注意GMT
和UTC
时间转换),发现两者时间不一致,响应码为200,请求了最新的资源。
etag
拓展
HTTP1.1 中etag
的出现主要是为了解决几个last-modified
比较难解决的问题:
- 一些文件也许会周期性的更改,但是内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新请求;
- 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),
if-modified-since
能检查到的粒度是秒级的,使用etag
就能够保证这种需求下客户端在1秒内能刷新 N 次 cache。 - 某些服务器不能精确的得到文件的最后修改时间。
etag
计算方式
etag
计算方式并没有明确规定,只要能标识文件唯一性就行了,但是对于大量的http请求,计算etag
不能有太高的消耗,否则对响应速度以及服务器的运算能力都会有影响
etag
默认格式是xxxxxxxx-xxx
,http默认计算etag
的方式是通过文件的last-modified
和content-length
值的十六进制数标识的,形如${last-modified}-${content-length}
对于以下config.js
文件
通过js计算该文件的etag
值
`${(new Date('Tue, 13 Jul 2021 19:25:30 GMT').getTime() / 1000).toString('16')}-${Number(1501).toString('16')}`
// 60ede8aa-5dd
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 289211569@qq.com