cacheHandlers 配置允许您为 'use cache' 和 'use cache: remote' 定义自定义缓存存储实现。这使您能够将缓存的组件和函数存储在外部服务中,或自定义缓存行为。'use cache: private' 不可配置。
大多数应用程序不需要自定义缓存处理器。 默认的内存缓存适用于典型用例。
自定义缓存处理器适用于高级场景,即您需要跨多个实例共享缓存,或更改缓存的存储位置。例如,您可以为外部存储(如键值存储)配置一个自定义的 remote 处理器,然后在您的代码中使用 'use cache' 进行内存缓存,使用 'use cache: remote' 进行外部存储,从而在同一应用程序中实现不同的缓存策略。
跨实例共享缓存
默认的内存缓存是隔离到每个 Next.js 进程的。如果您正在运行多个服务器或容器,每个实例都将拥有自己的缓存,这些缓存不与其他实例共享,并在重启时丢失。
自定义处理器允许您与所有 Next.js 实例都可以访问的共享存储系统(如 Redis、Memcached 或 DynamoDB)集成。
更改存储类型
您可能希望以不同于默认内存方法的方式存储缓存。您可以实现自定义处理器,将缓存存储在磁盘、数据库或外部缓存服务中。原因包括:跨重启的持久性、减少内存使用或与现有基础设施集成。
要配置自定义缓存处理器:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheHandlers: {
default: require.resolve('./cache-handlers/default-handler.js'),
remote: require.resolve('./cache-handlers/remote-handler.js'),
},
}
export default nextConfigmodule.exports = {
cacheHandlers: {
default: require.resolve('./cache-handlers/default-handler.js'),
remote: require.resolve('./cache-handlers/remote-handler.js'),
},
}default:由 'use cache' 指令使用remote:由 'use cache: remote' 指令使用如果您不配置 cacheHandlers,Next.js 会为 default 和 remote 使用一个内存中的 LRU(最近最少使用)缓存。您可以查看 默认实现 作为参考。
您还可以定义额外的命名处理器(例如 sessions、analytics),并使用 'use cache: <name>' 引用它们。
请注意,'use cache: private' 不使用缓存处理器,也无法自定义。
缓存处理器必须实现 CacheHandler 接口,包含以下方法:
get()为给定缓存键检索缓存条目。
get(cacheKey: string, softTags: string[]): Promise<CacheEntry | undefined>| 参数 | 类型 | 描述 |
|---|---|---|
cacheKey | string | 缓存条目的唯一键。 |
softTags | string[] | 用于检查陈旧性的标签(在某些缓存策略中使用)。 |
如果找到,则返回一个 CacheEntry 对象;如果未找到或已过期,则返回 undefined。
您的 get 方法应从存储中检索缓存条目,根据 revalidate 时间检查其是否已过期,并对缺失或已过期的条目返回 undefined。
const cacheHandler = {
async get(cacheKey, softTags) {
const entry = cache.get(cacheKey)
if (!entry) return undefined
// Check if expired
const now = Date.now()
if (now > entry.timestamp + entry.revalidate * 1000) {
return undefined
}
return entry
},
}set()为给定缓存键存储缓存条目。
set(cacheKey: string, pendingEntry: Promise<CacheEntry>): Promise<void>| 参数 | 类型 | 描述 |
|---|---|---|
cacheKey | string | 存储条目的唯一键。 |
pendingEntry | Promise<CacheEntry> | 一个解析为缓存条目的 Promise。 |
此方法被调用时,条目可能仍在等待中(即其值流可能仍在写入)。您的处理器应在处理条目之前等待该 Promise。
返回 Promise<void>。
您的 set 方法必须在存储 pendingEntry Promise 之前等待它解析,因为在调用此方法时,缓存条目可能仍在生成。一旦解析,请将条目存储在您的缓存系统中。
const cacheHandler = {
async set(cacheKey, pendingEntry) {
// Wait for the entry to be ready
const entry = await pendingEntry
// Store in your cache system
cache.set(cacheKey, entry)
},
}refreshTags()在开始新请求之前定期调用,以与外部标签服务同步。
refreshTags(): Promise<void>如果您正在协调跨多个实例或服务的缓存失效,这将很有用。对于内存缓存,这可以是一个空操作(no-op)。
返回 Promise<void>。
对于内存缓存,这可以是一个空操作。对于分布式缓存,在处理请求之前使用此方法从外部服务或数据库同步标签状态。
const cacheHandler = {
async refreshTags() {
// For in-memory cache, no action needed
// For distributed cache, sync tag state from external service
},
}getExpiration()获取一组标签的最大重新验证时间戳。
getExpiration(tags: string[]): Promise<number>| 参数 | 类型 | 描述 |
|---|---|---|
tags | string[] | 要检查过期时间的标签数组。 |
返回:
0Infinity 表示应在 get 方法中检查软标签如果您不跟踪标签重新验证时间戳,则返回 0。否则,查找所有提供标签中最新的重新验证时间戳。如果您倾向于在 get 方法中处理软标签检查,则返回 Infinity。
const cacheHandler = {
async getExpiration(tags) {
// Return 0 if not tracking tag revalidation
return 0
// Or return the most recent revalidation timestamp
// return Math.max(...tags.map(tag => tagTimestamps.get(tag) || 0));
},
}updateTags()当标签被重新验证或过期时调用。
updateTags(tags: string[], durations?: { expire?: number }): Promise<void>| 参数 | 类型 | 描述 |
|---|---|---|
tags | string[] | 要更新的标签数组。 |
durations | { expire?: number } | 可选的过期持续时间(秒)。 |
您的处理器应更新其内部状态,以将这些标签标记为已失效。
返回 Promise<void>。
当标签被重新验证时,您的处理器应使所有具有这些标签的缓存条目失效。遍历您的缓存并移除标签与提供的列表匹配的条目。
const cacheHandler = {
async updateTags(tags, durations) {
// Invalidate all cache entries with matching tags
for (const [key, entry] of cache.entries()) {
if (entry.tags.some((tag) => tags.includes(tag))) {
cache.delete(key)
}
}
},
}CacheEntry 对象具有以下结构:
interface CacheEntry {
value: ReadableStream<Uint8Array>
tags: string[]
stale: number
timestamp: number
expire: number
revalidate: number
}| 属性 | 类型 | 描述 |
|---|---|---|
value | ReadableStream<Uint8Array> | 作为流的缓存数据。 |
tags | string[] | 缓存标签(不包括软标签)。 |
stale | number | 客户端陈旧性的持续时间(秒)。 |
timestamp | number | 条目创建的时间(毫秒级时间戳)。 |
expire | number | 条目允许被使用多长时间(秒)。 |
revalidate | number | 条目应在多长时间后重新验证(秒)。 |
须知:
value是一个ReadableStream。如果您需要读取和存储流数据,请使用.tee()。- 如果流在部分数据时出错,您的处理器必须决定是保留部分缓存还是丢弃它。
这是一个使用 Map 进行存储的最小实现。此示例演示了核心概念,但要实现具有 LRU 逐出、错误处理和标签管理的生产级实现,请参见 默认缓存处理器。
const cache = new Map()
const pendingSets = new Map()
module.exports = {
async get(cacheKey, softTags) {
// Wait for any pending set operation to complete
const pendingPromise = pendingSets.get(cacheKey)
if (pendingPromise) {
await pendingPromise
}
const entry = cache.get(cacheKey)
if (!entry) {
return undefined
}
// Check if entry has expired
const now = Date.now()
if (now > entry.timestamp + entry.revalidate * 1000) {
return undefined
}
return entry
},
async set(cacheKey, pendingEntry) {
// Create a promise to track this set operation
let resolvePending
const pendingPromise = new Promise((resolve) => {
resolvePending = resolve
})
pendingSets.set(cacheKey, pendingPromise)
try {
// Wait for the entry to be ready
const entry = await pendingEntry
// Store the entry in the cache
cache.set(cacheKey, entry)
} finally {
resolvePending()
pendingSets.delete(cacheKey)
}
},
async refreshTags() {
// No-op for in-memory cache
},
async getExpiration(tags) {
// Return 0 to indicate no tags have been revalidated
return 0
},
async updateTags(tags, durations) {
// Implement tag-based invalidation
for (const [key, entry] of cache.entries()) {
if (entry.tags.some((tag) => tags.includes(tag))) {
cache.delete(key)
}
}
},
}对于 Redis 或数据库等持久存储,您需要序列化缓存条目。以下是一个简单的 Redis 示例:
const { createClient } = require('redis')
const client = createClient({ url: process.env.REDIS_URL })
client.connect()
module.exports = {
async get(cacheKey, softTags) {
// Retrieve from Redis
const stored = await client.get(cacheKey)
if (!stored) return undefined
// Deserialize the entry
const data = JSON.parse(stored)
// Reconstruct the ReadableStream from stored data
return {
value: new ReadableStream({
start(controller) {
controller.enqueue(Buffer.from(data.value, 'base64'))
controller.close()
},
}),
tags: data.tags,
stale: data.stale,
timestamp: data.timestamp,
expire: data.expire,
revalidate: data.revalidate,
}
},
async set(cacheKey, pendingEntry) {
const entry = await pendingEntry
// Read the stream to get the data
const reader = entry.value.getReader()
const chunks = []
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
}
} finally {
reader.releaseLock()
}
// Combine chunks and serialize for Redis storage
const data = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)))
await client.set(
cacheKey,
JSON.stringify({
value: data.toString('base64'),
tags: entry.tags,
stale: entry.stale,
timestamp: entry.timestamp,
expire: entry.expire,
revalidate: entry.revalidate,
}),
{ EX: entry.expire } // Use Redis TTL for automatic expiration
)
},
async refreshTags() {
// No-op for basic Redis implementation
// Could sync with external tag service if needed
},
async getExpiration(tags) {
// Return 0 to indicate no tags have been revalidated
// Could query Redis for tag expiration timestamps if tracking them
return 0
},
async updateTags(tags, durations) {
// Implement tag-based invalidation if needed
// Could iterate over keys with matching tags and delete them
},
}| 部署选项 | 支持 |
|---|---|
| Node.js server | 是 |
| Docker container | 是 |
| Static export | 否 |
| Adapters | 特定于平台 |
| 版本 | 变更 |
|---|---|
v16.0.0 | 引入了 cacheHandlers。 |