当前位置: 首页 > article >正文

前端缓存详解

HTTP 缓存分为 2 种,一种是强缓存,另一种是协商缓存。主要作用是可以加快资源获取速度,提升用户体验,减少网络传输,缓解服务端的压力。这是缓存运作的一个整体流程图:

强缓存

不需要发送请求到服务端,直接读取浏览器本地缓存,在 Chrome 的 Network 中显示的 HTTP 状态码是 200 ,在 Chrome 中,强缓存又分为 Disk Cache (存放在硬盘中)和 Memory Cache (存放在内存中),存放的位置是由浏览器控制的。是否强缓存由 ExpiresCache-ControlPragma 3 个 Header 属性共同来控制。

○ Expires

Expires 的值是一个 HTTP 日期,在浏览器发起请求时,会根据系统时间和 Expires 的值进行比较,如果系统时间超过了 Expires 的值,缓存失效。由于和系统时间进行比较,所以当系统时间和服务器时间不一致的时候,会有缓存有效期不准的问题。Expires 的优先级在三个 Header 属性中是最低的。

○ Cache-Control

Cache-Control 是 HTTP/1.1 中新增的属性,在请求头和响应头中都可以使用,常用的属性值如有:

  • max-age:单位是秒,缓存时间计算的方式是距离发起的时间的秒数,超过间隔的秒数缓存失效
  • no-cache:不使用强缓存,需要与服务器验证缓存是否新鲜
  • no-store:禁止使用缓存(包括协商缓存),每次都向服务器请求最新的资源
  • private:专用于个人的缓存,中间代理、CDN 等不能缓存此响应
  • public:响应可以被中间代理、CDN 等缓存
  • must-revalidate:在缓存过期前可以使用,过期后必须向服务器验证

○ Pragma

Pragma 只有一个属性值,就是 no-cache ,效果和 Cache-Control 中的 no-cache 一致,不使用强缓存,需要与服务器验证缓存是否新鲜,在 3 个头部属性中的优先级最高。

const express = require('express');
const app = express();
var options = { etag: false, // 禁用协商缓存lastModified: false, // 禁用协商缓存setHeaders: (res, path, stat) => {res.set('Cache-Control', 'max-age=10'); // 强缓存超时时间为10秒},
};
app.use(express.static((__dirname + '/public'), options));
app.listen(3000);

第一次访问

第二次访问


10s 之后再次请求

 

如果设置 Pragma: no-cache 将不使用强缓存,当 Pragma 和 Cache-Control 同时存在的时候,Pragma 的优先级高于 Cache-Control。

res.set({'Cache-Control': 'max-age=10','Pragma': 'no-cache'
});// 或res.set({'Cache-Control': 'max-age=00', // 浏览器不走强缓存'Pragma': 'no-cache', // 浏览器不走强缓存
});

 

协商缓存

当浏览器的强缓存失效的时候或者请求头中设置了不走强缓存,并且在请求头中设置了If-Modified-Since 或者 If-None-Match 的时候,会将这两个属性值到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified 或者 ETag 属性。

○ ETag/If-None-Match

ETag/If-None-Match 的值是一串 hash 码,代表的是一个资源的标识符,当服务端的文件变化的时候,它的 hash码会随之改变,通过请求头中的 If-None-Match 和当前文件的 hash 值进行比较,如果相等则表示命中协商缓存。ETag 又有强弱校验之分,如果 hash 码是以 "W/" 开头的一串字符串,说明此时协商缓存的校验是弱校验的,只有服务器上的文件差异(根据 ETag 计算方式来决定)达到能够触发 hash 值后缀变化的时候,才会真正地请求资源,否则返回 304 并加载浏览器缓存。

○ Last-Modified/If-Modified-Since

Last-Modified/If-Modified-Since 的值代表的是文件的最后修改时间,第一次请求服务端会把资源的最后修改时间放到 Last-Modified 响应头中,第二次发起请求的时候,请求头会带上上一次响应头中的 Last-Modified 的时间,并放到 If-Modified-Since 请求头属性中,服务端根据文件最后一次修改时间和 If-Modified-Since 的值进行比较,如果相等,返回 304 ,并加载浏览器缓存。

本地通过 express 起一个服务来验证协商缓存,代码如下:

我们先只考虑单因素影响,先不使用强缓存

const express = require('express');
const app = express();
var options = { etag: true, // 开启协商缓存lastModified: true, // 开启协商缓存setHeaders: (res, path, stat) => {res.set({'Cache-Control': 'max-age=00', // 浏览器不走强缓存'Pragma': 'no-cache', // 浏览器不走强缓存});},
};
app.use(express.static((__dirname + '/public'), options));
app.listen(3001);

第一次访问 

 

第二次访问

可以看到第二次访问该资源状态码304,表示资源未修改

Etag 值 未变化,还是 W/"e-1733c528895"

Last-Modified 值 也未变化,还是 Sat, 11 Jul 2020 05:21:48 GMT

 

 接下来我们看下这种情况:

1、采用的是对文件进行 MD5 加密来计算其 hash 值。

2、如果代码不使用 Last-Modified/If-Modified-Since,只使用 Etag

const express = require('express');
const CryptoJS = require('crypto-js/crypto-js');
const fs = require('fs');
const app = express();
var options = { etag: true, // 只通过Etag来判断lastModified: false, // 关闭另一种协商缓存setHeaders: (res, path, stat) => {const data = fs.readFileSync(path, 'utf-8'); // 读取文件const hash = CryptoJS.MD5((JSON.stringify(data))); // MD5加密res.set({'Cache-Control': 'max-age=00', // 浏览器不走强缓存'Pragma': 'no-cache', // 浏览器不走强缓存'ETag': hash, // 手动设置Etag值为MD5加密后的hash值});},
};app.use(express.static((__dirname + '/public'), options));
app.listen(3000);

在 test.js 加一行空格,再删除这行空格,第一次和第二次访问结果如下:

 

发现Etag的值无变化,都为 ETag: bde47610661e4e1ed8beb58e228ea0a5,说明如果只使用 Etag 来判断服务器资源是否被修改是不够,目前来看资源被修改过但是资源内容没有变化,仅仅使用 Etag 判断协商缓存,会返回 304,浏览器感知不到服务器的文件是被修改过的。

那么对于上述这种情况,可以在header中使用文件修改时间 lastModified 进行文件更新判断,如下:

第一次访问:记录 Last-Modified: Sat, 11 Jul 2020 07:10:44 GMT

我们 直接在 test.js 按一下 ctrl + s 保存一下(表示文件被修改过),再访问,可以看到 Last-Modified 的值从 Sat, 11 Jul 2020 07:10:44 GMT 变成了 Sat, 11 Jul 2020 07:14:24 GMT,状态码已经是 200 ,直接从服务器读取最新文件了。

 

 

由此可见,Last-Modified/If-Modified-Since 解决了 ETag/If-None-Match 如果文件做出修改,文件内容完全不变,继续使用协商缓存返回 304 的问题(其实此时服务器文件已经更新,应该返回200)

 仔细思考,如果存在某种极限情况,如果文件的修改频率在秒级以下,浏览器假设就在这毫秒级以内访问了两次, 那么这短短几毫秒之间访问两次, Last-Modified 无变化,那么我们可以使用 Etag,只要文件内容做出修改 Etag 就会变化,就会返回200.

你可能会说,如果是毫秒级文件更新, 并且文件内容无变化呢?是不是就傻了,返回304?这个问题,我暂时还不能回答你

但是总感觉文件毫秒级更新, Last-Modified  未变化是个伪命题,不成立。

 

言归正传,我们研究下 Express 的 Etag,和  Last-Modified 

const express = require('express');
const app = express();
var options = { etag: true, // 只通过Etag来判断lastModified: false, // 关闭另一种协商缓存setHeaders: (res, path, stat) => {res.set({'Cache-Control': 'max-age=00', // 浏览器不走强缓存'Pragma': 'no-cache', // 浏览器不走强缓存});},
};app.use(express.static((__dirname + '/public'), options));
app.listen(3000);

第一次

test.js 直接 ctrl + s 保存一下,可以看到 Etag 值从W/"e-1733cca6741"  变到 W/"e-1733ccc0656",那就更不用说搭上 Last-Modified 了,文件只要有一点内容或者任何的变化,就会返回 200,让浏览器获取最新资源。

 

 再深入研究,Express框架使用了serve-static中间件来配置缓存方案,其中,使用了一个叫etag的npm包来实现etag计算,从其源码可以看出,有两种计算方式:

方式一:使用文件大小和修改时间

function stattag (stat) {var mtime = stat.mtime.getTime().toString(16)var size = stat.size.toString(16)return '"' + size + '-' + mtime + '"'
}

方式二:使用文件内容的hash值和内容长度

function entitytag (entity) {if (entity.length === 0) {// fast-path emptyreturn '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"'}// compute hash of entityvar hash = crypto.createHash('sha1').update(entity, 'utf8').digest('base64').substring(0, 27)// compute length of entityvar len = typeof entity === 'string'? Buffer.byteLength(entity, 'utf8'): entity.lengthreturn '"' + len.toString(16) + '-' + hash + '"'
}

所以这也就是为什么使用 Express 的 Etag 时, 只保存一下 test.js ,浏览器再次访问就会获取 200 状态码的原因了, 因为 Etag 里面方法一 的Etag值中已经携带了修改时间。

 

参考文章:

图解 HTTP 缓存​​​​​​​: https://juejin.im/post/5eb7f811f265da7bbc7cc5bd

http://www.lryc.cn/news/2417910.html

相关文章:

  • 学习记录333@MySQL问题之server name is already exists解决方案
  • kernel panic 分析解决方法
  • 基于Java游戏论坛平台设计和实现(源码+LW+调试文档+讲解等)
  • Picasa生成图片幻灯片页面图文教程
  • Java集合详解(超详细)
  • 什么是CSharp
  • c语言常量详细解释及简单应用
  • 入门MySQL--0基础,操作详图,简单易懂
  • CDB(ContainerDatabase)与PDB(PluggableDatabase)
  • 【EVPN】EVPN名词简介
  • java环境变量详解_JAVA环境变量配置详解
  • php中file_get_contents如何读取大容量文件
  • 为什么下载的.msi安装文件打不开、运行不了?用mysql的.msi安装文件为例
  • 学习使用Python执行P4操作
  • 8Uftp连接服务器错误
  • 自学电脑编程_有哪些高质量的自学网站
  • javaSE(完整版)
  • 精进不休丨MogDB 数据库预读特性进一步提升20%+查询性能
  • 计算机期刊投稿须知
  • C语言qsort函数的使用详解
  • 2023最新个人博客文章发布系统的设计与实现(毕业设计+论文+开题报告+运行)
  • Android 游戏开发入门指南(一)
  • java Map遍历的5种方法和一些基本使用
  • Cocoa 框架概述
  • alternatives命令总结
  • PS(Photoshop)去水印的4个方法
  • MPEG音频文件格式(包括MP3文件格式)详解
  • 思科RIP路由协议介绍与实验操作步骤
  • 1.图文并茂详解Linux安装,客户端连接,xshell,虚拟机,虚拟网卡配置
  • 【统计类知识】大数定律与中心极限定理