我们应该了解的前端缓存
开发人员可以使用多个 headers 来操纵缓存行为。
旧规范与新规范混合在一起:它需要配置许多设置,并且多个用户可能会报告不一致的行为。
在本文中,我将重点解释代理服务器中不同 headers 如何影响浏览器缓存以及它们之间的关系。
对于单页应用程序,它实现无限期地缓存 CSS、字体和图像文件,并防止缓存 HTML 文件和适用的 service worker。该策略作为资源文件是可行的。它们的文件名具有唯一的标识符。
我们可以在 webpack 中对我们的资源文件名中添加hash或者chunkhash来执行相同的缓存配置,这种技术称为长期缓存。
但是,如果我们的网站阻止新的下载,我们该如何更新呢?为了保持站点的更新能力,永远不要缓存 HTML 文件是非常重要。
每次我们访问我的网站时,我们的浏览器都会显示来自服务器的新 HTML 副本,并且仅当有新的 src 脚本或 href 链接时,浏览器才会从服务器下载新资源。
检查缓存
Cache-Control: no-store
如果我们的网站不需要登录,我们的浏览器不应在请求中记录任何内容。我们可以将其用于 HTML 脚本和 Service Worker。
Cache-Control: public、 no -cache
## 或者
Cache-Control: public, max-age=0, must-revalidate
这两者是等效的,尽管为无缓存,但允许我们提供缓存响应,除非浏览器不检查缓存是否是最新的。
如果正确设置 ETag 或 Last-Modified 标头,浏览器可以检查缓存是否已经是最新的。该版本可帮助我们和我们的用户节省带宽。我们可以将其用于 HTML 和 Service Worker 脚本。
Cache-Control: private, no-cache
## 或者
Cache-Control: private, max-age=0, must-revalidate
同样,这两者也是等价的。公共和私有之间的区别在于共享缓存(例如CDN)可以缓存公共响应,但不能缓存私有响应。
本地缓存(例如浏览器)仍可能缓存私有响应。当我们在服务器上渲染 HTML 并且渲染的 HTML 包含敏感或用户特定信息时,我们可以使用私有。
Cache-Control: public, max-age=31536000, immutable
例如,浏览器将根据 max-age 指令(606024*365)将响应存储一年。
immutable 指令告诉浏览器不得修改此响应(文件)的内容,并且浏览器不得通过发送 If-None-Match(ETag 验证)或 If-Modified-Since(上次修改验证)来验证其缓存。
这用于我们的静态资源以支持长期缓存策略
Pragma和Expires
Pragma: no-cache
Expires: <http-date>
Pragma 是一个旧标头,在HTTP/1.0规范中定义为请求标头。
后来变成了HTTP/ 1.1 的指定"Pragma: no-cache"应该被视为"Cache-Control: no-cache",但这并不是一个可靠的替代品,因为它始终充当请求标头。
对于 HTML 文件,可以禁用 Expires 标头或将其设置为过去的日期。对于静态资源,可以在 Nginx 过期指令中使用 cache-control-max-age 来管理它们。
ETag: W/"5e15153d-120f"
## 或者
ETag: "5e15153d-120f"
ETag 必须唯一地标识资源,并且在大多数情况下,Web 服务器会根据资源的内容生成数字指纹。
如果资源发生变化,ETag 值就会不同。
ETag 有两种类型。低 ETag 相等性表示资源在语义上是等效的。ETag 的良好验证意味着资源逐字节相同。
我们可以通过为弱 ETag 定义的"W/"前缀来区分两者。
弱ETag不适合字节范围查询,但在操作过程中很容易生成。
实际上,我们不会配置 ETag 并将其保留在我们的 Web 服务器上。
curl -I <http-address>
curl -I -H "Accept-Encoding: gzip" <http-address>
可以看到,当请求静态文件时,Nginx设置了一个强ETag。如果启用了 gzip 压缩但未上传压缩文件,则即时压缩将导致 ETag 较弱。
如果浏览器使用带有缓存资源 ETag 的 If-None-Match 请求标头,它将等待带有新资源的 200 OK 响应或空的 304 Unmodified 响应,表明我们应该使用缓存资源来下载新的一个。
同样的优化可以应用于GET API响应,并且不限于静态文件。
如果我们的应用程序接收大量JSON有效负载,我们可以将后端配置为根据有效负载的内容计算和配置 ETag。
在发送给客户端之前,将其与 If-None-Match 请求标头进行比较。
如果匹配,则发送 304 Unmodified 而不是有效负载,以节省带宽并提高 Web 应用程序性能。
Last-Modified
Last-Modified: Tue, 07 Jan 2023 23:33:17 GMT
最后修改的响应标头是另一种缓存控制机制,并使用最后修改的日期。Last Modified 标头是更准确 ETag 的替代机制。
当我们发送带有缓存资源的上次修改日期的 If-Modified-Since 请求标头时,浏览器期望带有更新资源的 200 OK 响应或指示使用缓存资源的 304 响应 Empty Unmodified,而不是下载新的一个。
Nginx 配置
现在我们已经了解了不同类型的缓存头的作用,是时候专注于将我们的知识付诸实践了。
以下 Nginx 配置适用于旨在支持长期缓存的单页面应用程序。
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
首先,我对最能从单页应用程序中受益的内容类型启用了 GZIP 压缩。有关每个可用 gzip 配置的更多详细信息,请参阅 Nginx gzip 模块文档。
location ~* (\.html|\/sw\.js)$ {
expires -1y;
add_header Pragma "no-cache";
add_header Cache-Control "public";
}
将所有 HTML 文件与服务工作脚本 /sw.js 进行比较,两者都不应该被缓存。
location ~* \.(js|css|png|jpg|jpeg|gif|ico|json)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
最大化缓存所有静态元素(即 JavaScript 文件)、CSS 文件、图像和静态 JSON 文件。如果我们托管自己的字体文件,也可以添加它们。
location / {
try_files $uri $uri/ =404;
}
if ($host ~* ^www\.(.*)) {
set $host_without_www $1;
rewrite ^(.*) https://$host_without_www$1 permanent;
}
这两个与缓存无关,但却是 Nginx 配置的重要组成部分。
由于现代单一源有漂亮的 URL --- 它支持路由,而我们的静态服务器不知道它。需要为每个非静态文件的路径提供默认的index.html 文件。
对于不带 www 的 URL。如果我们将应用程序托管在服务提供商已经为我们提供服务的地方,则可能不需要后者。
express配置
有时我们无法通过 Nginx 等反向代理服务器提供静态文件。
在这种情况下,我们可能需要使用像 Expresse 这样的服务器来提供静态文件。
js
import express, { Response } from "express";
import compression from "compression";
import path from "path";
const PORT = process.env.PORT || 3000;
const BUILD_PATH = "public";
const app = express();
function setNoCache(res: Response) {
const date = new Date();
date.setFullYear(date.getFullYear() - 1);
res.setHeader("Expires", date.toUTCString());
res.setHeader("Pragma", "no-cache");
res.setHeader("Cache-Control", "public, no-cache");
}
function setLongTermCache(res: Response) {
const date = new Date();
date.setFullYear(date.getFullYear() + 1);
res.setHeader("Expires", date.toUTCString());
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
}
app.use(compression());
app.use(
express.static(BUILD_PATH, {
extensions: ["html"],
setHeaders(res, path) {
if (path.match(/(\.html|\/sw\.js)$/)) {
setNoCache(res);
return;
}
if (path.match(/\.(js|css|png|jpg|jpeg|gif|ico|json)$/)) {
setLongTermCache(res);
}
},
}),
);
app.get("*", (req, res) => {
setNoCache(res);
res.sendFile(path.resolve(BUILD_PATH, "index.html"));
});
app.listen(PORT, () => {
console.log(`Server is running http://localhost:${PORT}`);
});
该脚本模仿了我们的 Nginx 设置的功能。使用压缩中间件启用 gzip。
Express Static 中间件为我们设置 ETag 和 Last-Modified 标头。如果请求与任何已知的静态文件都不匹配,我们应该自己发送index.html。