cache 函数可以让你缓存数据获取或计算的结果,相比整页渲染缓存,它提供了更精细的数据粒度控制,并且适用于客户端渲染(CSR)、服务端渲染(SSR)、API 服务(BFF)等多种场景。
需要 x.65.5 及以上版本
如果在 BFF 中使用 cache 函数,应该从 @modern-js/server-runtime/cache 导入相关函数
import { cache } from '@modern-js/server-runtime/cache'
import { cache } from '@modern-js/runtime/cache';
import { fetchUserData } from './api';
const getUser = cache(fetchUserData);
const loader = async () => {
const user = await getUser(user); // 函数入参发生变化时,函数会重新执行
return {
user,
};
};fn: 需要缓存的数据获取或计算的函数options(可选): 缓存配置
tag: 用于标识缓存的标签,可以基于这个标签使缓存失效maxAge: 缓存的有效期 (毫秒)revalidate: 重新验证缓存的时间窗口(毫秒),与 HTTP Cache-Control 的 stale-while-revalidate 功能一致getKey: 简化的缓存键生成函数,根据函数参数生成缓存键customKey: 自定义缓存键生成函数,用于在函数引用变化时保持缓存options 参数的类型如下:
interface CacheOptions {
tag?: string | string[];
maxAge?: number;
revalidate?: number;
getKey?: <Args extends any[]>(...args: Args) => string;
customKey?: <Args extends any[]>(options: {
params: Args;
fn: (...args: Args) => any;
generatedKey: string;
}) => string | symbol;
}cache 函数会返回一个新的函数,该函数有缓存的能力,多次调用该函数,不会重复执行 fn 函数。
与 react 的 cache 函数只能在 server component 组件中使用不同,
EdenX 提供的 cache 函数可以在任意的前端或服务端的代码中使用。
options 参数当无 options 参数传入时,主要可以用于 SSR 项目,缓存的生命周期是单次 ssr 渲染的请求,如可以在多个 data loader 中调用同一个 cachedFn 时,不会重复执行 cachedFn 函数。这样可以在不同的 data loader 中共享数据,同时避免重复的请求,EdenX 会在每次收到服务端请求时,重新执行 fn 函数。
无 options 参数时,可以看作是 react cache 函数的替代品,可以在任意服务端代码中使用(比如可以在 SSR 项目的 data loader 中),不局限于 server component。
import { cache } from '@modern-js/runtime/cache';
import { fetchUserData } from './api';
const getUser = cache(fetchUserData);
const loader = async () => {
const user = await getUser();
return {
user,
};
};options 参数maxAge 参数每次计算完成后,框架会记录写入缓存的时间,当再次调用该函数时,会根据 maxAge 参数判断缓存是否过期,如果过期,则重新执行 fn 函数,否则返回缓存的数据。
import { cache, CacheTime } from '@modern-js/runtime/cache';
const getDashboardStats = cache(
async () => {
return await fetchComplexStatistics();
},
{
maxAge: CacheTime.MINUTE * 2, // 在 2 分钟内调用该函数会返回缓存的数据
}
);revalidate 参数revalidate 参数用于设置缓存过期后,重新验证缓存的时间窗口,可以和 maxAge 参数一起使用,类似与 HTTP Cache-Control 的 stale-while-revalidate 模式。
如以下示例,在缓存未过期的 2分钟内,如果调用 getDashboardStats 函数,会返回缓存的数据,如果缓存过期,2分到3分钟内,收到的请求会先返回旧数据,然后后台会重新请求数据,并更新缓存。
import { cache, CacheTime } from '@modern-js/runtime/cache';
const getDashboardStats = cache(
async () => {
return await fetchComplexStatistics();
},
{
maxAge: CacheTime.MINUTE * 2,
revalidate: CacheTime.MINUTE * 1,
}
);tag 参数tag 参数用于标识缓存的标签,可以传入一个字符串或字符串数组,可以基于这个标签使缓存失效,多个缓存函数可以使用一个标签。
import { cache, revalidateTag } from '@modern-js/runtime/cache';
const getDashboardStats = cache(
async () => {
return await fetchDashboardStats();
},
{
tag: 'dashboard',
}
);
const getComplexStatistics = cache(
async () => {
return await fetchComplexStatistics();
},
{
tag: 'dashboard',
}
);
await revalidateTag('dashboard-stats'); // 会使 getDashboardStats 函数和 getComplexStatistics 函数的缓存都失效getKey 参数getKey 参数用于简化缓存键的生成方式,例如你可能只需要依赖函数参数的一部分来区分缓存。它是一个函数,接收与原始函数相同的参数,返回一个字符串。
它的返回值会作为参数部分参与最终缓存键的生成,但最终的键仍然会包含函数的唯一标识,因此缓存是函数级别的。
import { cache, CacheTime } from '@modern-js/runtime/cache';
import { fetchUserData } from './api';
const getUser = cache(
async (userId, options) => {
// 这里 options 可能包含很多配置,但我们只想根据 userId 缓存
return await fetchUserData(userId, options);
},
{
maxAge: CacheTime.MINUTE * 5,
// 只使用第一个参数(userId)作为缓存键
getKey: (userId, options) => userId,
}
);
// 下面两次调用会共享缓存,因为 getKey 只使用了 userId
await getUser(123, { language: 'zh' });
await getUser(123, { language: 'en' }); // 命中缓存,不会重新请求
// 不同的 userId 会使用不同的缓存
await getUser(456, { language: 'zh' }); // 不会命中缓存,会重新请求你也可以使用 Modern.js 提供的 generateKey 函数配合 getKey 生成缓存的键:
Modern.js 中的 generateKey 函数确保即使对象属性顺序发生变化,也能生成一致的唯一键值,保证稳定的缓存
import { cache, CacheTime, generateKey } from '@modern-js/runtime/cache';
import { fetchUserData } from './api';
const getUser = cache(
async (userId, options) => {
return await fetchUserData(userId, options);
},
{
maxAge: CacheTime.MINUTE * 5,
getKey: (userId, options) => generateKey(userId),
}
);customKey 参数customKey 参数用于完全定制缓存的键,它是一个函数,接收一个包含以下属性的对象,返回值必须是字符串类型。
它的返回值将直接作为最终的缓存键,覆盖了默认的函数标识和参数组合。这允许你创建全局唯一的缓存键,从而实现跨函数共享缓存。
params:调用缓存函数时传入的参数数组fn:原始被缓存的函数引用generatedKey:框架基于入参自动生成的原始缓存键一般在以下场景,缓存会失效:
revalidateTag默认情况下,框架会基于函数的字符串表示生成稳定的函数 ID,并与生成的参数键组合。customKey 可以用于完全控制缓存键的生成,特别适用于在不同函数实例间共享缓存。如果只是需要自定义参数如何转换为缓存键,推荐使用 getKey。
这在某些场景下非常有用,比如当你希望在不同函数实例间共享缓存,或者需要可预测的缓存键用于外部缓存管理时。
import { cache } from '@modern-js/runtime/cache';
import { fetchUserData } from './api';
// 不同的函数引用,但是通过 customKey 可以使它们共享一个缓存
const getUserA = cache(
fetchUserData,
{
maxAge: CacheTime.MINUTE * 5,
customKey: ({ params }) => {
// 返回一个稳定的字符串作为缓存的键
return `user-${params[0]}`;
},
}
);
// 即使函数引用变了,只要 customKey 返回相同的值,也会命中缓存
const getUserB = cache(
(...args) => fetchUserData(...args), // 新的函数引用
{
maxAge: CacheTime.MINUTE * 5,
customKey: ({ params }) => {
// 返回与 getUserA 相同的键
return `user-${params[0]}`;
},
}
);
// 现在你可以在不同的函数实现间共享缓存
await getUserA(123); // 获取数据并使用键 "user-123" 缓存
await getUserB(123); // 缓存命中,返回缓存的数据
// 可以利用 generatedKey 参数在默认键的基础上进行修改
const getUserC = cache(
fetchUserData,
{
customKey: ({ generatedKey }) => `prefix-${generatedKey}`,
}
);
// 用于可预测的缓存键,便于外部管理
const getUserD = cache(
async (userId: string) => {
return await fetchUserData(userId);
},
{
maxAge: CacheTime.MINUTE * 5,
customKey: ({ params }) => `app:user:${params[0]}`,
}
);onCache 参数onCache 参数允许你跟踪缓存统计信息,例如命中率。这是一个回调函数,接收有关每次缓存操作的信息,包括状态、键、参数和结果。
import { cache, CacheTime } from '@modern-js/runtime/cache';
// 跟踪缓存统计
const stats = {
total: 0,
hits: 0,
misses: 0,
stales: 0,
hitRate: () => stats.hits / stats.total
};
const getUser = cache(
fetchUserData,
{
maxAge: CacheTime.MINUTE * 5,
onCache({ status, key, params, result }) {
// status 可以是 'hit'、'miss' 或 'stale'
stats.total++;
if (status === 'hit') {
stats.hits++;
} else if (status === 'miss') {
stats.misses++;
} else if (status === 'stale') {
stats.stales++;
}
console.log(`缓存${status === 'hit' ? '命中' : status === 'miss' ? '未命中' : '陈旧'},键:${String(key)}`);
console.log(`当前命中率:${stats.hitRate() * 100}%`);
}
}
);
// 使用示例
await getUser(1); // 缓存未命中
await getUser(1); // 缓存命中
await getUser(2); // 缓存未命中onCache 回调接收一个包含以下属性的对象:
status: 缓存操作状态,可以是:
hit: 缓存命中,返回缓存内容miss: 缓存未命中,执行函数并缓存结果stale: 缓存命中但数据陈旧,返回缓存内容同时在后台重新验证key: 缓存键,可能是 customKey 的结果或默认生成的键params: 传递给缓存函数的参数result: 结果数据(来自缓存或新计算的)这个回调只在提供 options 参数时被调用。当使用无 options 的缓存函数时,不会调用 onCache 回调。
onCache 回调对以下场景非常有用:
目前不管是客户端还是服务端,缓存都存储在内存中,默认情况下所有缓存函数共享的存储上限是 1GB,当达到存储上限后,使用 LRU 算法移除旧的缓存。
考虑到 cache 函数缓存的结果内容不会很大,所以目前默认都存储在内存中
可以通过 configureCache 函数指定缓存的存储上限:
import { configureCache, CacheSize } from '@modern-js/runtime/cache';
configureCache({
maxSize: CacheSize.MB * 10, // 10MB
});除了默认的内存存储,你还可以使用自定义的存储容器,例如 Redis、文件系统、数据库等。这样可以实现跨进程、跨服务器的缓存共享。
自定义存储容器需要实现 Container 接口:
interface Container {
get: (key: string) => Promise<string | undefined | null>;
set: (key: string, value: string, options?: { ttl?: number }) => Promise<any>;
has: (key: string) => Promise<boolean>;
delete: (key: string) => Promise<boolean>;
clear: () => Promise<void>;
}import { configureCache } from '@modern-js/runtime/cache';
// 使用自定义存储容器
configureCache({
container: customContainer,
});customKey 确保缓存键稳定性:::warning 重要建议
当使用自定义存储容器(如 Redis)时,建议配置 customKey 来确保缓存键的稳定性。这样可以确保:
默认的缓存键生成机制基于函数引用,在分布式环境中可能不够稳定。建议使用 customKey 提供稳定的缓存键:
import { cache, configureCache } from '@modern-js/runtime/cache';
// 配置 Redis 容器
configureCache({
container: redisContainer,
});
// 推荐:使用 customKey 确保键的稳定性
const getUser = cache(
async (userId: string) => {
return await fetchUserData(userId);
},
{
maxAge: CacheTime.MINUTE * 5,
// 使用被缓存函数相关的稳定标识符作为缓存键
customKey: () => `fetchUserData`,
}
);以下是一个使用 Redis 作为存储后端的示例:
import { Redis } from 'ioredis';
import { Container, configureCache } from '@modern-js/runtime/cache';
class RedisContainer implements Container {
private client: Redis;
constructor(client: Redis) {
this.client = client;
}
async get(key: string): Promise<string | null> {
const value = await this.redis.get(key);
return value ? JSON.parse(value) : null;
}
async set(
key: string,
value: string,
options?: { ttl?: number },
): Promise<'OK'> {
if (options?.ttl) {
return this.client.set(key, JSON.stringify(value), 'EX', options.ttl);
}
return this.client.set(key, JSON.stringify(value));
}
async has(key: string): Promise<boolean> {
const result = await this.client.exists(key);
return result === 1;
}
async delete(key: string): Promise<boolean> {
const result = await this.client.del(key);
return result > 0;
}
async clear(): Promise<void> {
// 注意:在生产环境中要谨慎使用,这会清空整个 Redis 数据库
// 更好的实现方式是使用键前缀,然后删除匹配该前缀的所有键
await this.client.flushdb();
}
}
// 配置 Redis 存储
const redisClient = new Redis({
host: 'localhost',
port: 6379,
});
configureCache({
container: new RedisContainer(redisClient),
});序列化:所有的缓存数据都会被序列化为字符串存储,容器只需要处理字符串的存取操作。
TTL 支持:如果你的存储后端支持 TTL(生存时间),可以在 set 方法中使用 options.ttl 参数(单位为秒)。