> For AI agents: the complete documentation index is available at /zh/llms.txt, the full documentation bundle is available at /zh/llms-full.txt.

# 数据缓存

`cache` 函数可以让你缓存数据获取或计算的结果，相比整页[渲染缓存](/zh/guides/basic-features/render/ssr-cache.md)，它提供了更精细的数据粒度控制，并且适用于客户端渲染（CSR）、服务端渲染（SSR）、API 服务（BFF）等多种场景。

## 基本用法

:::note

如果在 BFF 中使用 `cache` 函数，应该从 `@modern-js/server-runtime/cache` 导入相关函数：

`import { cache } from '@modern-js/server-runtime/cache'`

:::

```ts
import { cache } from '@modern-js/runtime/cache';
import { fetchUserData } from './api';

const getUser = cache(fetchUserData);

const loader = async () => {
  const user = await getUser('123'); // 函数入参发生变化时，函数会重新执行
  return {
    user,
  };
};
```

### 参数

- `fn`：需要缓存的数据获取或计算的函数
- `options`（可选）：缓存配置
  - `tag`：用于标识缓存的标签，可以基于这个标签使缓存失效
  - `maxAge`：缓存的有效期 (毫秒)
  - `revalidate`：重新验证缓存的时间窗口（毫秒），与 HTTP Cache-Control 的 stale-while-revalidate 功能一致
  - `getKey`：简化的缓存键生成函数，根据函数参数生成缓存键
  - `customKey`：自定义缓存键生成函数，用于在函数引用变化时保持缓存

`options` 参数的类型如下：

```ts
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](https://react.dev/reference/react/cache) 函数只能在 server component 组件中使用不同，
Modern.js 提供的 `cache` 函数可以在任意的前端或服务端的代码中使用。

## 详细用法

### 无 `options` 参数

当无 `options` 参数传入时，主要可以用于 SSR 项目，缓存的生命周期是单次 ssr 渲染的请求，如可以在多个 data loader 中调用同一个 cachedFn 时，不会重复执行 cachedFn 函数。这样可以在不同的 data loader 中共享数据，同时避免重复的请求，Modern.js 会在每次收到服务端请求时，重新执行 `fn` 函数。

:::info
无 `options` 参数时，可以看作是 react [`cache`](https://react.dev/reference/react/cache) 函数的替代品，可以在任意服务端代码中使用（比如可以在 SSR 项目的 data loader 中），不局限于 server component。
:::

```ts
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` 函数，否则返回缓存的数据。

```ts
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 模式。

如以下示例，缓存的行为会根据时间分为三个阶段：

1. **0 到 2 分钟（`maxAge` 内）**：缓存新鲜，直接返回缓存数据，不会重新请求
2. **2 到 3 分钟（`maxAge` 到 `maxAge + revalidate` 之间）**：缓存过期但在重新验证窗口内，会立即返回旧数据，同时在后台重新请求数据并更新缓存
3. **超过 3 分钟（超过 `maxAge + revalidate`）**：缓存完全过期，会重新执行函数获取新数据

```ts
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` 参数用于标识缓存的标签，可以传入一个字符串或字符串数组，可以基于这个标签使缓存失效，多个缓存函数可以使用一个标签。

```ts
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'); // 会使 getDashboardStats 函数和 getComplexStatistics 函数的缓存都失效
```

#### `getKey` 参数

`getKey` 参数用于**简化**缓存键的生成方式，例如你可能只需要依赖函数参数的一部分来区分缓存。它是一个函数，接收与原始函数相同的参数，返回一个字符串。

它的返回值会作为参数部分参与最终缓存键的生成，但最终的键仍然会包含函数的唯一标识，因此缓存是**函数级别**的。

```ts
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 生成缓存的键：

:::info

Modern.js 中的 `generateKey` 函数确保即使对象属性顺序发生变化，也能生成一致的唯一键值，保证稳定的缓存

:::

```ts
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`：框架基于入参自动生成的原始缓存键

:::info

一般在以下场景，缓存会失效：

1. 函数的入参发生变化
2. 不满足 maxAge 条件
3. 调用了 `revalidateTag`

默认情况下，框架会基于函数的字符串表示生成稳定的函数 ID，并与生成的参数键组合。`customKey` 可以用于完全控制缓存键的生成，特别适用于在不同函数实例间共享缓存。如果只是需要自定义参数如何转换为缓存键，推荐使用 `getKey`。

:::

这在某些场景下非常有用，比如当你希望在不同函数实例间共享缓存，或者需要可预测的缓存键用于外部缓存管理时。

```ts
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` 参数允许你跟踪缓存统计信息，例如命中率。这是一个回调函数，接收有关每次缓存操作的信息，包括状态、键、参数和结果。

```ts
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 算法移除旧的缓存。

:::info
考虑到 `cache` 函数缓存的结果内容不会很大，所以目前默认都存储在内存中
:::

可以通过 `configureCache` 函数指定缓存的存储上限：

```ts
import { configureCache, CacheSize } from '@modern-js/runtime/cache';

configureCache({
  maxSize: CacheSize.MB * 10, // 10MB
});
```

#### 自定义存储器

除了默认的内存存储，你还可以使用自定义的存储容器，例如 Redis、文件系统、数据库等。这样可以实现跨进程、跨服务器的缓存共享。

##### Container 接口

自定义存储容器需要实现 `Container` 接口：

```ts
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>;
}
```

##### 基本使用

```ts
import { configureCache } from '@modern-js/runtime/cache';

// 使用自定义存储容器
configureCache({
  container: customContainer,
});
```

##### 使用 `customKey` 确保缓存键稳定性

:::warning 重要建议

当使用自定义存储容器（如 Redis）时，**建议配置 `customKey`** 来确保缓存键的稳定性。这样可以确保：

1. **跨进程共享**：不同服务器实例能够共享相同的缓存
2. **应用重启后缓存有效**：重启应用后仍能命中之前的缓存
3. **代码部署后缓存保持**：代码更新后相同逻辑的缓存仍然有效

:::

默认的缓存键生成机制基于函数引用，在分布式环境中可能不够稳定。建议使用 `customKey` 提供稳定的缓存键：

```ts
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 存储示例

以下是一个使用 Redis 作为存储后端的示例：

```ts
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.client.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),
});
```

##### 注意事项

1. **序列化**：所有的缓存数据都会被序列化为字符串存储，容器只需要处理字符串的存取操作。

2. **TTL 支持**：如果你的存储后端支持 TTL（生存时间），可以在 `set` 方法中使用 `options.ttl` 参数（单位为秒）。
