# 数据缓存 `cache` 函数可以让你缓存数据获取或计算的结果,它提供了更精细的数据粒度控制,并且适用于客户端渲染(CSR)、服务端渲染(SSR)、API 服务(BFF)等多种场景。 ## 基本用法 ```ts 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` 参数的类型如下: ```ts 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` 函数,否则返回缓存的数据。 ```ts 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分钟内,收到的请求会先返回旧数据,然后后台会重新请求数据,并更新缓存。 ```ts 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` 参数用于标识缓存的标签,可以传入一个字符串或字符串数组,可以基于这个标签使缓存失效,多个缓存函数可以使用一个标签。 ```ts 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` 参数用于自定义缓存键的生成方式,例如你可能只需要依赖函数参数的一部分来区分缓存。它是一个函数,接收与原始函数相同的参数,返回一个字符串作为缓存键: ```ts 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` 函数确保即使对象属性顺序发生变化,也能生成一致的唯一键值,保证稳定的缓存 ::: ```ts 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` ::: 这在某些场景下非常有用,比如当函数引用发生变化时,但你希望仍然返回缓存的数据。 ```ts 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` 来阻止命中缓存。 ```ts 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` 函数指定缓存的存储上限: ```ts import { configureCache, CacheSize } from '@module-federation/bridge-react'; configureCache({ maxSize: CacheSize.MB * 10, // 10MB }); ```