浏览器缓存相关面试题一网打尽,理论结合实践,用代码学习缓存问题,建议关注+收藏,(含项目源代码)

发布时间:2024年01月08日

前言

浏览器缓存的问题是面试中关于浏览器知识的重要组成部分,也是性能优化题目的一部分,但是不要被吓到,我话放到这里,就那么点东西,我这一篇文章基本上就涵盖了所有相关的知识点,认真看一遍,所有的问题都是纸老虎。

一、准备工作

1.1 拉取仓库

本篇文章因为涉及到了在服务端设置缓存的内容,所以需要一个服务端的项目,可以跟着我的这篇文章搭建自己的服务端项目,或者直接克隆我的仓库代码。直接拉取最新的 master 分支的代码即可。

1.2 搭建服务器

1.2.1 新建 cache 文件夹

1.2.2 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    cache 
</body>
</html>

1.2.3 index.js

const express = require('express')
const path = require('path')

const app = express()

app.get('/', (req, res) => {
    res.sendFile(path.resolve(__dirname, 'index.html') )
})

app.listen(3000)

1.2.4 运行

npm run dev cache

为什么运行参数要加一个 cache?

这是因为我的项目设置的,加一个参数,可以运行其文件夹下面的文件,具体请看这篇文章

?1.2.5 提交代码

二、准备知识

我们前端说的缓存一般有两种类型,一个是浏览器相关的缓存,一个是 CDN 的缓存,CDN 的缓存一般我们控制不了,是服务商设置的【如阿里云】,我们最多可以使用服务商的管理平台开启、关闭或者清空缓存。此外后端也有很多缓存比如 redis 缓存,但是我们可以先不用考虑。

所以,我们前端面试题目中经常出现的缓存基本都和浏览器有关,浏览器的缓存又有很多种我们来逐一击破。

2.1 前端缓存方式

前端缓存有一下几种方式

  1. Service Workers 【可以缓存资源的请求,参考
  2. web Storage【LocalStorage/SessionStorage】
  3. IndexedDB【这个相当于浏览器中的数据库,请看官网
  4. Cache API【这个我没用过,可以看官网,是实验性技术】
  5. Http 缓存头【这个是本篇文章的重点】

本篇文章主要介绍使用 http 缓存头完成的缓存逻辑。

2.2?浏览器的多进程模型

在学习浏览器的缓存之前,我们需要先来学习一下浏览器的相关内容,内容不多,记住浏览器的主要的几个进程就可以。

  1. 浏览器主进程
  2. 网络进程
  3. 渲染进程
  4. 插件进程
  5. GPU 进程

具体的内容可以看一下我的这篇文章

一般来说和 http 缓存头相关的缓存内容是浏览器的【网络进程】控制的

2.3 缓存的目的

如果网络进程在发送请求之前,发现本地有有效的缓存的文件和要请求的 URL 地址所匹配,那么就不需要发起后续的请求了。所以缓存的目的是减少网络请求,减少服务器的压力,提高页面加载的速度,提高网络的性能。特别是对于一些静态资源,不经常改动,那就一次请求,然后缓存下来,直到这个资源有改动,我们再去服务端拉去新的资源。

三、http 请求头缓存

下面所说的所有关于缓存的内容都是针对前端 http 请求头缓存的。

3.1 缓存位置

常见的缓存位置有两个分别是 disk cache 和 memory cache

memory cache存在内存中,关闭浏览器标签页就释放,容量小,读取速度快
disk cache存在磁盘中,容量大,读取速度慢

什么资源存储在 disk 中,什么资源存储在memory 中是浏览器的规则?,一般来说肯定是大的资源比如字体啥的存在disk中,小的资源存取比较频繁的放在 memory 中,貌似没必要知道具体的详细的规则。

具体的缓存存在什么地方,我们在开发者工具中可以看到

3.2 刷新缓存

刷新页面的时候,不通的方式对缓存的处理对比

地址栏回车/直接访问 URL保留强缓存,保留协商缓存,走正常请求流程
点击浏览器刷新按钮忽略强缓存,保留协商缓存
按f5【command + r】忽略强缓存,保留协商缓存
ctrl + f5 【command + shift + r 】忽略强缓存,忽略协商缓存,从服务器端请求最新资源【强制刷新】

如果你改了页面提测了,测试同学说,不对啊,页面没有更新!

这个时候,那么你就应该考虑是不是有缓存,一般你说 【强制刷新】试下呢?基本就好了?

这个里面还有个问题,就是可以使用无痕模式/ 隐身模式,无痕模式在关闭浏览器窗口后会删除浏览会话的所有信息。

  1. 不存储历史记录
  2. 不存储表单数据
  3. 不存储 cookie
  4. 不存储缓存

注意!他这里的不存储是指在关闭无痕窗口之后不存储,不是你在无痕窗口打开的时候没有,比如说缓存吧,你在无痕窗口刷新页面该有缓存,还是会有缓存。

3.3 缓存类型

http 请求头的缓存就分为两种类型【强缓存】和【协商缓存】。先记住一点,强缓存是用浏览器本地的缓存,不需要服务器参与;协商缓存是需要和服务器协商的;但是本质两种缓存用的缓存文件还都是存在浏览器本地的,和服务器协商的目的是为了验证本地缓存是否有效。

而且这两种缓存的共同点是,都是用 http 请求头部控制,接下来我们分别详细介绍强缓存和协商缓存。

3.4?请求头部

缓存涉及的无论是响应头、请求头有且仅有下面几个

  1. expires 【响应头】
  2. cache-control【请求头】【响应头】
  3. Etag【响应头】? / if-none-match【请求头】
  4. last-modified【响应头】 / if-modified-since【请求头】

至于每个头部的取值和赋值的规则,下面的章节会有详细的介绍。

注意,http 头部是不区分大小写的。

四、强缓存

强缓存的涉及的请求头只有两个分别是?expires 和 cache-control。

4.1 expires 响应头

expires 只作为响应头出现在 http?请求的头部,expires? 响应头设置一个服务器的绝对过期时间【必须是GMT时间,且需要考虑时差】,在这个时间之前资源都不用重新请求。

这是一个服务器的绝对时间,所以一定是由设置在响应头上的。

4.1.1?代码模拟

(1)修改 index.html

(2) 修改 index.js

注意这里面有一个坑,就是 express 服务回自动带上 ETag 但是这个是属于协商缓存的内容,我们暂时还没有用到,所以需要先禁用!

const express = require('express');
const path = require('path');

const app = express();
// 先禁用协商缓存的头部
app.disable('etag')

app.get('/', (req, res) => {
  res.sendFile(path.resolve(__dirname, 'index.html'));
});

app.get('/info', (req, res) => {
  try {
    // 设置一分钟后过期
    const now = new Date();
    // 计算时差
    const now2 = now.setTime(
      now.getTime() - now.getTimezoneOffset() * 60 + 1 * 60
    );
    // 转换成 GMT 时间
    const expiresDate = new Date(now2).toUTCString()
    // 设置响应头
    res.setHeader('expires', expiresDate);
    res.send('ok');
  } catch (err) {
    console.log(err);
  }
});

app.listen(3000);

(3)运行

npm run dev cache

?我们的服务设置的 info 请求一分钟后过期

?

我们来看一下使用缓存的第二个请求的请求头是啥样的,注意这个时候返回的状态码依旧是 200,代表请求成功。

?至此关于强缓存的第一个 http 头部,响应头 expries 就介绍完了。

(4)提交代码

4.2 cache-control

和 expires 不同的是,cache-control 不仅可以服务端设置作为响应头,还可以客户端设置作为请求头,至于到底是客户端端设置,还是服务端设置,结论在这篇文章。? ? ? ? ? ? ? ? ??

我们可以看 mdn 官方文档 cache-control 有很多取值,请求头和响应头还不一样;下表中标红的是常见的,在这篇文章中我们也只研究标红的请求/响应头。

请求头响应头
1no-cacheno-cache使用协商缓存
2no-storeno-store不缓存
3max-agemax-age强缓存失效后,使用协商缓存
4no-transformmust-revalidate
5min-freshno-transform
6only-if-cachedpublic
7private
8proxy-revalidate
9s-maxage

常用的头部其实只有3个,分别是 no-cache、no-store、max-age

cache-control取值说明
no-cache在发布缓存副本之前,强制要求缓存把请求提交给原始服务器进行验证 (协商缓存验证)。
no-store缓存不应存储有关客户端请求或服务器响应的任何内容,即不使用任何缓存。
max-age=<seconds>设置缓存存储的最大周期,超过这个时间缓存被认为过期 (单位秒)。与Expires相反,时间是相对于请求的时间。max-age =0 实际上和 no-cache 的行为一致

这个怎么记呢,尤其是 no-cache 和 no-store 总是容易弄混?

记住这个单词?store? 有存储的意思,no-store,那就是不存储,无论如何都不存储,所以no-store 更狠一些,不存储任何东西,不使用任何缓存。

看看官网怎么说

The "no-store" request directive indicates that a cache MUST NOT store any part of either this request or any response to it. This directive applies to both private and shared caches. "MUST NOT store" in this context means that the cache MUST NOT intentionally store the information in non-volatile storage, and MUST make a best-effort attempt to remove the information from volatile storage as promptly as possible after forwarding it.“no-store”请求指令表明缓存不能存储此请求的任何部分或对其的任何响应。 这指令适用于私 有和共享缓存。
? “一定不在此上下文中“存储”意味着缓存不得故意将信息存储在非易失性存储器中,并且必须制作? ,一个尽最大努力尝试从易失性存储中删除信息 转发后尽快。

对比之下 no-cache 就温和一点了,不使用缓存,但是还有协商缓存这一个口子

4.3 cache-control 响应头

我们先来看响应头设置 cache-control 的情况,对于强缓存的响应头其实只有一个 max-age

且max-age的值不能为0,max-age=0 等价于 no-cache

4.3.1 修改 index.html?

?

4.3.2 no-store 响应头

?

no-store 响应头不做任何缓存,无论发送多少请求都是重新请求

?

4.3.3?no-cache 响应头

?

4.3.4?max-age

max-age 的单位是秒

(1)max-age=0

?

(2)max-age 不等于0

这个才是真正的强缓存,和之前说的 expires 头部一样,真正的强缓存。

设置 max-age=10? 10秒之后过期。

?

?

4.4 cache-control 请求头

4.4.1 no-store

我们在之前的代码基础上,不修改 index.js 保留响应头 max-age=10 的设置

我们发现了,即便是请求头设置了不使用缓存,浏览器依旧使用了响应头的强缓存,这个就很神奇了。

4.4.2 no-cache

把请求头设置为 no-cache

4.4.3 max-age=0?

?4.4.4 max-age 不等于0

客户端和服务端同时设置了max-age,这里面以服务端设置的响应头为准了。

我们接下来吧响应头的 max-age去掉,仅保留请求头的,发现强缓存并未生效。

所以我得出了一个结论,就是在客户端设置 max-age 请求头值为非 0 的强况下,强缓存不会生效。

至少我测试的 express + chrome 浏览器是这样的,不通的浏览器可能对于缓存有不同的规则。

4.5 请求头 vs 响应头

通过上面的例子,我么你注意到 cache-control 的同一个值可以即由客户端设置在的请求头,又可以由服务度设置在响应头。但是你要问了,到底设置在哪边呢?

我的建议是不要纠结,同时设置到底以那个为准了,不通浏览器貌似有不同的处理。

对于客户端和服务端同时设置 cache-control 不通的值的时候还有很多组合,但是感觉没有逐个测试的意义,我们只要只要每个值的含义就行了,在实际应用过程中,如果客户端设置的不满足需求,我们就使用服务端设置就行了。

如果你还有其他的见解,也可以在评论区发表观点,我们一起讨论下。

4.6 expires 和?cache-control

强缓存涉及到两个 http 头部,分别是 【expires】和【cache-control】其中【cache-control】的优先级高于【expires】,这是因为 【expires】是在http1.0 提出来的,【cache-control】是在http1.1 提出的。二者同时存在的时候以【cache-control】为准。

4.6.1 修改 index.html

4.6.2 修改?index.js

4.7 刷新强缓存

(1)使用浏览器的强制刷新功能就可以强制刷新缓存,比如 按下ctrl + f5【mac 电脑是 command+shift +R】,浏览器会忽略所有缓存,也不执行协商缓存。

(2)使用开发者工具禁用缓存,注意这个只在开发者工具打开的时候有效!关掉开发者工具,刷新缓存就只能用强制刷新。开发者工具的这个禁用缓存浏览器会忽略所有缓存,也不执行协商缓存。

小结

至此,强缓存相关的知识已经总结完毕,结论是设置响应头为 max-age 且值不为0?或者expires 时会使用强缓存。

五、协商缓存

如果命中强缓存,那就直接使用缓存内容即可,不需要发起后续请求,但是上面我们提到了,如果cache-control 设置了 no-cache / 或者 max-age=0 / 或者 max-age =xxx 过期了,就需要进行协商缓存。

5.1 协商缓存条件

协商缓存的条件有

  1. cache-control: no-cache 或者
  2. cache-control: max-age=xxx (单位是秒) 过期了/或者是 =0

协商缓存涉及到的请求头有 2 种方式,这两组一般是选其中一种,但是也可以同时设置,这两组请求头的第一个字段是需要后端在服务器代码中加上的(last-modified或者etag);第二个字段是浏览器根据上一个请求的响应头的 last-modified 或者 etag,自动设置的,不需要人工参与。

我们前端只需要在请求头上设置 cache-control 的值为 no-cache,或者设置 max-age=0,也要服务端配合设置响应头,具体的规则参考这篇文章

注意 no-cache 和 max-age 是两个c ache-control 字段的取值,他俩可以同时使用(使用逗号分隔),也可以只使用其中的一个。

(1)仅设置了?no-cache 那么每次使用缓存之前,都需要去服务器确认资源;

(2)仅设置了 max-age 在未过期之前不需要向服务器确认资源的有效性;

(3)no-cache 和 max-age 同时设置的时候 no-cache 生效,每次都需要去服务器进行资源验证

如果你是服务端开发者,你就需要从下面 2 对协商缓存头中选一对。但是一般如果是使用的服务端的框架,对于缓存框架有默认的内部的处理,不需要手动设置,比如 express 就会默认携带etag,详情请看4.1.1,我们手动禁用了express 中的Etag

5.2 协商缓存头部

5.2.1?Last-Modified / If-Modified-Since【优先级低】

当浏览器首次请求一个资源时,服务器会返回资源的最后修改时间(服务端在响应头设置?Last-Modified)。当浏览器再次请求该资源时,会在请求头中自动包含?If-Modified-Since?字段,该字段的值是上次服务器返回的最后修改时间。如果资源在这个时间之后没有发生变化,服务器可能会返回状态码 304(Not Modified),告诉浏览器可以使用本地缓存。

这里面说的【服务器返回】,就不是浏览器的功能了,是后端开发人员手动写的,比如我们手动写的 express 服务 需要手动设置,last-modifed 的取值需要时 GMT 时间。

?res.setHeader('Last-Modified', xxx)

然后还需要获取请求头部的?If-Modified-Since 并且和 服务端保留的上一个请求的 last-mofified 字段进行对比,如果没有修改返回 304。大概的代码如下,这个例子使用的是express

let lastModifed = ''
app.get('/auth', function (req, res, next) {
  res.setHeader('cache-control', 'max-age=20')
  // 获取浏览器自动 加上的请求头
  const ifModifiySince = req.headers['if-modified-since']
  // 判断内容是否被修改
  // lastModifed 注意这个字段是上一个请求的
  if (ifModifiySince === lastModifed) {
    res.status(304) // 给浏览器返回304 状态码
    res.end('end')
    return
  }
  lastModifed = new Date().getTime()
  // 服务端设置 Last-Modified,这是给下一个请求设置的
  res.setHeader('Last-Modified', lastModifed)
  res.end('ok');
});

如果是我们自己实现的服务端,那么就需要有这块的逻辑,但是如果用的后端的框架,一般框架都会带有这些逻辑,就不用自己写了。

5.1.2 ETag / If-None-Match 【优先级高】

类似于?Last-Modified,服务器在响应头返回资源的唯一标识符(这个标识符由服务端自己定义可,一般是基于资源内容的哈希值),称为 ETag。浏览器在后续请求中,会在请求头中自动包含?If-None-Match?字段,该字段的值是上次服务器返回的ETag。如果资源的ETag匹配,服务器同样可以返回状态码 304,告诉浏览器可以使用本地缓存。

如果?Last-Modified 和 ETag 同时存在,那么优先判断 ETag / if-none-match,因为ETag 是http1.1 提出来的。

ETag 优先的意思是,判断完是否有 ETag ,如果有就不判断 last-modified了,如果没有 Etag 再判断 last-modifed?

5.3 代码模拟

首先我们把所有的强缓存的头部去掉,客户端和服务端都不设置任何强缓存头部。

5.3.1?Last-Modified / If-Modified-Since

Last-Modified 的值需要时 GMT 时间,这一点和 expires 是一样的,但是其实我们可以设置任意值,因为对比?Last-Modified ?和?If-Modified-Since 的逻辑是我们的服务端自己写的的。如果我们设置的值是非时间格式的,那么其实就和 ETag 一样了。

(1)修改 index.html

(2)?修改 index.js

(3)运行结果

字段给带上了,但是还没有使用缓存,因为 304 逻辑需要服务端自己实现。

(3)代码实现使用缓存

304 这个状态码记住,就是未修改的意思。

5.3.2 ETag / if-none-match

如果上面的 last-modified 不设置为时间,那么就是和 ETag 的逻辑一致。

(1)修改代码

(2)运行结果

?5.4 小结

注意,我们整个协商缓存的代码都没有设置 cache-control, 也就是说可以理解为协商缓存的头部不是依赖于 cache-control 的,但是cache-control 如果设置了 no-cache/max-age=0/max-age 过期,接下来就会判断是否有协商缓存的头部。

5.4.1 强缓存+协商缓存

我们以 max-age 为例试一下。

总结

前端缓存,使用http请求头控制的缓存这块,如果用代码自己实现之后发现,就那么点东西。

内容比较多难免疏漏,有问题欢迎指正。

我的仓库地址是,yangjihong2113/learn-express

这是一系列文章,我会在我的专栏《面试题一网打尽》中持续更新,欢迎关注!

文章来源:https://blog.csdn.net/qq_17335549/article/details/135413879
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。