数据缓存

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

基本用法

import { cache } from '@module-federation/bridge-react';

export type Data = {
  data: string;
};

export const fetchData = cache(async (): Promise<Data> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        data: `[ provider ] fetched data: ${new Date()}`,
      });
    }, 1000);
  });
});

参数

  • fetchData: DataLoader 中的 fetchData 函数。
  • options(可选): 缓存配置
    • tag: 用于标识缓存的标签,可以基于这个标签使缓存失效
    • maxAge: 缓存的有效期 (毫秒)
    • revalidate: 重新验证缓存的时间窗口(毫秒),与 HTTP Cache-Control 的 stale-while-revalidate 功能一致
    • getKey: 简化的缓存键生成函数,根据函数参数生成缓存键
    • onCache: 缓存数据时的回调函数,用于自定义缓存数据的处理逻辑 options 参数的类型如下:
interface CacheOptions {
  tag?: string | string[];
  maxAge?: number;
  revalidate?: number;
  getKey?: <Args extends any[]>(...args: Args) => string;
  onCache?: (info: CacheStatsInfo) => boolean;
}

返回值

cache 函数会返回一个新的 fetchData 函数,该函数有缓存的能力,多次调用该函数,不会重复执行。

使用范围

仅支持在 DataLoader 中使用。

详细用法

maxAge

每次计算完成后,框架会记录写入缓存的时间,当再次调用该函数时,会根据 maxAge 参数判断缓存是否过期,如果过期,则重新执行 fn 函数,否则返回缓存的数据。

import { cache, CacheTime } from '@module-federation/bridge-react';

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 '@module-federation/bridge-react';

const getDashboardStats = cache(
  async () => {
    return await fetchComplexStatistics();
  },
  {
    maxAge: CacheTime.MINUTE * 2,
    revalidate: CacheTime.MINUTE * 1,
  }
);

tag

tag 参数用于标识缓存的标签,可以传入一个字符串或字符串数组,可以基于这个标签使缓存失效,多个缓存函数可以使用一个标签。

import { cache, revalidateTag } from '@module-federation/bridge-react';

const getDashboardStats = cache(
  async () => {
    return await fetchDashboardStats();
  },
  {
    tag: 'dashboard',
  }
);

const getComplexStatistics = cache(
  async () => {
    return await fetchComplexStatistics();
  },
  {
    tag: 'dashboard',
  }
);

revalidateTag('dashboard-stats'); // 会使 getDashboardStats 函数和 getComplexStatistics 函数的缓存都失效

getKey

getKey 参数用于自定义缓存键的生成方式,例如你可能只需要依赖函数参数的一部分来区分缓存。它是一个函数,接收与原始函数相同的参数,返回一个字符串作为缓存键:

import { cache, CacheTime } from '@module-federation/bridge-react';
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 '@module-federation/bridge-react';
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 参数用于定制缓存的键,它是一个函数,接收一个包含以下属性的对象,返回值必须是字符串或 Symbol 类型,将作为缓存的键:

  • params:调用缓存函数时传入的参数数组
  • fn:原始被缓存的函数引用
  • generatedKey:框架基于入参自动生成的原始缓存键
INFO

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

  1. 缓存的函数引用发生变化
  2. 函数的入参发生变化
  3. 不满足 maxAge
  4. 调用了 revalidateTag

customKey 可以用在函数引用不同,但希望共享缓存的场景,如果只是自定义缓存键,推荐使用 getKey

这在某些场景下非常有用,比如当函数引用发生变化时,但你希望仍然返回缓存的数据。

import { cache } from '@module-federation/bridge-react';
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]}`;
    },
  }
);

// 即使 getUserA 和 getUserB 是不同的函数引用,但由于它们的 customKey 返回相同的值
// 所以当调用参数相同时,它们会共享缓存
const dataA = await getUserA(1);
const dataB = await getUserB(1); // 这里会命中缓存,不会再次发起请求

// 也可以使用 Symbol 作为缓存键(通常用于共享同一个应用内的缓存)
const USER_CACHE_KEY = Symbol('user-cache');
const getUserC = cache(
  fetchUserData,
  {
    maxAge: CacheTime.MINUTE * 5,
    customKey: () => USER_CACHE_KEY,
  }
);

// 可以利用 generatedKey 参数在默认键的基础上进行修改
const getUserD = cache(
  fetchUserData,
  {
    customKey: ({ generatedKey }) => `prefix-${generatedKey}`,
  }
);

onCache

onCache 参数允许你跟踪缓存统计信息,例如命中率。这是一个回调函数,接收有关每次缓存操作的信息,包括状态、键、参数和结果。你可以在 onCache 返回 false 来阻止命中缓存。

import { cache, CacheTime } from '@module-federation/bridge-react';

// 跟踪缓存统计
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 '@module-federation/bridge-react';

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