# useRequestCache

用于处理请求缓存的 Hook，支持内存缓存、持久化缓存（LocalStorage/SessionStorage）、请求去重（并发共享）和结果转换。

## 基本用法

所有配置项均通过唯一的 `options` 对象传入。

```vue
<script setup lang="ts">
import { useRequestCache } from 'xn-fe-tools'
import { ref, computed } from 'vue'

// 模拟请求函数
const fetchUserInfo = async (params: { id: number }) => {
  return new Promise(resolve =>
    setTimeout(() => {
      resolve({ id: params.id, name: `User ${params.id}`, role: 'admin' })
    }, 500)
  )
}

const userId = ref(1)

const { data, loading, run, refresh } = useRequestCache({
  // 请求函数
  requestFn: fetchUserInfo,
  // 参数 (响应式)
  params: computed(() => ({ id: userId.value })),
  // 缓存 Key (动态)
  cacheKey: params => `user-info-${params.id}`,
  // 缓存时间 5 分钟
  cacheTime: 5 * 60 * 1000,
  // 自动执行
  immediate: true,
})
</script>

<template>
  <div>
    <div v-if="loading">Loading...</div>
    <div v-else>
      <pre>{{ data }}</pre>
    </div>

    <button @click="userId++">Next User (New Request)</button>
    <button @click="userId--">Prev User (Cached?)</button>
    <button @click="refresh">Force Refresh</button>
  </div>
</template>
```

## 缓存策略

支持以下存储模式：

- `memory`: (默认) 内存缓存，页面刷新后失效。
- `localStorage`: 浏览器的本地存储，永久有效。
- `sessionStorage`: 浏览器的会话存储，标签页关闭后失效。
- `indexedDB`: 基于 IndexedDB 的异步存储，适合存储体积较大的数据，完全不通过内存同步。

```ts
const { data } = useRequestCache({
  requestFn: fetchLargeList,
  cacheKey: 'large-data-list',
  storage: 'indexedDB', // 开启 IndexedDB 模式
  dbName: 'MyCacheDB', // 可选：自定义数据库名
  cacheTime: 60 * 60 * 1000,
})
```

## 缓存时间速记值 <Badge type="tip" text="v1.9.0+" />

`cacheTime` 除了接受毫秒数，还支持两个布尔速记值：

| 值        | 含义                   |
| --------- | ---------------------- |
| `true`    | 永久缓存（不会过期）   |
| `false`   | 禁用缓存（每次都请求） |
| `0`       | 禁用缓存（同 `false`） |
| `number`  | 毫秒数（原有行为）     |

```ts
// 永久缓存（适合静态配置、字典数据）
useRequestCache({ ..., cacheTime: true })

// 禁用缓存（每次都发请求，但保留去重/回调等其他特性）
useRequestCache({ ..., cacheTime: false })

// 毫秒数（原有用法不变）
useRequestCache({ ..., cacheTime: 30 * 60 * 1000 })
```

## API

```ts
function useRequestCache<T, P, R = any>(
  options: UseRequestCacheOptions<T, P, R>
): UseRequestCacheReturn<T>
```

### Options

| 参数                  | 类型                              | 默认值            | 说明                                                             |
| --------------------- | --------------------------------- | ----------------- | ---------------------------------------------------------------- |
| `requestFn`           | `(params: P) => Promise<R>`       | -                 | **必填**。实际发起请求的异步函数。                               |
| `cacheKey`            | `string \| (params: P) => string` | -                 | **必填**。缓存唯一标识。如果是函数，则根据 params 生成。         |
| `params`              | `MaybeRefOrGetter<P>`             | -                 | 请求参数。如果是响应式数据，变化时会自动触发请求。               |
| `cacheTime`           | `CacheTimeValue`                  | `300000`          | 缓存时间。`true` = 永久，`false`/`0` = 不缓存，数字 = 毫秒数。默认 5 分钟。 |
| `storage`             | `MaybeRefOrGetter<CacheStrategy>` | `'memory'`        | 缓存存储策略（'memory' \| 'localStorage' \| 'sessionStorage' \| 'indexedDB'）。 |
| `storageKeyPrefix`    | `string`                          | `'req_cache_'`    | 存储空间的 Key 前缀。                                            |
| `immediate`           | `boolean`                         | `true`            | 是否初始化时立即执行。                                           |
| `transformResult`     | `(data: R) => T`                  | -                 | 数据转换函数，将原始响应 `R` 转换为结果 `T`。                    |
| `forceRefreshOnFirst` | `MaybeRefOrGetter<boolean>`       | `false`           | 首次请求是否强制更新（跳过缓存，即使持久化存储中有值）。         |
| `dbName`              | `string`                          | `'RequestCacheDB'` | IndexedDB 数据库名称，仅 `storage: 'indexedDB'` 时有效。        |
| `storeName`           | `string`                          | `'request_cache'` | IndexedDB 存储对象名称，仅 `storage: 'indexedDB'` 时有效。      |
| `onSuccess`           | `(data: T) => void`               | -                 | 请求成功的回调。                                                 |
| `onError`             | `(error: any) => void`            | -                 | 请求失败的回调。                                                 |

### Return

| 属性         | 类型                              | 说明                           |
| ------------ | --------------------------------- | ------------------------------ |
| `data`       | `Ref<T \| undefined>`             | 请求返回的数据（已转换）。     |
| `loading`    | `Ref<boolean>`                    | 是否正在请求中。               |
| `error`      | `Ref<any>`                        | 请求错误信息。                 |
| `run`        | `() => Promise<T>`                | 手动触发请求（优先读取缓存）。 |
| `refresh`    | `() => Promise<T>`                | 强制刷新（忽略缓存）。         |
| `clearCache` | `(key?: string) => Promise<void>` | 清除指定 Key 的缓存。          |

### 全局管理函数 <Badge type="tip" text="v1.7.0+" />

与 `useRequestCache` 实例方法不同，以下函数作用于**模块级注册表**，可跨组件操作所有已发起过请求的缓存条目。

#### `setRequestCache(entries)` <Badge type="tip" text="v1.8.0+" />

在项目初始化阶段批量注入静态缓存数据。注入后，对应 `cacheKey` 的 `useRequestCache` 实例首次 `run()` 将直接命中缓存，不发起网络请求。

```ts
function setRequestCache(entries: SetRequestCacheEntry | SetRequestCacheEntry[]): Promise<void>
```

**SetRequestCacheEntry**

| 参数               | 类型            | 默认值             | 说明                                                                 |
| ------------------ | --------------- | ------------------ | -------------------------------------------------------------------- |
| `key`              | `string`        | -                  | **必填**。与对应 `useRequestCache` 实例的 `cacheKey` 保持一致。      |
| `data`             | `unknown`       | -                  | **必填**。要注入的原始数据（与 `requestFn` 返回类型一致）。          |
| `cacheTime`        | `CacheTimeValue` | `true`            | 缓存时间。`true`/不传 = 永久，`false`/`0` = 不缓存，数字 = 毫秒数。 |
| `storage`          | `CacheStrategy` | `'memory'`         | 存储策略。                                                           |
| `storageKeyPrefix` | `string`        | `'req_cache_'`     | storage key 前缀，需与对应实例保持一致。                             |
| `dbName`           | `string`        | `'RequestCacheDB'` | IndexedDB 数据库名，仅 `storage: 'indexedDB'` 时有效。              |
| `storeName`        | `string`        | `'request_cache'`  | IndexedDB store 名，仅 `storage: 'indexedDB'` 时有效。              |

#### `clearRequestCache(filter?)`

清除注册表中匹配的缓存。不传 `filter` 时清除全部。

```ts
function clearRequestCache(filter?: RequestCacheFilter): Promise<void>
```

#### `getRequestCache(filter?)`

获取注册表中匹配的缓存快照列表。不传 `filter` 时返回全部。

```ts
function getRequestCache(filter?: RequestCacheFilter): Promise<CacheSnapshot[]>
```

#### RequestCacheFilter

| 参数               | 类型                                    | 说明                                      |
| ------------------ | --------------------------------------- | ----------------------------------------- |
| `keys`             | `string[]`                              | 指定要操作的 key 列表，不传则匹配所有。   |
| `storage`          | `CacheStrategy \| CacheStrategy[]`      | 按 storage 策略过滤，不传则匹配所有。     |
| `storageKeyPrefix` | `string`                                | 按 storageKeyPrefix 过滤，不传则匹配所有。|

#### CacheSnapshot

| 属性        | 类型            | 说明                                           |
| ----------- | --------------- | ---------------------------------------------- |
| `key`       | `string`        | 缓存的原始 key（不含 storageKeyPrefix）。       |
| `data`      | `unknown`       | 缓存的原始数据（**未经** `transformResult` 处理）。 |
| `timestamp` | `number`        | 写入时间戳（毫秒）。                           |
| `storage`   | `CacheStrategy` | 所在 storage 策略。                            |
| `isExpired` | `boolean`       | 是否已过期但尚未被清除。                       |

## 高级用法

### 封装通用枚举选择器组件

在后台管理系统中，多个页面往往需要获取同样的枚举/字典数据。使用 `useRequestCache` 可以确保无论有多少个下拉组件同时存在，同类的接口只会请求一次，并且刷新页面后可依然通过 `sessionStorage` 极速加载。

```vue
<script setup lang="ts">
import { useRequestCache } from 'xn-fe-tools'

interface Option {
  label: string
  value: any
}

const props = defineProps<{
  enumKey: string
  requestFn: (key: string) => Promise<any[]>
  lazy?: boolean
}>()

// 使用 sessionStorage 缓存枚举值，刷新页面亦可复用
const { data, loading, run } = useRequestCache<Option[], string, any[]>({
  // 根据不同的枚举标识符创建缓存
  cacheKey: `enum-${props.enumKey}`,
  requestFn: () => props.requestFn(props.enumKey),
  storage: 'sessionStorage',
  cacheTime: 30 * 60 * 1000, // 30 分钟不失效
  immediate: !props.lazy,
  // 转换原始数据为 el-select 格式
  transformResult: list => list.map(item => ({ label: item.name, value: item.id })),
})

const handleFocus = () => {
  if (props.lazy && !data.value) run()
}
</script>

<template>
  <el-select v-model="value" :loading="loading" @focus="handleFocus">
    <el-option v-for="opt in data" :key="opt.value" :label="opt.label" :value="opt.value" />
  </el-select>
</template>
```

### IndexedDB 存储大型数据

在需要缓存大量列表数据且不希望占用 JS 内存（避免页面卡顿）时，推荐使用 `indexedDB` 模式。

```vue
<script setup lang="ts">
import { useRequestCache } from 'xn-fe-tools'

const { data, loading } = useRequestCache({
  requestFn: () => axios.get('/api/heavy-list').then(res => res.data),
  cacheKey: 'heavy-list-data',
  storage: 'indexedDB',
  dbName: 'AppStorage',
  storeName: 'api_cache',
  cacheTime: 12 * 60 * 60 * 1000, // 缓存12小时
})
</script>
```

### 手动控制与清除缓存

除了自动触发外，你还可以通过 `run`、`refresh` 和 `clearCache` 手动控制行为。

```vue
<script setup lang="ts">
import { useRequestCache } from 'xn-fe-tools'

const { run, refresh, clearCache } = useRequestCache({
  requestFn: fetchUserInfo,
  cacheKey: 'user-data',
  immediate: false, // 手动触发
})

const handleLoad = () => run() // 优先使用缓存
const handleUpdate = () => refresh() // 强制从接口拉取
const handleReset = () => clearCache() // 仅清除缓存，不重新请求
</script>
```

### 数据转换 (transformResult)

在数据存入缓存和返回给 `data` 之前，可以使用 `transformResult` 进行格式化。转换后的结果会被存入缓存，下次命中缓存时将直接返回转换后的数据。

```ts
const { data } = useRequestCache({
  requestFn: () => axios.get('/api/user'),
  cacheKey: 'user-profile',
  // 只保留需要的字段，减少缓存体积
  transformResult: res => ({
    name: res.data.userName,
    avatar: res.data.userAvatar,
  }),
})
```

### 初始化阶段预注入数据 <Badge type="tip" text="v1.8.0+" />

适用于应用启动时已能从服务端或 `window` 对象获取到数据，希望组件渲染时直接跳过请求、即时展示的场景。

```ts
// main.ts
import { createApp } from 'vue'
import { setRequestCache } from 'xn-fe-tools'
import App from './App.vue'

const app = createApp(App)

// 从全局配置对象预注入数据（默认永久不过期）
await setRequestCache([
  { key: 'user-profile',  data: window.__APP_CONFIG__.user },
  { key: 'permissions',   data: window.__APP_CONFIG__.permissions },
  { key: 'feature-flags', data: window.__APP_CONFIG__.features },
])

app.mount('#app')
```

组件内使用方式不变，`run()` 会直接命中预注入的缓存：

```ts
const { data } = useRequestCache({
  requestFn: fetchUserProfile,
  cacheKey: 'user-profile', // 与注入时的 key 一致
})
// data.value 立即就是预注入的数据，不会发起网络请求
```

如需设置过期时间（例如 Token 类数据），可显式传入 `cacheTime`：

```ts
await setRequestCache({
  key: 'access-token',
  data: tokenData,
  cacheTime: 30 * 60 * 1000, // 30 分钟后过期，触发重新请求
})
```

### 全局缓存管理 <Badge type="tip" text="v1.7.0+" />

`clearRequestCache` 和 `getRequestCache` 是独立的模块级函数，不依附于任何组件实例，可在路由守卫、登出逻辑、调试工具等场景中使用。

**登出时清空所有缓存：**

```ts
import { clearRequestCache } from 'xn-fe-tools'

async function logout() {
  await clearRequestCache() // 清除全部
  router.push('/login')
}
```

**只清除持久化存储（保留内存缓存）：**

```ts
await clearRequestCache({ storage: ['localStorage', 'sessionStorage'] })
```

**按业务模块前缀批量清除：**

```ts
// 不同模块使用不同 storageKeyPrefix
useRequestCache({ ..., storageKeyPrefix: 'order_' })
useRequestCache({ ..., storageKeyPrefix: 'user_' })

// 只清除订单模块的缓存
await clearRequestCache({ storageKeyPrefix: 'order_' })
```

**查看当前所有缓存状态（调试用）：**

```ts
import { getRequestCache } from 'xn-fe-tools'

const snapshots = await getRequestCache()
console.table(
  snapshots.map(s => ({
    key: s.key,
    storage: s.storage,
    isExpired: s.isExpired,
    age: `${Math.round((Date.now() - s.timestamp) / 1000)}s`,
  }))
)
```

**获取指定 key 的缓存并判断是否过期：**

```ts
const [snapshot] = await getRequestCache({ keys: ['user-profile'] })

if (snapshot?.isExpired) {
  console.log('缓存已过期，下次访问将重新请求')
}
```

> **注意**：全局函数只能感知**已注册的缓存**，即调用过 `run()` 且成功写入缓存的条目。组件卸载后未主动调用 `clearCache` 的条目仍保留在注册表中，但其 localStorage/sessionStorage 数据本身并不随组件生命周期消失（孤儿缓存）。孤儿缓存不会出现在 `getRequestCache` 返回列表中，但会在 `cacheTime` 到期后自然失效。

### 成功与失败的回调 (Dedupe 友好)

`useRequestCache` 支持在请求成功或失败时触发回调。

**特性**：当多个组件同时发起同一个 `cacheKey` 的请求时，物理上的网络请求只会发送一次（去重），但**每个调用处的 `onSuccess` 或 `onError` 回调都会被依次触发**。

这在处理列表中的重复项或全局并发任务时非常有用：

```ts
const list = [1, 1, 1, 1, 1] // 模拟五个相同的 ID

list.forEach((id, index) => {
  useRequestCache({
    requestFn: fetchUserInfo,
    params: { id },
    cacheKey: `user-${id}`,
    onSuccess: data => {
      // 虽然接口只请求了一次，但这里的日志会输出 5 次
      console.log(`第 ${index + 1} 次调用执行成功:`, data)
    },
    onError: err => {
      console.log(`第 ${index + 1} 次调用执行失败:`, err)
    },
  })
})
```
