在 HTTP 协议中,Cache-Control 和 Expires 都是与缓存相关的响应头,它们用于控制资源在客户端(例如浏览器)和代理服务器(例如 CDN)上的缓存行为。这两个头部有相似的功能,但是它们的使用方式和精确性有所不同。
Expires
Expires 头部是 HTTP/1.0 的产品,主要用于指定一个资源的过期时间。这是一个绝对时间,即指定一个具体的日期和时间,在此时间之前,资源被认为是新鲜的,可以从缓存中提供,而无需重新请求原服务器。
http
HTTP/1.1 200 OK
Date: Sun, 16 Apr 2024 12:00:00 GMT
Expires: Sun, 20 Apr 2024 12:00:00 GMT
Content-Type: text/html
Content-Length: 7777
<html>
...
</html>
在此示例中,Expires 头指定资源在 2024 年 4 月 20 日 GMT 12:00 过期。如果请求发生在这个时间之前,浏览器可以直接从缓存中加载资源,不必再发起网络请求。
因为这些特性,它也存在一些问题,主要有以下几个方面:
-
时间同步问题:如果客户端和服务器之间的时钟不同步,可能导致缓存被错误地使用或过早地舍弃。
-
更新问题:一旦 Expires 头被发送,服务器端的任何更新策略改变都需要等到新的 Expires 时间点到来后才能影响客户端行为。
-
灵活性不足:Expires 头只能设置一个固定时间,不能根据不同场景调整缓存策略。
Cache-Control
相对于 Expires,Cache-Control 提供了更多的控制功能和更高的灵活性。它是在 HTTP/1.1 中引入的,允许通过多种指令来精细控制缓存策略。
它的主要指令主要有以下几个方面:
-
max-age=<seconds>
: 指定一个相对时间,表示资源在多少秒后过期。 -
no-cache: 要求每次请求都必须去服务器验证缓存的有效性。
-
no-store: 禁止缓存响应的任何部分,适用于敏感信息。
-
must-revalidate: 一旦资源过期(如 max-age 时间到),在使用前必须去服务器验证。
-
public vs private: public 意味着响应可以被任何缓存存储,包括 CDN 等;private 则仅限用户个人设备缓存。
它的响应示例如下所示:
http
HTTP/1.1 200 OK
Date: Sun, 16 Apr 2024 12:00:00 GMT
Cache-Control: max-age=86400, must-revalidate
Content-Type: text/html
Content-Length: 1234
<html>
...
</html>
在这个例子中,Cache-Control 设置 max-age=86400(1 天),这意味着从响应被生成开始计时,24 小时内资源都是新鲜的。此外,must-revalidate 指令要求一旦资源过期,必须向服务器验证其有效性才能继续使用。
如何在 NestJs 中设置
在 NestJS 中设置 HTTP 响应头 Cache-Control 和 Expires,可以通过多种方式实现,具体取决于我们想如何控制这些头部。NestJS 提供了强大的装饰器和中间件支持,使得添加这些头部变得既简单又灵活。下面我将详细介绍几种常见的实现方法。
使用装饰器直接设置头部
在 NestJS 中,一直比较直接的方法就是我们可以直接在控制器的处理函数上使用装饰器来设置响应头,具体代码示例如下所示:
ts
import { Controller, Get, Res } from "@nestjs/common";
import { Response } from "express";
@Controller("example")
export class ExampleController {
@Get()
sendResponse(@Res() response: Response) {
response.set("Cache-Control", "public, max-age=3600");
response.set("Expires", new Date(Date.now() + 3600000).toUTCString());
response.send("Hello World");
}
}
在上面的代码中,我们使用了 express 的 Response 对象来设置 Cache-Control 和 Expires 头部。这种方法直观且易于控制。
使用拦截器全局设置头部
如果你想在多个路由间共享相同的缓存策略,可以使用拦截器来设置响应头。
首先我们要创建一个拦截器,如下代码所示:
ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from "@nestjs/common";
import { Response } from "express";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
@Injectable()
export class CacheControlInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
tap(() => {
const response = context.switchToHttp().getResponse<Response>();
response.set("Cache-Control", "public, max-age=3600");
response.set("Expires", new Date(Date.now() + 3600000).toUTCString());
})
);
}
}
然后再要使用到的模块中注册拦截器:
ts
import { Module } from "@nestjs/common";
import { APP_INTERCEPTOR } from "@nestjs/core";
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: CacheControlInterceptor,
},
],
})
export class AppModule {}
这种方式能够确保所有的响应都会包含相应的缓存控制头部,非常适合我们需要在全局范围内应用缓存策略的场景。
使用中间件设置头部
另一个灵活的方案是使用中间件来设置这些头部。中间件在请求处理流程中提供了更多控制权。
首先创建一个中间件:
ts
import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
@Injectable()
export class HeaderMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
res.set("Cache-Control", "public, max-age=3600");
res.set("Expires", new Date(Date.now() + 3600000).toUTCString());
next();
}
}
@Module({
imports: [],
controllers: [],
providers: [],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(HeaderMiddleware).forRoutes("*"); // 应用于所有路由
}
}
在这个例子中,中间件 HeaderMiddleware 被应用于所有路由,为它们设置统一的缓存控制头部。你也可以选择只对特定路由应用这个中间件。
总结
当 Expires 和 Cache-Control 同时存在时,HTTP/1.1 兼容的客户端会优先考虑 Cache-Control,这样可以保证缓存策略的正确执行,避免由于时间同步问题导致的资源过期错误。
鉴于 Cache-Control 提供了更加详细和灵活的缓存管理能力,建议在实际开发中优先使用 Cache-Control 来控制资源的缓存策略。这不仅可以减少因时间不同步导致的问题,还可以根据不同需求调整缓存规则,如使用 private 对敏感数据进行保护,或者通过 no-store 完全禁止缓存。这样的策略更能适应现代 Web 应用的多变需求。
最后分享两个我的两个开源项目,它们分别是:
这两个项目都会一直维护的,如果你想参与或者交流学习,可以加我微信 yunmz777 如果你也喜欢,欢迎 star 🚗🚗🚗