初窥 HTTP 缓存

引言

对于前端来说, 你肯定听说过 HTTP 缓存。 当然不管你知不知道它, 对于提高网站性能和用户体验, 它都扮演着重要的角色! 它通过在客户端和服务器之间存储和重用先前获取的资源副本, 来减少网络流量和降低资源加载时间, 从而提升用户体验! 以下是 HTTP 缓存的重要性:

  1. 减少加载时间: 通过使用缓存, 浏览器可以重用已下载的资源, 而不必每次都从服务器重新请求。这减少了页面加载时间, 提高了用户体验, 尤其是在较慢的网络连接下
  2. 降低服务器负载: HTTP 缓存可以减少服务器的负载, 因为不再需要为相同的资源处理大量重复请求。这有助于减少服务器资源的使用, 降低运营成本
  3. 减少网络流量: HTTP 缓存可以减少不必要的网络流量, 因为浏览器可以从本地缓存中加载资源, 而不必每次都从服务器下载。这有助于减少带宽成本, 尤其是对于大型网站来说
  4. 提高用户体验: 快速加载的网页提供更好的用户体验, 吸引更多的访问者, 并增加用户留存率。缓存使用户能够更快地访问网站内容, 从而提高了他们的满意度

实际开发中我们有时为了避免因为缓存导致资源没有更新, 常常会简单而粗暴的为资源文件路径添加一个时间戳! 该方式虽然有效, 也能解决实际问题, 但过于粗暴了, 我们完全可以采用更加优雅的方式。本文将对 HTTP 缓存进行一个详细的讲解, 来帮助大家对 HTTP 缓存有一个较深的理解, 帮忙大家在实际项目中设计出更为合理、高效的缓存方案!!

补充(偶然看到一句话, 送给大家): 所有的技术都是为了解决问题而存在的, 在不了解问题情况下强行记忆, 效果肯定是打折扣的!!

本文使用到的 DEMO 地址点 这里

一、强缓存

首先在上文我们已经强调了 HTTP 缓存的一个重要性, 那么要想设计一个缓存方案, 最简单的办法就是根据资源加载的时间:

  • 首次加载资源时将资源缓存起来
  • 然后在指定时间内如果加载同一份资源, 则客户端(浏览器)直接使用缓存数据, 而不向服务器发起任何请求
  • 当然如果超时则会发起新的请求获取资源, 然后再次缓存起来

image

而这种不发起任何请求(没经过服务端), 直接由客户端(浏览器)自行决定是否使用缓存的方案, 则被称为 强缓存!!

1.1 Expires

Expires 响应头, 表示当前获取到的资源的过期时间, 该时间是一个 GMT 时间, 即格林尼治时间!!

image

当客户端(浏览器)再次获取资源前:

  • 如果 存在未到期 的缓存资源, 则直接获取缓存的资源, 不发起任何请求!!
  • 如果 不存在缓存资源 或者 缓存的资源已过期, 则向服务器发起请求获取资源, 并再次缓存资源

image

特别注意的是, ExpiresHTTP/1.0 的产物了, 现在大部分浏览器均默认使用 HTTP/1.1 所以它的作用基本可以忽略!!

image

但这里其实有一个问题, Expires 时间是由服务端生成的, 这时如果客户端时间跟服务器时间不一致, 就会导致缓存命中的误差!!

所以后面就有了 Cache-Control

1.2 Cache-Control

Cache-Control 响应头, HTTP/1.1 出现的一个响应头, 它弥补了 Expires 的缺陷:

  • Expires 是直接由服务端给定一个资源过期时间
  • Cache-Control 则不同, 服务端只会告诉客户端该资源的有效期! 就比如, 它有一个 max-age 字段, 当它被设置为 10, 则表示当前资源有效期为 10 秒, 10 秒过后资源缓存将失效

image

Cache-Control 相对来说使用起来也更为复杂, 可设置的属性也比较多。当然复杂也说明它可适用于更广泛的复杂场景, 下面我们来看下 Cache-Control 响应头的语法规则:

  • 不区分大小写, 但建议使用小写
  • 多个指令以逗号分隔
  • 具有可选参数, 可以用令牌或者带引号的字符串语法

Cache-Control 可选属性有: 注意 no-cacheno-store 的区别, no-store 才是禁用 强缓存

类型属性描述
可缓存性public表明响应可以被任何对象(发送请求的客户端、代理服务器等等)缓存
可缓存性private私有缓存, 响应只能被单个客户端缓存
可缓存性no-cache无论本地缓存是否过期, 都需要请求源服务器进行验证(协商缓存验证)
可缓存性no-store不使用任何缓存
到期时间max-age=<seconds>设置缓存有效期的最大周期, 超过这个时间缓存被认为过期(单位秒)
到期时间s-maxage=<seconds>max-age, 但是仅适用于共享缓存(代理服务器), 生效时会覆盖 max-age 或者 Expires
到期时间max-stale[=<seconds>]表明客户端愿意接收一个已经过期的资源, 可以设置一个可选的秒数, 表示缓存允许过期的最大值
到期时间min-fresh=<seconds>希望在一个指定的秒数内保持资源的最新, 也就是说在设定的时间内获取资源不管缓存有没效, 都需要发起缓存进行验证(协商缓存验证)
到期时间stale-while-revalidate=<seconds>实验版, 有点类似 max-stale, 客户端愿意接收一个已经过期的资源, 同时客户端会在后台异步获取新的资源; 可以设置一个秒数, 表示允许接受陈旧缓存的最大值
到期时间stale-if-error=<seconds>实验版, 如果新的检查失败, 则客户端愿意接受陈旧的响应, 设置的秒数值表示客户端在初始到期后愿意接受陈旧响应的时间
重新验证或重新加载must-revalidate如果本地副本未过期, 可以使用本地副本; 否则, 需要请求源服务器进行验证
重新验证或重新加载proxy-revalidatemust-revalidate 作用相同, 但它仅适用于共享缓存(例如代理服务器), 对于私有缓存(本地缓存)该值会被忽略
重新验证或重新加载immutable实验版, 表示缓存一直有效, 再也不会发起请求了
其他no-transform不允许对资源进行转换或转变, Content-EncodingContent-RangeContent-TypeHTTP 头允许被代理服务修改
其他only-if-cached客户端只接受已缓存的响应, 不会进行任何请求, 如果缓存不命中则返回错误

下面我们来测试下, 如下代码是使用 Koa 搭建的一个简单服务:

  • 代码中实现了一个 GET 接口 /api/cache-control
  • 在接口 /api/cache-control 响应体中设置了 Cache-Control 响应头, 值为 public,max-age=120
  • 那么当客户端访问 /api/cache-control 理论上, 请求的响应内容会被任何端缓存起来, 缓存有效期 120s
const Koa = require('koa');
const moment = require('moment');
const cors = require('@koa/cors')
const Router = require('@koa/router');

const app = new Koa();
const router = new Router();

// 跨域设置
app.use(cors({
  maxAge: 5,
  origin: "*",
  credentials: true,
  allowMethods: ['GET', 'POST'],
  allowHeaders: ['Content-Type'],
  exposeHeaders: ['Content-Type'],
}));

// 2. Cache-Control
router.get('/api/cache-control', async (ctx) => {
  ctx.status = 200;
  ctx.body = 111111111;
  ctx.set('Cache-Control', `public,max-age=120`);
});

app.use(router.routes()).use(router.allowedMethods());
app.listen(3000);

1.3 Pragma

Pragma 是一个在 HTTP/1.0 中规定的响应头, 它只有一个属性值, 就是 no-cache, 其效果和 Cache-Control 中的 no-cache 是一致的, 表明不使用强缓存, 需要客户端(浏览器)与服务器验证缓存是否有效, Pragma 在本节的 3 个头部属性中的优先级是最高的

image

也许你会奇怪为啥有了 Cache-Control 还要整个 Pragma, 这里你需要注意的是 Cache-Control 其实是后面 HTTP/1.1 才出来的, 在 Cache-Control 之前 Pragma 是作为 Expires 的一个补充!!

当然 Pragma 目前主要就是用于向后兼容那些只支持 HTTP/1.0 协议的客户端!!

二、协商缓存

上文几个响应头, 客户端、服务端是通过约定缓存有效时间来决定, 是否 直接 使用本地缓存!! 但是这个方案其实存在很明显的问题:

  • 假设我们约定每次资源缓存的有效时间为 2 分钟
  • 但是呢? 2 分钟内, 如果服务端资源更新了, 按强缓存来, 这时客户端拿到的资源都是缓存内容, 就无法保证资源的最新
  • 同样, 如果服务端资源一直不更新, 那么 2 分钟后, 客户端如果发起请求这时将会重新获取资源, 这就不可避免的造成资源的浪费

那么要想解决上面这个问题, 就可以使用 协商缓存, 其实所谓 协商缓存 就是客户端(浏览器)在请求资源时先向服务端进行 协商: 客户端(浏览器)向服务端询问本地缓存的资源是否是最新的

  • 如果本地缓存的资源 未过期, 那么服务端就会将请求状态码设置为 304(Not Modified), 这时请求不会返回任何 body, 客户端(浏览器)将直接使用本地缓存
  • 如果本地缓存已过期, 那么服务端正常处理请求, 状态码设置为 200, 这时的 body 将是完整的资源内容

image

那么服务端如何判断客户端本地的缓存不是最新的呢? 它们之间又是怎么协调的呢?

2.1 Last-Modified / If-Modified-Since

第一个方案其实就是根据 资源最后修改时间 来进行判断:

  • 当客户端首次请求资源时, 服务端会把资源的最后修改时间放到 Last-Modified 响应头中(浏览器自动将 Last-Modified 也缓存起来)
  • 当客户端再次请求资源时, 浏览器会通过 If-Modified-Since 请求头, 将本地缓存资源对应最后修改时间带上(这个是浏览器自己的行为, 不需要我们认为去处理)
  • 服务端就可以根据 If-Modified-Since 来判断客户端(浏览器)的资源是否是最新的
  • 如果是客户缓存的资源是最新的, 则 body 直接返回空, 同时将状态码改为 304(Not Modified)
  • 否则按首次请求资源处理, 状态码为 200, body 为请求的资源内容

image

下面使用 Koa 搭建的一个简单服务:

  • 在接口中我们将客户端请求头中的 if-modified-since 和当前的 Last-Modified 进行比较
  • 如果相同, 则表示客户端的资源是最新的, 则不返回任何内容(body 为空), 让客户端直接使用缓存(状态码设置为 304)
  • 如果不相同, 则表示客户端的资源不是最新的, 则正常返回内容(状态码为 200, body 为当前资源内容), 当然这时还需要带上 Last-Modified
const Koa = require('koa');
const moment = require('moment');
const cors = require('@koa/cors')
const Router = require('@koa/router');

const app = new Koa();
const router = new Router();

// 跨域设置
app.use(cors({
  maxAge: 5,
  origin: "*",
  credentials: true,
  allowMethods: ['GET', 'POST'],
  allowHeaders: ['Content-Type'],
  exposeHeaders: ['Content-Type'],
}));

// 4. Last-Modified / If-Modified-Since
const lastModified = 'Thu, 09 Nov 2023 06:37:41 GMT'
router.get('/api/last-modified', async (ctx) => {
  // 客户端本地缓存最新更改时间和当前的一致 => 让客户端直接使用缓存吧
  if (ctx.request.header['if-modified-since'] === lastModified) {
    ctx.status = 304;
    ctx.body = '2222'
  } else {
    // 返回最新内容
    ctx.status = 200;
    ctx.body = '111111111'
  }

  ctx.set('Last-Modified', lastModified);
  ctx.set('Cache-Control', 'no-cache');
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000);

image

这里有个奇怪的现象, 第二次请求时, 服务器实际上设置的状态码为 304, body 内容为 2222, 但是在 chrome 中查看的话会发现展示出来的状态码为 200, body 为缓存的数据!!

image
image

猜测: chrome 针对本地存在缓存的 304 接口做了处理, 这里有一篇文章可供参考 《为什么Chrome开发工具显示200状态代码而不是 304? 》, 具体原因就不细究了…

2.2 ETag / If-None-Match

「根据最后修改时间, 来判定客户端缓存是否是最新的资源」这个依据在大部分情况应该是有效的!! 但是也避免不了一种情况就是, 资源更新时间变了, 但是呢内容和之前却是一样的(比如项目重新打包、资源修改一版又撤销回去…)

这种情况下, 我们就可以使用 ETag 来判断资源是否有效!!! ETag 作为资源内容的唯一标记被使用, 它可以是资源内容对应的一个唯一 ID, 也可以是根据资源文件内容生成的一个 MD5, 总之它代表的当前资源的一个唯一值!!

ETag 在服务端和客户端之前的工作流程和 Last-Modified 基本一致:

  • 当客户端首次请求资源时, 服务端会把当前资源对应唯一标记放到 ETag 响应头中(浏览器会将 ETag 也缓存起来)
  • 当客户端再次请求资源时, 浏览器会通过 If-None-Match 请求头, 将本地缓存资源对应的 ETag 带上(浏览器自己的行为)
  • 服务端就可以根据 If-None-Match 以及当前资源的 ETag 来判断客户端(浏览器)的资源是否是最新的
  • 如果客户缓存的资源是最新的, 则 body 直接返回空, 同时将状态码改为 304(Not Modified)
  • 否则按首次请求资源处理, 同时带上 ETag

image

下面使用 Koa 搭建的一个简单服务:

  • 在接口中我们将客户端请求头中的 if-none-match 和当前的 ETag 进行比较
  • 如果相同, 则表示客户端的资源是最新的, 则不返回任何内容(body 为空), 让客户端直接使用缓存(状态码设置为 304)
  • 如果不相同, 则表示客户端的资源不是最新的, 则正常返回内容(状态码为 200, body 为当前资源内容), 当然这时还需要带上 ETag
const Koa = require('koa');
const moment = require('moment');
const cors = require('@koa/cors')
const Router = require('@koa/router');

const app = new Koa();
const router = new Router();

// 跨域设置
app.use(cors({
  maxAge: 5,
  origin: "*",
  credentials: true,
  allowMethods: ['GET', 'POST'],
  allowHeaders: ['Content-Type'],
  exposeHeaders: ['Content-Type'],
}));

// 5. ETag / If-None-Match
const ETag = '33a64df551425fcc55e4d42a148795d9f25f89d4'
router.get('/api/etag', async (ctx) => {
  // 客户端本地缓存最新 ETag 和当前的一致 => 让客户端直接使用缓存吧
  if (ctx.request.header['if-none-match'] === ETag) {
    ctx.status = 304;
    ctx.body = '2222'
  } else {
    // 返回最新内容
    ctx.status = 200;
    ctx.body = '111111111'
  }

  ctx.set('ETag', ETag);
  ctx.set('Cache-Control', 'no-cache');
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000);

image

同上文, 如果本地缓存有效, 并且服务端状态码为 304, chrome 展示状态码也只会是 200, 内容为缓存的内容

2.3 补充: If-Unmodified-Since / If-Match

上文提到的几个消息头, 都只是在获取资源(get 请求)情况下生效!! If-Unmodified-SinceIf-Match 则是在修改内容的情况下生效, 比如 postputremove 等请求:

  • 当客户端(浏览器)发起请求试图修改资源时, 可以携带上 If-Unmodified-Since(本地资源对应 Last-Modified)If-Match(本地资源 ETag)
  • 服务端通过请求头中的 If-Unmodified-SinceIf-Match 来判断服务端资源是否已经被修改过(当前本地资源是否是最新的)
  • 如果服务端的资源已经被修改过, 那么服务端直接返回 412 (Precondition Failed) 错误, 不处理本次请求
  • 相反, 则返回 200! 正常处理请求

image

三、优先级

  1. 首先强缓存优先级大于协商缓存, 只有在强缓存失效情况下, 然后才会和服务端建立连接进行协商
  2. 强缓存中: Pragma > Cache-Control > Expires
  3. 协商缓存中: ETag > Last-Modified, 因为 ETag 更为精准

四、参考

  • 30分钟搞懂 HTTP 缓存
  • 前端应该知道 HTTP 缓存机制
  • 从未如此简单 5 分钟搞懂 HTTP 缓存机制
  • 彻底搞懂 HTTP 缓存策略,切记死背概念!
  • HTTP 缓存看这一篇就够了
  • 图解 HTTP 缓存
  • 轻松理解 HTTP 缓存策略
  • 面试官: 你懂 HTTP 缓存,那说下浏览器强制刷新是怎么实现的?

image

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/927673.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Ubuntu在NVME硬盘使用Systemback安装记录

问题 使用Systemback重装系统找不到NVME硬盘。 0.使用Systemback制作iso后&#xff0c;制作启动盘 1.插入启动盘进入live mode模式 2.安装gparted sudo apt-get update sudo apt-get install gparted3.使用gparted对待分区硬盘进行分区 gparted按照你希望的分区方式分区即…

机器学习8-决策树CART原理与GBDT原理

Gini 系数 和Gini 系数增益 CART决策树算法流程举例 该篇文章对于CART的算法举例讲解&#xff0c;一看就懂。 决策树(Decision Tree)—CART算法 同时也可以观看视频 分类树 GBDT原理举例 可以看如下示例可以理解GBDT的计算原理 用通俗易懂的方式讲解&#xff1a; GBDT算法及…

编译器优化技术

方法内联 逃逸分析 公共子表达式消除 数据边界检查消除

VSCode中“Run Code”运行程序时,终端出现中文乱码解决方法

问题描述 在VSCode中“Run Code”运行程序时&#xff0c;终端输出结果出现中文乱码现象&#xff1a; 解决方法 1. 检查系统cmd的默认编码 查看Windows终端当前编码方式的命令&#xff1a; chcp输出结果是一段数字代码&#xff0c;如936&#xff0c;这说明当前的cmd编码方式…

运维工作常用Shell脚本(Commonly Used Shell Scripts for Operation and Maintenance Work)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 本人主要分享计算机核心技…

数据分析流程中的Lambda架构,以及数据湖基于Hadoop、Spark的实现

文章目录 一、Lambda架构1、Lambda的三层架构2、简单解释&#xff1a;3、Lambda架构的优缺点 二、数据湖基于Hadoop、Spark的实现1、架构2、数据管理&#xff08;存储层的辅助功能&#xff09; 一、Lambda架构 1、Lambda的三层架构 Batch View&#xff08;批处理视图层&#…

ROS基本框架2——在ROS开发中创建并使用自定义消息(C++版本)

ROS基本框架2——在ROS开发中创建并使用自定义消息(C++版本) code review! 参考笔记 1.ROS基本框架1——编写简单的发布者和订阅者(C++和Python版本) 2.ROS基本框架2——在ROS开发中创建并使用自定义消息(C++版本) 文章目录 ROS基本框架2——在ROS开发中创建并使用自定义…

【Linux 篇】Docker 容器星河与镜像灯塔:Linux 系统下解锁应用部署奇幻征程

文章目录 【Linux 篇】Docker 容器星河与镜像灯塔&#xff1a;Linux 系统下解锁应用部署奇幻征程前言一 、docker上部署mysql1. 拉取mysql镜像2. 创建容器3. 远程登录mysql 二 、docker上部署nginx1. 拉取nginx镜像2. 在dockerTar目录下 上传nginx.tar rz命令3. 创建nginx容器4…

Matlab模块From Workspace使用数据类型说明

Matlab原文连接&#xff1a;Load Data Using the From Workspace Block 模型&#xff1a; 从信号来源的数据&#xff1a; timeseries 数据&#xff1a; sampleTime 0.01; numSteps 1001;time sampleTime*[0:(numSteps-1)]; time time;data sin(2*pi/3*time);simin time…

【计算机网络】实验7:默认路由和特定主机路由以及路由环路问题

实验 7&#xff1a;默认路由和特定主机路由以及路由环路问题 一、 实验目的 了解默认路由以及特定主机路由。 了解静态路由配置错误导致的路由环路问题。 二、 实验环境 • Cisco Packet Tracer 模拟器 三、 实验内容 1、默认路由以及特定主机路由 (1) 第一步&#xff…

kube-proxy的iptables工作模式分析

系列文章目录 iptables基础知识 文章目录 系列文章目录前言一、kube-proxy介绍1、kube-proxy三种工作模式2、iptables中k8s相关的链 二、kube-proxy的iptables模式剖析1.集群内部通过clusterIP访问到pod的流程1.1.流程分析 2.从外部访问内部service clusterIP后端pod的流程2.1…

学习ASP.NET Core的身份认证(基于Session的身份认证3)

开源博客项目Blog中提供了另一种访问控制方式&#xff0c;其基于自定义类及函数的特性类控制访问权限。本文学习并测试开源博客项目Blog的访问控制方式&#xff0c;测试程序中直接复用开源博客项目Blog中的相关类及接口定义&#xff0c;并在其上调整判断逻辑。   首先是接口A…

HTML前端开发-- Flex布局详解及实战

引言 Flex布局&#xff0c;全称为Flexible Box Layout&#xff0c;是一种现代CSS布局技术&#xff0c;它提供了一种更有效的方式来设计响应式布局和复杂页面布局。本文将详细介绍Flex布局的基本概念、属性以及实战应用。 一、基本概念 Flex布局的核心是Flex容器&#xff08;…

ESG研究报告白皮书与ESG治理报告合集(2020-2023年)

一.资料范围&#xff1a;&#xff08;1&#xff09;ESG白皮书及指南;&#xff08;2&#xff09;ESG研究报告,&#xff08;3&#xff09;ESG治理报告分析&#xff08;4&#xff09;上市公司ESG报告&#xff08;知名企业&#xff09; 二、资料用途&#xff1a;可以分析研究企业E…

C/C++每日一练:合并K个有序链表

本篇博客将探讨如何 “合并K个有序链表” 这一经典问题。本文将从题目要求、解题思路、过程解析和相关知识点逐步展开&#xff0c;同时提供详细注释的代码示例。 链表&#xff08;Linked List&#xff09; 链表是一种线性数据结构&#xff0c;由一系列节点&#xff08;Node&…

计算机网络复习1——导言和概论

网络简史 1946年&#xff0c;美国物理学家莫奇利任总设计师研制成功世界上第一台电子管计算机ENIAC&#xff08;这标志着人类自学会使用工具的漫长岁月中&#xff0c;终于拥有了可以替代人类脑力劳动的“工具”&#xff09; 1969年9月2日&#xff0c;以雷克雷洛克为首的约20名…

详解桥接模式

引言 在开发过程中&#xff0c;可能会遇到系统设计有多种维度变化的情况&#xff0c;比如我们想画一幅五彩斑斓的画&#xff0c;需要用到12个颜色&#xff0c;但是需要粗细不同的线条&#xff08;粗、中、细&#xff09;&#xff0c;如果用蜡笔&#xff0c;就需要粗中细三种蜡笔…

MySQL笔记-启动时log报错Table ‘mysql.user‘ doesn‘t exist

安装好mysql后&#xff0c;正常使用&#xff08;使用的是rpm版安装的&#xff09; service mysqld start | stop | restart 不会出现这个问题。 我遇到的情况是在凝思操作系统上&#xff0c;已经存在了一个mysql。网上查找了一些资料&#xff0c;卸载&#xff0c;后可能卸载…

【SpringBoot+Vue】x-admin管理系统跟做

技术栈 前端技术说明Vue前端框架Vuex全局状态管理框架ElementUI前端UI框架Axios前端HTTP框架vue-element-admin项目脚手架 后端技术说明SpringBoot容器MVC框架MyBatisORM框架MyBatis-plusMyBatis增强工具Redis非关系型数据库 数据库准备 SET NAMES utf8mb4; SET FOREIGN_KE…

AI智算-正式上架GPU资源监控概览 Grafana Dashboard

下载链接 https://grafana.com/grafana/dashboards/22424-ai-gpu-20241127/