For AI agents: the complete documentation index is available at /zh/llms.txt, the full documentation bundle is available at /zh/llms-full.txt, and this page is available as Markdown at /zh/guides/basic-features/data/data-cache.md.
  • 简体中文
  • 数据缓存

    cache 函数可以让你缓存数据获取或计算的结果,相比整页渲染缓存,它提供了更精细的数据粒度控制,并且适用于客户端渲染(CSR)、服务端渲染(SSR)、API 服务(BFF)等多种场景。

    基本用法

    Note

    如果在 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('123'); // 函数入参发生变化时,函数会重新执行
      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 组件中使用不同, Modern.js 提供的 cache 函数可以在任意的前端或服务端的代码中使用。

    详细用法

    无 options 参数

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

    Info

    无 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 模式。

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

    1. 0 到 2 分钟(maxAge 内):缓存新鲜,直接返回缓存数据,不会重新请求
    2. 2 到 3 分钟(maxAge 到 maxAge + revalidate 之间):缓存过期但在重新验证窗口内,会立即返回旧数据,同时在后台重新请求数据并更新缓存
    3. 超过 3 分钟(超过 maxAge + revalidate):缓存完全过期,会重新执行函数获取新数据
    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'); // 会使 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 生成缓存的键:

    Info

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

    一般在以下场景,缓存会失效:

    1. 函数的入参发生变化
    2. 不满足 maxAge 条件
    3. 调用了 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 算法移除旧的缓存。

    Info

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

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

    import { configureCache, CacheSize } from '@modern-js/runtime/cache';
    
    configureCache({
      maxSize: CacheSize.MB * 10, // 10MB
    });

    自定义存储器

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

    Container 接口

    自定义存储容器需要实现 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 确保缓存键稳定性
    重要建议

    当使用自定义存储容器(如 Redis)时,建议配置 customKey 来确保缓存键的稳定性。这样可以确保:

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

    默认的缓存键生成机制基于函数引用,在分布式环境中可能不够稳定。建议使用 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 存储示例

    以下是一个使用 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.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 参数(单位为秒)。