@qiaopeng/tanstack-query-plus 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,9 +16,11 @@
16
16
  10. [第八步:智能预取](#10-第八步智能预取)
17
17
  11. [第九步:Suspense 模式](#11-第九步suspense-模式)
18
18
  12. [第十步:离线支持与持久化](#12-第十步离线支持与持久化)
19
- 13. [第十一步:焦点管理](#13-第十一步焦点管理)
20
- 14. [第十二步:工具函数与选择器](#14-第十二步工具函数与选择器)
21
- 15. [最佳实践与常见问题](#15-最佳实践与常见问题)
19
+ 13. [第十一步:数据防护与安全](#13-第十一步数据防护与安全)
20
+ 14. [第十二步:焦点管理](#14-第十二步焦点管理)
21
+ 15. [第十三步:工具函数与选择器](#15-第十三步工具函数与选择器)
22
+ 16. [最佳实践与常见问题](#16-最佳实践与常见问题)
23
+ 17. [API 索引](#17-api-索引)
22
24
 
23
25
  ---
24
26
 
@@ -42,6 +44,46 @@
42
44
 
43
45
  接下来,让我们一步步学习如何使用这些功能。
44
46
 
47
+ ### 1.1 设计初衷与原则
48
+
49
+ - 保持与 TanStack Query v5 完全兼容,不改变其核心行为,只做“安全增强”。
50
+ - 提供开箱即用的最佳实践配置,减少重复劳动与认知负担。
51
+ - 以“安全”为首要前提:数据防护、持久化安全、离线队列的稳健性、错误处理的可控性。
52
+ - API 设计坚持渐进增强:原生用法不变,增强能力按需启用,便于迁移和学习。
53
+ - TypeScript 友好:导出类型与范型参数与 TanStack 保持一致,避免类型陷阱。
54
+
55
+ ### 1.2 适用场景
56
+
57
+ - 中大型前端应用,需要统一的查询管理与最佳实践配置。
58
+ - 有离线需求(电商、文档编辑、移动端 Web)或需要缓存持久化与恢复的场景。
59
+ - 需要更强的乐观更新、并发冲突处理、数据一致性保障。
60
+ - 希望最小化自定义基础设施代码,将精力聚焦在业务逻辑。
61
+
62
+ ### 1.3 非目标与边界
63
+
64
+ - 不替代后端的并发控制与数据一致性保障;前端 Data Guard 仅作为“最后防线”。
65
+ - 不内置与具体后端协议的强绑定(如 GraphQL/REST 的特定实现);保持通用。
66
+ - 不存储任何敏感凭据;持久化仅针对查询缓存,且可配置与可关闭。
67
+
68
+ ### 1.4 安全与合规
69
+
70
+ - 持久化默认仅保存可序列化且成功的查询数据,避免异常对象导致恢复失败(参见 `createPersistOptions`)。
71
+ - 建议不要在 `queryKey` 中包含敏感信息(如 token、身份证号、手机号原文)。
72
+ - DevTools 仅在开发环境启用,避免在生产泄露内部状态(参见 `isDevToolsEnabled`)。
73
+ - 离线队列持久化时去除了函数体,仅存操作元数据;实际执行函数需通过注册表安全绑定。
74
+
75
+ ### 1.5 术语速览
76
+
77
+ - `queryKey`:查询的唯一标识;必须是稳定、可序列化的值(通常为数组)
78
+ - `queryFn`:实际获取数据的异步函数;返回 Promise
79
+ - `staleTime`:数据保持“新鲜”的时间窗口;新鲜期内不会重复请求
80
+ - `gcTime`:缓存保留时间;超过后缓存会被清理
81
+ - `invalidate`:标记查询为过期;下一次渲染或焦点恢复时会重新请求
82
+ - `refetch`:主动重新请求数据
83
+ - `persist`:将查询缓存持久化到存储(localStorage/IndexedDB)并在刷新后恢复
84
+ - `offline`:网络不可用时的状态;本库提供队列与自动恢复机制
85
+ - `optimistic update`:先更新 UI,再与服务端同步;失败时需回滚
86
+ - `Data Guard`:防止旧数据覆盖新数据的前端机制(版本/时间戳/哈希比对)
45
87
  ---
46
88
 
47
89
  ## 2. 安装与环境准备
@@ -69,6 +111,9 @@ npm install @tanstack/react-query-devtools
69
111
 
70
112
  # 视口预取功能(如果需要 useInViewPrefetch)
71
113
  npm install react-intersection-observer
114
+
115
+ # 路由预取示例所需(如果使用 useRoutePrefetch 示例中的 Link/useNavigate)
116
+ npm install react-router-dom
72
117
  ```
73
118
 
74
119
  ### 2.3 环境要求
@@ -80,11 +125,104 @@ npm install react-intersection-observer
80
125
 
81
126
  现在环境准备好了,让我们开始配置应用。
82
127
 
128
+ ### 2.4 学习路径与检查清单
129
+
130
+ 严格建议按照以下顺序学习与落地,并在每一步完成后进行自检:
131
+
132
+ 1. 安装依赖:确保安装本库及 peer 依赖(`@tanstack/react-query`、`react`、`react-dom`),按需安装 `devtools`、`react-intersection-observer`、`react-router-dom`
133
+ 2. 创建 `QueryClient`:使用 `GLOBAL_QUERY_CONFIG`,避免随意调整 `retry`、`staleTime` 造成请求风暴
134
+ 3. 包裹应用:使用 `PersistQueryClientProvider` 开启持久化与离线支持(生产环境建议保留持久化)
135
+ 4. 添加 DevTools(开发环境):`isDevToolsEnabled()` 控制显示,严禁在生产强制开启
136
+ 5. 发起首个查询:优先使用 `useEnhancedQuery`,在慢查询或错误场景验证日志输出
137
+ 6. 增强 Mutation:在列表 CRUD 场景启用乐观更新,并验证回滚路径与错误处理
138
+ 7. 离线与持久化:断网测试页面行为;验证缓存恢复与离线队列的稳健性
139
+ 8. 数据防护(可选但推荐):开启 Data Guard 的版本/时间戳/哈希策略,防止旧数据覆盖
140
+ 9. 焦点管理:按照业务需要控制窗口聚焦时的刷新频率,避免抖动
141
+ 10. 预取:根据网络情况与页面流量,按需启用悬停/视口/路由/空闲预取,避免过度预取
142
+
143
+ 完成以上 10 点后,再进入“最佳实践与常见问题”章节进行整体检查与性能、安全优化。
144
+
145
+ ### 2.5 五分钟上手示例
146
+
147
+ 目标:用 5 分钟完成“配置 Provider + 首个查询 + DevTools 调试”。
148
+
149
+ 1. 安装依赖:
150
+
151
+ ```bash
152
+ npm install @qiaopeng/tanstack-query-plus @tanstack/react-query @tanstack/react-query-persist-client
153
+ npm install @tanstack/react-query-devtools --save-dev
154
+ ```
155
+
156
+ 2. 创建 Provider:
157
+
158
+ ```tsx
159
+ // main.tsx
160
+ import { QueryClient, PersistQueryClientProvider } from '@qiaopeng/tanstack-query-plus'
161
+ import { GLOBAL_QUERY_CONFIG } from '@qiaopeng/tanstack-query-plus/core'
162
+ import { ReactQueryDevtools, isDevToolsEnabled } from '@qiaopeng/tanstack-query-plus/core/devtools'
163
+
164
+ const queryClient = new QueryClient({ defaultOptions: GLOBAL_QUERY_CONFIG })
165
+
166
+ function Providers({ children }) {
167
+ return (
168
+ <PersistQueryClientProvider client={queryClient}>
169
+ {children}
170
+ {isDevToolsEnabled() && <ReactQueryDevtools initialIsOpen={false} />}
171
+ </PersistQueryClientProvider>
172
+ )
173
+ }
174
+ ```
175
+
176
+ 3. 发起首个查询:
177
+
178
+ ```tsx
179
+ // App.tsx
180
+ import { useEnhancedQuery } from '@qiaopeng/tanstack-query-plus/hooks'
181
+
182
+ export default function App() {
183
+ const { data, isLoading, isError } = useEnhancedQuery({
184
+ queryKey: ['hello'],
185
+ queryFn: async () => ({ message: 'Hello Query Plus' }),
186
+ })
187
+ if (isLoading) return <div>加载中...</div>
188
+ if (isError) return <div>加载失败</div>
189
+ return <div>{data.message}</div>
190
+ }
191
+ ```
192
+
193
+ 4. 跑起来:在浏览器中打开 DevTools 面板,查看 `['hello']` 查询状态。
194
+
195
+ ### 2.6 TypeScript 配置建议
196
+
197
+ 以下 `tsconfig.json` 选项可以帮助初学者避免常见类型问题:
198
+
199
+ ```json
200
+ {
201
+ "compilerOptions": {
202
+ "target": "ES2020",
203
+ "module": "ESNext",
204
+ "moduleResolution": "Node",
205
+ "jsx": "react-jsx",
206
+ "strict": true,
207
+ "skipLibCheck": true,
208
+ "esModuleInterop": true,
209
+ "forceConsistentCasingInFileNames": true,
210
+ "resolveJsonModule": true,
211
+ "isolatedModules": true,
212
+ "useUnknownInCatchVariables": true
213
+ }
214
+ }
215
+ ```
216
+
217
+ 说明:
218
+ - `strict: true` 有助于暴露隐含的 `any` 与未处理的 `undefined`
219
+ - `skipLibCheck: true` 可避免第三方库类型检查的噪音(对本库安全)
220
+ - `useUnknownInCatchVariables` 提醒你显式处理错误类型
221
+
83
222
  ---
84
223
 
85
224
  ## 3. 第一步:配置 Provider
86
225
 
87
-
88
226
  任何使用 TanStack Query 的应用都需要一个 Provider 来提供 QueryClient 实例。本库提供了一个增强版的 Provider,让配置变得更简单。
89
227
 
90
228
  ### 3.1 最简配置
@@ -819,33 +957,43 @@ mutation.mutate({ newTitle: '新标题' })
819
957
 
820
958
  ### 7.5 条件性乐观更新
821
959
 
822
- 有时候只想在特定条件下执行乐观更新:
960
+ 本库未提供单独的 `useConditionalOptimisticMutation`。如需按条件启用乐观更新,使用以下两种安全模式:
961
+
962
+ 1. 使用两个 mutation,根据条件选择调用哪个(最清晰、类型安全):
823
963
 
824
964
  ```tsx
825
- import { useConditionalOptimisticMutation } from '@qiaopeng/tanstack-query-plus/hooks'
965
+ import { useMutation } from '@qiaopeng/tanstack-query-plus/hooks'
826
966
 
827
- const mutation = useConditionalOptimisticMutation(
828
- // 第一个参数:mutation 函数
829
- updateTodo,
830
- // 第二个参数:条件函数,只有返回 true 时才执行乐观更新
831
- (variables) => variables.priority === 'high',
832
- // 第三个参数:配置选项
833
- {
834
- mutationKey: ['updateTodo'], // 可选的 mutation key
835
- optimistic: {
836
- queryKey: ['todos'],
837
- updater: (oldTodos, updatedTodo) =>
838
- oldTodos?.map(t => t.id === updatedTodo.id ? { ...t, ...updatedTodo } : t)
839
- },
840
- onSuccess: () => {
841
- console.log('更新成功')
842
- }
967
+ const optimisticUpdate = useMutation({
968
+ mutationFn: updateTodo,
969
+ optimistic: {
970
+ queryKey: ['todos'],
971
+ updater: (oldTodos, updatedTodo) => oldTodos?.map(t => t.id === updatedTodo.id ? { ...t, ...updatedTodo } : t)
843
972
  }
844
- )
973
+ })
845
974
 
846
- // 使用
847
- mutation.mutate({ id: '1', title: '新标题', priority: 'high' }) // 会乐观更新
848
- mutation.mutate({ id: '2', title: '新标题', priority: 'low' }) // 不会乐观更新
975
+ const plainUpdate = useMutation({ mutationFn: updateTodo })
976
+
977
+ function save(todo) {
978
+ const shouldOptimistic = todo.priority === 'high'
979
+ const runner = shouldOptimistic ? optimisticUpdate : plainUpdate
980
+ runner.mutate(todo)
981
+ }
982
+ ```
983
+
984
+ 2. 基于状态切换 `optimistic.enabled`(适合全局开关):
985
+
986
+ ```tsx
987
+ // 以应用自身配置或组件状态为准(此处仅示例)
988
+ const enableOptimistic = true
989
+ const mutation = useMutation({
990
+ mutationFn: updateTodo,
991
+ optimistic: {
992
+ queryKey: ['todos'],
993
+ enabled: enableOptimistic,
994
+ updater: (oldTodos, updatedTodo) => oldTodos?.map(t => t.id === updatedTodo.id ? { ...t, ...updatedTodo } : t)
995
+ }
996
+ })
849
997
  ```
850
998
 
851
999
  ### 7.6 列表操作的简化 Mutation
@@ -897,20 +1045,38 @@ function TodoList() {
897
1045
 
898
1046
  ### 7.7 批量 Mutation
899
1047
 
900
- 处理批量操作:
1048
+ 本库未提供 `useBatchMutation`。进行批量操作时,推荐两种模式:
1049
+
1050
+ 1. 在一个 mutation 中封装批量逻辑(一次请求或并发 Promise):
901
1051
 
902
1052
  ```tsx
903
- import { useBatchMutation } from '@qiaopeng/tanstack-query-plus/hooks'
1053
+ import { useMutation } from '@qiaopeng/tanstack-query-plus/hooks'
904
1054
 
905
- const batchMutation = useBatchMutation(
906
- async (todoIds) => {
907
- // 批量删除
908
- return Promise.all(todoIds.map(id => api.deleteTodo(id)))
1055
+ const batchDelete = useMutation({
1056
+ mutationFn: async (ids: string[]) => {
1057
+ return Promise.all(ids.map(id => api.deleteTodo(id)))
1058
+ },
1059
+ optimistic: {
1060
+ queryKey: ['todos'],
1061
+ updater: (old, ids: string[]) => old?.filter(t => !ids.includes(String(t.id)))
909
1062
  }
910
- )
1063
+ })
911
1064
 
912
1065
  // 使用
913
- batchMutation.mutate(['id1', 'id2', 'id3'])
1066
+ batchDelete.mutate(['id1', 'id2', 'id3'])
1067
+ ```
1068
+
1069
+ 2. 使用离线队列在恢复网络后批量执行(稳健且可持久化):
1070
+
1071
+ ```tsx
1072
+ import { createOfflineQueueManager, mutationRegistry } from '@qiaopeng/tanstack-query-plus/features'
1073
+
1074
+ const queue = createOfflineQueueManager({ storageKey: 'todo-ops', concurrency: 3 })
1075
+
1076
+ function registerDelete(id: string) {
1077
+ mutationRegistry.register(['todos','delete',id].join('-'), () => api.deleteTodo(id))
1078
+ queue.add({ mutationKey: ['todos','delete',id], mutationFn: () => api.deleteTodo(id), priority: 1 })
1079
+ }
914
1080
  ```
915
1081
 
916
1082
  ### 7.8 乐观更新工具函数
@@ -968,7 +1134,7 @@ const list7 = conditionalUpdateItems(
968
1134
  )
969
1135
  ```
970
1136
 
971
- ### 7.9 完整示例:Todo 应用
1137
+ ### 7.9 完整示例:Todo 应用
972
1138
 
973
1139
  ```tsx
974
1140
  import { useEnhancedQuery, useMutation } from '@qiaopeng/tanstack-query-plus/hooks'
@@ -1042,6 +1208,13 @@ function TodoApp() {
1042
1208
  }
1043
1209
  ```
1044
1210
 
1211
+ ### 7.10 安全提示
1212
+
1213
+ - 明确回滚路径:在 `onError` 或 `rollback` 中恢复缓存或触发重新拉取
1214
+ - 稳定的 `queryKey`:使用 Key 工厂,避免结构漂移导致更新不到位
1215
+ - 变量安全:Mutation 变量不包含敏感信息(如 token),错误上报需脱敏
1216
+ - 冲突处理:对 409 触发家族失效与 UI 提示;对 500 展示兜底提示并记录错误
1217
+
1045
1218
  现在你已经掌握了数据变更和乐观更新。接下来,让我们学习如何处理无限滚动和分页场景。
1046
1219
 
1047
1220
  ---
@@ -2023,6 +2196,14 @@ function ProductBrowser() {
2023
2196
 
2024
2197
  现在你已经掌握了预取策略。接下来,让我们学习 Suspense 模式,它可以让你的代码更简洁。
2025
2198
 
2199
+ ### 10.12 安全提示
2200
+
2201
+ - 结合网络状况:使用 `useSmartPrefetch` 在慢网络禁用预取,避免拥塞
2202
+ - 控制频率与间隔:为悬停/路由预取设置 `minInterval`,避免重复请求
2203
+ - 严格限定 Key:预取目标必须是稳定且可序列化的 `queryKey`
2204
+ - 避免敏感信息:不要将敏感数据拼入 `queryKey`
2205
+ - 可回收:在复杂页面中适时清理预取历史(`clearPrefetchHistory`)以避免状态膨胀
2206
+
2026
2207
  ---
2027
2208
 
2028
2209
  ## 11. 第九步:Suspense 模式
@@ -2498,9 +2679,11 @@ async function handleUpdateUser(userData) {
2498
2679
  } else {
2499
2680
  // 在线时直接执行
2500
2681
  await updateUserAPI(userData)
2501
- }
2682
+ }
2502
2683
  }
2503
2684
 
2685
+ ### 队列管理器操作参考
2686
+
2504
2687
  // 获取队列状态
2505
2688
  const state = queueManager.getState()
2506
2689
  console.log({
@@ -2644,14 +2827,118 @@ async function checkAndMigrate() {
2644
2827
 
2645
2828
  现在你已经掌握了离线支持。接下来,让我们学习焦点管理,它可以优化用户切换标签页时的体验。
2646
2829
 
2830
+ ### 12.9 安全提示与 SSR 注意
2831
+
2832
+ - 持久化范围:仅持久化成功且可序列化的查询数据(默认行为),避免异常对象恢复失败
2833
+ - 敏感信息:严禁将敏感凭据放入缓存数据或 `queryKey`
2834
+ - 离线队列:仅持久化操作元信息,不持久化函数体;真实执行需通过 `mutationRegistry` 注册,避免函数闭包泄露
2835
+ - 存储容量:`checkStorageSize` 超过 5MB 建议迁移到 IndexedDB;定期 `clearExpiredCache`
2836
+ - SSR 注意:服务端渲染环境下 `PersistQueryClientProvider` 会自动降级为普通 Provider,浏览器 API 均有守卫,不会在 Node 环境访问 `window`
2837
+
2838
+ ---
2839
+
2840
+ ## 13. 第十一步:数据防护与安全
2841
+
2842
+ 在多人协作或高并发的应用中,数据一致性是一个常见挑战。例如:
2843
+ - **旧数据覆盖新数据**:用户 A 打开页面,用户 B 更新了数据,用户 A 保存时可能会覆盖 B 的更改。
2844
+ - **乐观更新冲突**:前端乐观更新了数据,但后端因为版本冲突拒绝了请求。
2845
+ - **竞态条件**:网络请求乱序返回,导致旧数据覆盖了新数据。
2846
+
2847
+ 本库提供了 **Data Guard** 机制来解决这些问题。
2848
+
2849
+ ### 13.1 查询数据防护
2850
+
2851
+ 使用 `useDataGuardQueryConfig` 可以自动检测并拒绝过期的服务端数据:
2852
+
2853
+ ```tsx
2854
+ import { useEnhancedQuery, useDataGuardQueryConfig } from '@qiaopeng/tanstack-query-plus/hooks'
2855
+
2856
+ function ProductList() {
2857
+ // 自动包装查询配置
2858
+ const config = useDataGuardQueryConfig(
2859
+ ['products'],
2860
+ fetchProducts,
2861
+ {
2862
+ // 策略配置
2863
+ maxDataAge: 5000, // 允许的最大数据时差
2864
+ onStaleDataDetected: ({ reason, cached, rejected }) => {
2865
+ console.warn(`拒绝了旧数据: ${reason}`)
2866
+ // cached: 当前缓存的较新数据
2867
+ // rejected: 被拒绝的服务端旧数据
2868
+ }
2869
+ }
2870
+ )
2871
+
2872
+ const { data } = useEnhancedQuery(config)
2873
+
2874
+ return <div>...</div>
2875
+ }
2876
+ ```
2877
+
2878
+ **防护策略优先级**:
2879
+ 1. **版本号 (Version)**: 如果数据包含 `version` 字段,严格比较版本号。
2880
+ 2. **时间戳 (Timestamp)**: 如果数据包含 `updatedAt` 字段,比较更新时间。
2881
+ 3. **哈希 (Hash)**: 如果都没有,计算内容哈希,拒绝相同内容的重复更新。
2882
+
2883
+ ### 13.2 变更数据防护
2884
+
2885
+ 使用 `useDataGuardMutation` 处理并发更新冲突:
2886
+
2887
+ ```tsx
2888
+ import { useDataGuardMutation } from '@qiaopeng/tanstack-query-plus/hooks'
2889
+
2890
+ function EditProduct({ product }) {
2891
+ const mutation = useDataGuardMutation(
2892
+ (updates) => api.updateProduct(updates),
2893
+ ['products'], // 相关的查询 key
2894
+ {
2895
+ // 发生冲突时的回调 (HTTP 409)
2896
+ onConflict: (error) => {
2897
+ toast.error('检测到数据冲突,页面将自动刷新')
2898
+ },
2899
+ // 内置乐观更新支持
2900
+ optimistic: {
2901
+ queryKey: ['products'],
2902
+ updater: (old, newProduct) => {
2903
+ // Data Guard 会自动处理版本号递增和时间戳更新
2904
+ return { ...old, ...newProduct }
2905
+ }
2906
+ }
2907
+ }
2908
+ )
2909
+
2910
+ const handleSave = (updates) => {
2911
+ // 提交时带上当前版本号
2912
+ mutation.mutate({
2913
+ ...updates,
2914
+ version: product.version
2915
+ })
2916
+ }
2917
+ }
2918
+ ```
2919
+
2920
+ **`useDataGuardMutation` 的特性**:
2921
+ - **自动冲突检测**:捕获 409 Conflict 错误并触发回调。
2922
+ - **智能乐观更新**:自动递增本地数据的版本号,防止 UI 闪烁。
2923
+ - **家族元数据同步**:更新成功后,自动同步相关查询元数据。
2924
+ - **自动失效**:发生冲突时,自动失效相关缓存以获取最新数据。
2925
+
2926
+ ### 13.3 安全提示
2927
+
2928
+ - 统一数据版本:后端需提供 `version` 或 `updatedAt` 字段,前端 Data Guard 才能更有效地比对
2929
+ - 明确冲突策略:将 409 视为冲突并提示用户刷新,避免静默覆盖
2930
+ - 审计与日志:为冲突与拒绝旧数据的情况打点,便于后续排查
2931
+ - 仅在必要处开启 Data Guard:对只读数据可关闭防护以减少开销
2932
+ - 家族同步慎用:确保变体的 `queryKey` 有共同前缀,避免误伤无关查询
2933
+
2647
2934
  ---
2648
2935
 
2649
- ## 13. 第十一步:焦点管理
2936
+ ## 14. 第十二步:焦点管理
2650
2937
 
2651
2938
 
2652
2939
  当用户切换浏览器标签页或窗口时,TanStack Query 默认会在窗口重新获得焦点时刷新数据。本库提供了更精细的焦点管理功能。
2653
2940
 
2654
- ### 13.1 获取焦点状态
2941
+ ### 14.1 获取焦点状态
2655
2942
 
2656
2943
  ```tsx
2657
2944
  import { useFocusState, usePageVisibility } from '@qiaopeng/tanstack-query-plus/hooks'
@@ -2669,7 +2956,7 @@ function FocusIndicator() {
2669
2956
  }
2670
2957
  ```
2671
2958
 
2672
- ### 13.2 焦点恢复时刷新指定查询
2959
+ ### 14.2 焦点恢复时刷新指定查询
2673
2960
 
2674
2961
  默认情况下,所有查询都会在窗口聚焦时刷新。但有时你只想刷新特定的查询:
2675
2962
 
@@ -2691,7 +2978,7 @@ function Dashboard() {
2691
2978
  }
2692
2979
  ```
2693
2980
 
2694
- ### 13.3 焦点恢复时执行回调
2981
+ ### 14.3 焦点恢复时执行回调
2695
2982
 
2696
2983
  ```tsx
2697
2984
  import { useFocusCallback } from '@qiaopeng/tanstack-query-plus/hooks'
@@ -2712,7 +2999,7 @@ function AnalyticsTracker() {
2712
2999
  }
2713
3000
  ```
2714
3001
 
2715
- ### 13.4 条件性焦点刷新
3002
+ ### 14.4 条件性焦点刷新
2716
3003
 
2717
3004
  只在满足条件时刷新:
2718
3005
 
@@ -2731,7 +3018,7 @@ function ChatRoom({ roomId, isActive }) {
2731
3018
  }
2732
3019
  ```
2733
3020
 
2734
- ### 13.5 暂停焦点管理
3021
+ ### 14.5 暂停焦点管理
2735
3022
 
2736
3023
  在某些场景下(如模态框打开时),你可能想暂停焦点刷新:
2737
3024
 
@@ -2769,7 +3056,7 @@ function VideoPlayer() {
2769
3056
  }
2770
3057
  ```
2771
3058
 
2772
- ### 13.6 智能焦点管理器
3059
+ ### 14.6 智能焦点管理器
2773
3060
 
2774
3061
  获取焦点管理的统计信息:
2775
3062
 
@@ -2796,7 +3083,7 @@ function FocusDebugPanel() {
2796
3083
  }
2797
3084
  ```
2798
3085
 
2799
- ### 13.7 焦点管理最佳实践
3086
+ ### 14.7 焦点管理最佳实践
2800
3087
 
2801
3088
  1. **设置 minInterval**:避免用户频繁切换标签页时过度刷新
2802
3089
  2. **选择性刷新**:不是所有数据都需要在焦点恢复时刷新
@@ -2807,12 +3094,12 @@ function FocusDebugPanel() {
2807
3094
 
2808
3095
  ---
2809
3096
 
2810
- ## 14. 第十二步:工具函数与选择器
3097
+ ## 15. 第十三步:工具函数与选择器
2811
3098
 
2812
3099
 
2813
3100
  本库提供了丰富的工具函数,帮助你更高效地处理数据。
2814
3101
 
2815
- ### 14.1 选择器(Selectors)
3102
+ ### 15.1 选择器(Selectors)
2816
3103
 
2817
3104
  选择器用于 `select` 选项,可以在数据返回后进行转换。注意:大部分选择器是高阶函数,需要先调用生成实际的选择器函数。
2818
3105
 
@@ -2875,7 +3162,7 @@ const { data: userBasicInfo } = useQuery({
2875
3162
  })
2876
3163
  ```
2877
3164
 
2878
- ### 14.2 组合选择器
3165
+ ### 15.2 组合选择器
2879
3166
 
2880
3167
  选择器可以组合使用:
2881
3168
 
@@ -2903,7 +3190,7 @@ const { data: adminEmails } = useQuery({
2903
3190
  })
2904
3191
  ```
2905
3192
 
2906
- ### 14.3 独立使用选择器函数
3193
+ ### 15.3 独立使用选择器函数
2907
3194
 
2908
3195
  选择器也可以独立使用。注意:这些函数大多是高阶函数,需要先传入参数生成选择器,再传入数据:
2909
3196
 
@@ -2949,7 +3236,7 @@ const activeNamesSelector = compose(
2949
3236
  const activeNames = activeNamesSelector(users) // ['Alice', 'Charlie']
2950
3237
  ```
2951
3238
 
2952
- ### 14.4 列表更新工具
3239
+ ### 15.4 列表更新工具
2953
3240
 
2954
3241
  用于乐观更新的列表操作:
2955
3242
 
@@ -2989,7 +3276,7 @@ const batchRemoved = batchRemoveItems(todos, ['1', '3'])
2989
3276
  const reordered = reorderItems(todos, 0, 2)
2990
3277
  ```
2991
3278
 
2992
- ### 14.5 创建乐观更新配置
3279
+ ### 15.5 创建乐观更新配置
2993
3280
 
2994
3281
  快速创建常用的乐观更新配置:
2995
3282
 
@@ -3019,7 +3306,7 @@ const addMutation = useMutation({
3019
3306
  })
3020
3307
  ```
3021
3308
 
3022
- ### 14.6 Query Key 工具
3309
+ ### 15.6 Query Key 工具
3023
3310
 
3024
3311
  ```tsx
3025
3312
  import {
@@ -3071,7 +3358,7 @@ const normalized = normalizeQueryParams(
3071
3358
  ) // { page: 1, sort: 'name' }
3072
3359
  ```
3073
3360
 
3074
- ### 14.7 网络工具
3361
+ ### 15.7 网络工具
3075
3362
 
3076
3363
  ```tsx
3077
3364
  import {
@@ -3105,7 +3392,7 @@ const info = getNetworkInfo()
3105
3392
  // }
3106
3393
  ```
3107
3394
 
3108
- ### 14.8 存储工具
3395
+ ### 15.8 存储工具
3109
3396
 
3110
3397
  ```tsx
3111
3398
  import {
@@ -3139,7 +3426,7 @@ cloned.nested.value = 2
3139
3426
  console.log(original.nested.value) // 1(原始数据不变)
3140
3427
  ```
3141
3428
 
3142
- ### 14.9 字段映射工具
3429
+ ### 15.9 字段映射工具
3143
3430
 
3144
3431
  ```tsx
3145
3432
  import {
@@ -3176,7 +3463,7 @@ const newTodo = {
3176
3463
 
3177
3464
  **注意**:`createFieldEnricher` 是一个高级函数,用于根据配置数据丰富查询结果中的字段(如将 ID 映射为名称),需要配合 QueryClient 使用,适用于特定的业务场景。
3178
3465
 
3179
- ### 14.10 保持上一次数据
3466
+ ### 15.10 保持上一次数据
3180
3467
 
3181
3468
  在数据刷新时保持显示上一次的数据:
3182
3469
 
@@ -3200,7 +3487,7 @@ function SearchResults({ query }) {
3200
3487
  }
3201
3488
  ```
3202
3489
 
3203
- ### 14.11 家族一致性工具
3490
+ ### 15.11 家族一致性工具
3204
3491
 
3205
3492
  在某些高级场景下,你可能需要自行枚举并同步同一资源的家族查询变体(分页/筛选/排序等)。本库提供了工具函数用于匹配与安全同步:
3206
3493
 
@@ -3234,7 +3521,7 @@ function useManualFamilySync() {
3234
3521
 
3235
3522
  ---
3236
3523
 
3237
- ## 15. 最佳实践与常见问题
3524
+ ## 16. 最佳实践与常见问题
3238
3525
 
3239
3526
  ### 导入路径速查表
3240
3527
 
@@ -3264,7 +3551,7 @@ function useManualFamilySync() {
3264
3551
  - 如果需要 TanStack Query 的原生 `useQuery`(而非增强版),从 `@tanstack/react-query` 导入
3265
3552
  - 子路径导入可以实现更好的 tree-shaking
3266
3553
 
3267
- ### 15.1 项目结构建议
3554
+ ### 16.1 项目结构建议
3268
3555
 
3269
3556
  ```
3270
3557
  src/
@@ -3286,7 +3573,7 @@ src/
3286
3573
  └── App.tsx
3287
3574
  ```
3288
3575
 
3289
- ### 15.2 封装自定义 Hooks
3576
+ ### 16.2 封装自定义 Hooks
3290
3577
 
3291
3578
  将查询逻辑封装成自定义 hooks:
3292
3579
 
@@ -3321,7 +3608,7 @@ function UserProfile({ userId }) {
3321
3608
  }
3322
3609
  ```
3323
3610
 
3324
- ### 15.3 配置最佳实践
3611
+ ### 16.3 配置最佳实践
3325
3612
 
3326
3613
  ```tsx
3327
3614
  // config/queryClient.ts
@@ -3345,7 +3632,7 @@ export const queryClient = new QueryClient({
3345
3632
  })
3346
3633
  ```
3347
3634
 
3348
- ### 15.4 错误处理最佳实践
3635
+ ### 16.4 错误处理最佳实践
3349
3636
 
3350
3637
  ```tsx
3351
3638
  // 全局错误处理
@@ -3374,7 +3661,7 @@ const queryClient = new QueryClient({
3374
3661
  })
3375
3662
  ```
3376
3663
 
3377
- ### 15.5 TypeScript 类型最佳实践
3664
+ ### 16.5 TypeScript 类型最佳实践
3378
3665
 
3379
3666
  ```tsx
3380
3667
  import type {
@@ -3415,7 +3702,7 @@ function useUpdateUser() {
3415
3702
  }
3416
3703
  ```
3417
3704
 
3418
- ### 15.6 常见问题解答
3705
+ ### 16.6 常见问题解答
3419
3706
 
3420
3707
  #### Q: DevTools 报错 "Module not found"
3421
3708
 
@@ -3523,7 +3810,7 @@ useEnhancedQuery({
3523
3810
  ```
3524
3811
  3. 检查 queryKey 是否正确(使用 key 工厂避免拼写错误)
3525
3812
 
3526
- ### 15.7 性能优化建议
3813
+ ### 16.7 性能优化建议
3527
3814
 
3528
3815
  1. **合理设置 staleTime**:避免不必要的重复请求
3529
3816
  2. **使用 select**:只选择需要的数据,减少重渲染
@@ -3532,9 +3819,9 @@ useEnhancedQuery({
3532
3819
  5. **懒加载**:结合 Suspense 和代码分割
3533
3820
  6. **避免过度乐观更新**:只在必要时使用
3534
3821
 
3535
- ### 15.8 安全建议
3822
+ ### 16.8 安全建议
3536
3823
 
3537
- 1. **不要在 queryKey 中包含敏感信息**:queryKey 可能被记录或暴露
3824
+ 1. **不要在 queryKey 中包含敏感信息**:queryKey 可能被记录或暴露
3538
3825
  2. **验证服务端响应**:不要盲目信任 API 返回的数据
3539
3826
  3. **处理认证过期**:在全局错误处理中处理 401 错误
3540
3827
  4. **清理敏感缓存**:用户登出时清除缓存
@@ -3554,8 +3841,9 @@ useEnhancedQuery({
3554
3841
  7. ✅ 智能预取策略
3555
3842
  8. ✅ Suspense 模式
3556
3843
  9. ✅ 离线支持和持久化
3557
- 10. ✅ 焦点管理
3558
- 11. ✅ 工具函数和选择器
3844
+ 10. ✅ 数据防护与安全
3845
+ 11. ✅ 焦点管理
3846
+ 12. ✅ 工具函数和选择器
3559
3847
 
3560
3848
  ### 下一步
3561
3849
 
@@ -3564,3 +3852,90 @@ useEnhancedQuery({
3564
3852
  - 在 [Issues](https://github.com/qiaopengg/qiaopeng-tanstack-query-plus/issues) 中提问或反馈
3565
3853
 
3566
3854
  祝你编码愉快!🚀
3855
+ ### 16.9 类型与错误处理规范
3856
+
3857
+ - 明确类型参数:在增强 hooks 中显式标注 `TData` 与 `TError`,避免 `any` 漏出
3858
+ - 统一错误模型:为后端错误定义统一类型(如 `ApiError`),在 UI 层集中处理
3859
+ - 不吞错误:日志与监控采集应在开发开启,生产使用采样与脱敏
3860
+ - 优先 `isError` 分支渲染兜底组件,避免在 `data` 为 `undefined` 时解构引发异常
3861
+ - 乐观更新的回滚必须可重入:错误重试不应导致状态错乱
3862
+
3863
+ ### 16.10 SSR 注意事项
3864
+
3865
+ - Provider 降级:SSR 环境自动使用普通 `QueryClientProvider`,浏览器 API 有环境守卫
3866
+ - 数据注入:如需 SSR 注水,建议结合 TanStack 原生脱水/注水(本库不强绑)
3867
+ - 路由预取:服务端不执行预取相关浏览器 API,需在客户端挂载后进行
3868
+
3869
+ ### 16.11 Tree-shaking 与导入路径
3870
+
3871
+ - 使用子路径导入(如 `@qiaopeng/tanstack-query-plus/hooks`、`/core`、`/utils`),提升摇树效果
3872
+ - 保持副作用为零:本包 `sideEffects: false`,按需导入可以减少体积
3873
+ - 避免从根入口导入全部模块:仅在需要时按子路径引入具体能力
3874
+
3875
+ ### 16.12 起步排障清单
3876
+
3877
+ - DevTools 未显示:安装并从 `@qiaopeng/tanstack-query-plus/core/devtools` 导入,仅在开发环境显示(`core/devtools.ts:28`)
3878
+ - 视口预取报错:安装 `react-intersection-observer` 并从 `hooks/inview` 子路径导入(`src/hooks/useInViewPrefetch.ts`)
3879
+ - 路由预取报错:安装 `react-router-dom`,示例仅依赖其 Link/useNavigate
3880
+ - 缓存未恢复:检查 `enablePersistence` 与 `cacheKey`;确认浏览器支持 localStorage(`features/persistence.ts:43`)
3881
+ - 离线队列不执行:确保在恢复网络后调用 `createOfflineQueueManager`,并通过 `mutationRegistry` 注册函数(`features/offline.ts:30`)
3882
+ - 乐观更新错乱:确保 `queryKey` 稳定、列表更新器使用 `id` 对齐(`utils/optimisticUtils.ts:14`)
3883
+ - 冲突未处理:确认后端返回 409 或约定错误码,前端使用 `useDataGuardMutation` 捕获(`hooks/useDataGuardMutation.ts:80`)
3884
+ - SSR 环境错误:确保 Provider 自动降级,不在服务端访问 `window`(`PersistQueryClientProvider.tsx:34`)
3885
+
3886
+ ### 16.13 生产前检查清单
3887
+
3888
+ - DevTools:生产环境关闭(`isDevToolsEnabled()` 为 false)
3889
+ - 错误处理:所有查询与变更都有兜底 UI 与日志记录
3890
+ - 缓存策略:为高频接口设置合理 `staleTime`,避免重复请求
3891
+ - 乐观更新:具备回滚与重试策略;冲突触发家族失效
3892
+ - 持久化:确认缓存大小与迁移策略(localStorage → IndexedDB)
3893
+ - 安全审查:queryKey 与缓存不包含敏感信息;队列不持久化函数体
3894
+ - 监控:慢查询上报、错误采样与脱敏处理
3895
+
3896
+ ## 17. API 索引
3897
+
3898
+ 为方便查找,这里列出各子路径的主要导出与用途:
3899
+
3900
+ - `@qiaopeng/tanstack-query-plus`(顶层)
3901
+ - `PersistQueryClientProvider`、`usePersistenceStatus`、`usePersistenceManager`
3902
+ - `QueryClient`、`QueryClientProvider`、`useQueryClient`、`skipToken`、`useIsMutating`(直接再导出原生 API)
3903
+
3904
+ - `@qiaopeng/tanstack-query-plus/core`
3905
+ - 配置:`GLOBAL_QUERY_CONFIG`、`createCustomConfig`、`DEFAULT_STALE_TIME`、`DEFAULT_GC_TIME`
3906
+ - 重试:`defaultQueryRetryStrategy`、`defaultMutationRetryStrategy`、`exponentialBackoff`
3907
+ - 环境:`isDev`、`isProd`、`isTest`
3908
+ - DevTools:`ReactQueryDevtools`、`isDevToolsEnabled`、`createDevToolsConfig`(src/core/devtools.ts:28)
3909
+ - 焦点管理:`focusManager`、`getSmartFocusManager`、`pauseFocusManager`、`resumeFocusManager`
3910
+ - Key 工具:`queryKeys`、`normalizeQueryKey`、`createDomainKeyFactory`、`createMutationKeyFactory`
3911
+ - Query 配置:`createAppQueryOptions`、`createListQueryOptions`
3912
+
3913
+ - `@qiaopeng/tanstack-query-plus/hooks`
3914
+ - 查询:`useEnhancedQuery`、`useEnhancedSuspenseQuery`、`useEnhancedInfiniteQuery`
3915
+ - 批量查询:`useEnhancedQueries`、`useAutoRefreshBatchQueries`、`useDashboardQueries`
3916
+ - Mutation:`useMutation`、`useListMutation`、`setupMutationDefaults`
3917
+ - 预取:`useHoverPrefetch`、`useInViewPrefetch`(子路径 `hooks/inview`)、`useRoutePrefetch`、`useSmartPrefetch`、`useConditionalPrefetch`、`useIdlePrefetch`、`usePeriodicPrefetch`、`usePredictivePrefetch`、`usePriorityPrefetch`
3918
+ - 焦点:`useFocusState`、`useFocusRefetch`、`useConditionalFocusRefetch`、`usePauseFocus`、`useSmartFocusManager`
3919
+ - 数据防护:`useDataGuardQueryConfig`、`useDataGuardMutation`
3920
+
3921
+ - `@qiaopeng/tanstack-query-plus/features`
3922
+ - 离线:`setupOnlineManager`、`isOnline`、`createOfflineQueueManager`、`OfflineQueueManager`、`mutationRegistry`、`subscribeToOnlineStatus`
3923
+ - 持久化:`createPersistOptions`、`createPersister`、`clearCache`、`clearExpiredCache`、`checkStorageSize`、`getStorageStats`、`migrateToIndexedDB`
3924
+
3925
+ - `@qiaopeng/tanstack-query-plus/components`
3926
+ - Loading:`DefaultLoadingFallback`、`FullScreenLoading`、`ListSkeletonFallback`、`PageSkeletonFallback`、`SmallLoadingIndicator`、`TextSkeletonFallback`
3927
+ - 错误边界:`QueryErrorBoundary`
3928
+ - Suspense:`SuspenseWrapper`、`QuerySuspenseWrapper`
3929
+
3930
+ - `@qiaopeng/tanstack-query-plus/utils`
3931
+ - 选择器:`selectById`、`selectFields`、`compose` 等
3932
+ - 列表/乐观工具:`listUpdater`、`createAddItemConfig`、`createUpdateItemConfig`、`reorderItems`
3933
+ - 预取管理器:`getPrefetchManager`、`SmartPrefetchManager`、`resetPrefetchManager`
3934
+ - Query Key 工厂:`createQueryKeyFactory`、`normalizeQueryParams`
3935
+ - 存储与网络:`isStorageAvailable`、`getNetworkSpeed`、`isSlowNetwork`
3936
+ - 数据防护:`applyDataGuard`、`updateFamilyMetadata`
3937
+
3938
+ - `@qiaopeng/tanstack-query-plus/react-query`
3939
+ - 原生 API 再导出:`useQuery`、`useMutation`、`useInfiniteQuery`、`useSuspenseQuery` 等(src/react-query/index.ts:1)
3940
+
3941
+ 提示:完整导出列表可在 `package.json:33` 的 `exports` 字段中查看;顶层入口再导出常用原生 API,子路径按模块分层导出,便于 tree-shaking。
package/dist/index.d.ts CHANGED
@@ -4,5 +4,5 @@ export * from "./hooks/index.js";
4
4
  export { PersistQueryClientProvider, usePersistenceStatus, usePersistenceManager, type PersistQueryClientProviderProps } from "./PersistQueryClientProvider.js";
5
5
  export * from "./types/index.js";
6
6
  export * from "./utils/index.js";
7
- export { QueryClient, QueryClientProvider, skipToken, useQueryClient, useIsMutating } from "@tanstack/react-query";
7
+ export { QueryClient, QueryClientProvider, skipToken, useQueryClient, useIsMutating, type UseMutationOptions, type UseQueryOptions } from "@tanstack/react-query";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,KAAK,+BAA+B,EAAE,MAAM,iCAAiC,CAAC;AAChK,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,SAAS,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,KAAK,+BAA+B,EAAE,MAAM,iCAAiC,CAAC;AAChK,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,SAAS,EAAE,cAAc,EAAE,aAAa,EAAE,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qiaopeng/tanstack-query-plus",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Enhanced TanStack Query toolkit: defaults, hooks, persistence, offline, data guard, utils",
5
5
  "author": "qiaopeng",
6
6
  "license": "MIT",