Vanilla Signal Query 设计原理

设计原则

vanilla-signal-query 专注于 server state 管理。它处理 query 状态、缓存、失效和请求生命周期,不绑定 DOM 渲染,也不规定 UI 组织方式。

核心原则:

  • 数据层独立:query 不直接操作 DOM。
  • 状态可观察:每个 query 暴露响应式 state
  • 缓存可共享:相同 queryKey 可以复用缓存和 pending 请求。
  • 刷新可控:通过 staleTime、缓存适配器 ttlinvalidateQueries 控制数据新鲜度。
  • 请求安全:通过 AbortController、timeout、retry 和 request id 处理取消、超时、重试与竞态。

架构

当前项目由三层运行时组成:

  • createQuery(options):单个业务请求实例,负责解析 queryKey、维护状态、触发请求和暴露控制方法。
  • createQueryClient(options):请求协调器,负责 pending 去重、预取、失效、删除和事件通知。
  • 缓存适配器:负责把 query 记录保留在 memory、cookie、localStorage 或 indexedDB 中。

具体 HTTP 或异步数据请求位于 queryFn 中。vanilla-signal-query 调用 queryFn 并管理返回的数据;它不提供 base URL、headers、interceptor 或响应传输层能力。

默认提供一个共享 queryClient。如果需要隔离缓存,可以创建独立 client:

JavaScript
const client = createQueryClient({ cache: { adapter: 'memory', options: { maxSize: 300, ttl: 10 * 60_000, }, },});

createQuery 返回值

createQuery 返回一个函数:

JavaScript
const query = createQuery({ queryKey: ['products'], queryFn: fetchProducts,});query(); // 当前 dataquery.state; // 当前状态query.refetch(); // 控制方法

这种设计让读取数据保持简单,同时把状态和控制方法收敛在同一个 query 对象上。

状态模型

status 描述数据结果:

  • pending:还没有成功数据。
  • success:已有成功数据。
  • error:最近一次请求失败。

fetchStatus 描述请求过程:

  • idle:没有请求。
  • fetching:正在请求。

因此可以表达“已有数据但正在刷新”的状态:

JavaScript
query.state.status === 'success';query.state.fetchStatus === 'fetching';query.state.isStale === true;

常用布尔状态:

  • isPending:还没有成功数据。
  • isLoading:没有可展示数据,并且正在请求。
  • isFetching:当前正在请求。
  • isStale:当前数据已过期,或正在用旧数据等待新结果。
  • isSuccess:当前有成功数据。
  • isError:最近一次请求失败。
  • isPaused:query 当前未启用。

queryKey

queryKey 是请求身份,决定缓存、去重和失效。

推荐使用数组:

JavaScript
['products', { page: 1, keyword: 'phone' }];

对象字段会稳定排序,因此下面两个 key 等价:

JavaScript
['products', { page: 1, keyword: 'phone' }];['products', { keyword: 'phone', page: 1 }];

数组 key 也支持前缀失效:

JavaScript
queryClient.invalidateQueries(['products']);

可以命中:

JavaScript
['products', 1];['products', 2];['products', { keyword: 'phone' }];

缓存模型

缓存记录结构:

JavaScript
{ data, queryKey, updatedAt, staleTime, invalidated, meta,}

缓存新鲜度:

JavaScript
isStale = invalidated || Date.now() - updatedAt >= staleTime;

缓存保留由所选适配器管理:

  • cache.options.ttl 控制记录保留时间,所有适配器都支持。
  • cache.options.maxSize 控制 memory LRU 适配器的最大记录数。
  • staleTime 控制数据新鲜时间。

支持的适配器:

  • memory:默认适配器,底层使用 vanilla-lru
  • cookie:持久化适配器,底层使用 vanilla-create-storage
  • localStorage:持久化适配器,底层使用 vanilla-create-storage
  • indexedDB:持久化适配器,底层使用 vanilla-create-storage

fresh cache 命中时,query 直接使用缓存。stale cache 命中时,query 可以先展示旧数据,再发起后台刷新。

持久化适配器会维护 memory shadow cache,并从所选浏览器存储中 hydrate 记录。异步 query 执行可以等待 hydrate;同步缓存读取暴露当前 memory 中已有的视图。

请求模型

请求流程:

  1. 解析 queryKeyenabled
  2. 如果 fresh cache 命中,直接写入 state。
  3. 如果 stale cache 命中,先展示旧数据,再后台请求。
  4. 如果没有可展示数据,进入 loading。
  5. client 按 hash key 去重 pending 请求。
  6. 请求成功后执行 normalizeselect
  7. 写入 state 和 cache。
  8. 请求失败后按 retry 策略重试。
  9. 最终失败时写入 error state。
  10. 过期请求返回时通过 request id 忽略结果。

错误模型

默认 normalize 支持 { success, data, message, code } 风格响应。

JavaScript
{ success: true, data: [] }{ success: false, message: 'No access', code: 'NO_ACCESS' }

success: false 会转换为 BusinessError

也可以自定义 normalize

JavaScript
createQuery({ normalize(response) { if (response.errno !== 0) { throw new Error(response.errmsg); } return { data: response.result }; }, queryFn,});

扩展点

常用扩展方式:

  • queryClient.subscribe(listener):监听缓存和请求事件。
  • onSuccessonErroronSettled:监听单个 query 生命周期。
  • normalize:统一业务响应结构。
  • select:从响应数据派生最终数据。
  • 自定义 retryretryDelayshouldRetry:控制失败重试策略。
最后更新于 2026-09-23 14:51:52 UTC+8