request-kv-cache
v1.0.4
Published
Framework-agnostic request result cache with TTL, persistence, and request deduplication.
Maintainers
Readme
request-kv-cache
无框架依赖的请求返回值缓存。缓存值保持原始对象、数组或其他 JavaScript 值;use() 返回原生状态对象,不包装成 Vue ref。
特性
- 相同参数共享缓存结果
- 相同缓存键的并发请求自动去重
use()为相同缓存键返回同一个完整状态对象- 支持 TTL,
0表示永不过期 - 支持
localStorage或自定义同步存储 - 请求失败不写缓存,下次调用可直接重试
refresh()失败时保留原有成功缓存- 默认稳定序列化 primitive、数组、普通对象参数
- 同时提供 ESM、CommonJS 和 TypeScript 类型
安装
npm install request-kv-cache基础用法
import { createRequestCache } from "request-kv-cache";
interface User {
id: number;
name: string;
}
interface UserParams {
page: number;
status: string[];
}
const userCache = createRequestCache<User[], UserParams>({
ttl: 30_000,
request: async (params) => {
const search = new URLSearchParams({
page: String(params.page),
status: params.status.join(","),
});
const response = await fetch(`/api/users?${search}`);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json() as Promise<User[]>;
},
});
const users = await userCache.get({ page: 1, status: ["active"] });get() 和 refresh() 直接返回请求函数产生的原始值。缓存命中时,同一对象或数组引用会被复用,直到刷新、删除或过期。
API
createRequestCache(options)
const cache = createRequestCache<TData, TParams>({
request,
ttl,
storage,
storageKey,
serializeKey,
});| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| request | (params) => Promise<TData> | 必填 | 实际请求函数 |
| ttl | number | 0 | 缓存有效期,单位毫秒;0 表示永不过期 |
| storage | RequestCacheStorage | 无 | 可选同步持久化存储 |
| storageKey | string | request-kv-cache | 持久化命名空间 |
| serializeKey | (params) => string | 稳定序列化 | 自定义缓存键 |
| wrapState | (state) => state | 原始对象 | 可选状态对象包装函数,仅在每个 key 首次调用 use() 时执行 |
get(params, refresh = false)
默认返回未过期缓存;没有缓存时执行请求。第二个参数传 true 时忽略已有缓存并重新请求。同一缓存键正在请求时,所有调用仍共享同一个 Promise。
const data = await cache.get(params);
const refreshedData = await cache.get(params, true);use(params)
返回指定 key 对应的稳定状态对象。相同 key 多次调用返回同一个对象;没有有效缓存时会自动请求。load() 使用缓存,refresh() 强制请求,失败信息写入 error,不会抛出到调用方:
const state = cache.use(params);
state.loading;
state.data;
state.error;
await state.load();
await state.refresh();use() 默认只使用原生对象,不依赖 Vue、React 或其他框架。框架需要接管状态对象时,应通过 wrapState 包装;核心后续会直接更新包装后的同一个对象。
refresh(params)
get(params, true) 的兼容别名。忽略现有缓存并重新请求。如果同一缓存键已有进行中的请求,则复用该请求。刷新失败会抛出错误,但不会删除原有成功缓存。
const data = await cache.refresh(params);peek(params)
同步读取未过期缓存,不发请求。没有缓存时返回 undefined。
const cached = cache.peek(params);has(params)
同步判断是否存在未过期缓存。
if (cache.has(params)) {
console.log("cache hit");
}remove(params)
删除指定缓存,返回是否实际删除。
cache.remove(params);clear()
清空当前实例的全部缓存和对应持久化内容。
cache.clear();cleanExpired()
清理已过期缓存,返回删除数量。
const removedCount = cache.cleanExpired();size
返回当前未过期缓存数量。
console.log(cache.size);缓存键
默认缓存键支持以下参数:
null、undefined、字符串、布尔值、数字、bigint- 数组
- 普通对象和
Object.create(null)对象
普通对象字段会按键名排序,因此 { page: 1, type: "a" } 和 { type: "a", page: 1 } 命中同一缓存。
循环引用、函数、Symbol、Date、Map、Set 和类实例默认不支持。此类参数应提供 serializeKey:
const cache = createRequestCache<Result, Request>({
request: sendRequest,
serializeKey: (request) => `${request.method}:${request.url}`,
});持久化
浏览器可直接传入 localStorage:
const cache = createRequestCache<Result, Params>({
request: loadResult,
ttl: 60_000,
storage: localStorage,
storageKey: "user-list-cache",
});自定义存储只需实现同步接口:
interface RequestCacheStorage {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
}持久化通过 JSON 完成,因此请求返回值必须可被 JSON.stringify()。存储权限、容量或序列化失败不会中断内存缓存。
在 Vue 3 中使用
包本身不依赖 Vue。下面在创建缓存时通过 wrapState 配置一次 shallowReactive();业务 useXxx() 只返回核心维护的状态对象,不重复维护 Map,也不绑定组件生命周期。
场景一:系统配置
接口接收配置键 key,返回对应配置值 value。缓存参数就是 key,因此不同配置分别缓存:
// src/utils/system-config.ts
import { shallowReactive } from "vue";
import { createRequestCache } from "request-kv-cache";
interface ConfigResponse {
data: string;
}
export const systemConfigCache = createRequestCache<string, string>({
wrapState: shallowReactive,
request: async (key) => {
const response = await fetch(
`/api/system/config?key=${encodeURIComponent(key)}`
);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const result = (await response.json()) as ConfigResponse;
return result.data;
},
});
export function getSystemConfig(
key: string,
refresh = false
): Promise<string> {
// 配置 key 直接作为缓存 key,不同配置互不覆盖。
return systemConfigCache.get(key, refresh);
}
export function useSystemConfig(key: string) {
// 核心缓存负责稳定维护相同 key 的状态引用。
return systemConfigCache.use(key);
}Vue 文件使用 use 方式:
<script setup lang="ts">
import { useSystemConfig } from "@/utils/system-config";
const systemName = useSystemConfig("system.name");
</script>
<template>
<button :disabled="systemName.loading" @click="systemName.refresh">刷新</button>
<p v-if="systemName.error">系统名称加载失败</p>
<h1>{{ systemName.data }}</h1>
</template>其他 JavaScript/TypeScript 文件直接使用 get 方式:
import { getSystemConfig } from "@/utils/system-config";
const copyright = await getSystemConfig("system.copyright");重复调用相同 key 会返回同一个完整状态对象,且只请求一次;不同 key 分别维护自己的状态和缓存值。
场景二:城市列表
城市列表没有请求参数。工具文件使用固定内部 key,且 ttl 默认为 0,所以应用生命周期内只有第一次调用会请求接口:
// src/utils/city-list.ts
import { shallowReactive } from "vue";
import { createRequestCache } from "request-kv-cache";
export interface City {
code: string;
name: string;
}
interface CityListResponse {
data: City[];
}
export const CITY_LIST_KEY = "city-list";
export const cityListCache = createRequestCache<City[], string>({
wrapState: shallowReactive,
request: async () => {
const response = await fetch("/api/cities");
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const result = (await response.json()) as CityListResponse;
return result.data;
},
});
export function getCityList(refresh = false): Promise<City[]> {
// 固定 key 让 use/get 方式共享同一条城市缓存记录。
return cityListCache.get(CITY_LIST_KEY, refresh);
}
export function useCityList() {
// 核心缓存负责稳定维护城市列表的状态引用。
return cityListCache.use(CITY_LIST_KEY);
}Vue 文件使用 use 方式:
<script setup lang="ts">
import { useCityList } from "@/utils/city-list";
const cities = useCityList();
</script>
<template>
<button :disabled="cities.loading" @click="cities.refresh">刷新城市</button>
<p v-if="cities.error">城市加载失败</p>
<ul>
<li v-for="city in cities.data" :key="city.code">{{ city.name }}</li>
</ul>
</template>其他 JavaScript/TypeScript 文件直接使用 get 方式:
import { getCityList } from "@/utils/city-list";
const cities = await getCityList();useCityList() 始终返回同一个完整状态对象,并和 getCityList() 使用同一个模块级缓存实例。无论被多少个文件调用,第一次成功请求后都会复用城市列表缓存。
本地调试
pnpm install
pnpm dev调试页完全使用原生 DOM、普通对象和数组,不依赖 Vue,并提供同 key 三路并发成功、并发失败场景。
测试
完整测试使用 Node.js 内置 node:test 和 assert,不依赖第三方测试框架:
pnpm testpnpm test 会先构建 dist,再测试全部公开 API、TTL、并发去重、失败重试、缓存键和持久化。已有最新构建产物时也可直接运行:
pnpm test:nodeLicense
MIT
