fix(common): 对齐 @szyx-mobile useRequest 行为并修复 POST body 丢失
- 修复 useRequest POST/PUT/UPLOAD/POSTFORM 请求忽略 fetchAsync data 的问题 - RequestOptions 增加 tag/log 字段,与旧项目兼容 - 导出 ValidationError,供宿主二次封装使用 instanceof 判断 - usePageApi 支持 list 与 records 两种分页字段 - 升级 devDependencies 版本(axios 1.18.0、zod 3.25.76 等) Co-Authored-By: Claude <noreply@anthropic.com>
这个提交包含在:
父节点
1f72b415dc
当前提交
1143224194
@ -26,11 +26,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.2.14",
|
||||||
"@react-native-async-storage/async-storage": "^2.1.2",
|
"@react-native-async-storage/async-storage": "^3.1.1",
|
||||||
"axios": "^1.7.0",
|
"axios": "^1.18.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.2.0",
|
||||||
"react-native": "^0.85.0",
|
"react-native": "^0.85.3",
|
||||||
"zod": "^3.23.0"
|
"zod": "^3.25.76"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export { useRequest } from './useRequest'
|
export { useRequest, ValidationError } from './useRequest'
|
||||||
export { useApi } from './useApi'
|
export { useApi } from './useApi'
|
||||||
export { usePageApi } from './usePageApi'
|
export { usePageApi } from './usePageApi'
|
||||||
export { RequestError } from './errors'
|
export { RequestError } from './errors'
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
} from 'axios'
|
} from 'axios'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { RequestError } from './errors'
|
import { RequestError } from './errors'
|
||||||
import { type RequestOptions, useRequest } from './useRequest'
|
import { type RequestOptions, useRequest, ValidationError } from './useRequest'
|
||||||
|
|
||||||
const SUCCESS_STATUS = /^0$/
|
const SUCCESS_STATUS = /^0$/
|
||||||
|
|
||||||
@ -20,21 +20,6 @@ type ApiResponse<T> = {
|
|||||||
|
|
||||||
export type ApiMethod = 'GET' | 'POSTJSON' | 'POSTFORM' | 'PUT' | 'UPLOAD'
|
export type ApiMethod = 'GET' | 'POSTJSON' | 'POSTFORM' | 'PUT' | 'UPLOAD'
|
||||||
|
|
||||||
// ValidationError shape (duck-typed to avoid circular dependency)
|
|
||||||
interface HasIssues {
|
|
||||||
issues: Array<{ path: (string | number)[]; message: string }>
|
|
||||||
response?: { data?: { message?: string } }
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidationError(e: unknown): e is HasIssues {
|
|
||||||
return (
|
|
||||||
e != null &&
|
|
||||||
typeof e === 'object' &&
|
|
||||||
'issues' in e &&
|
|
||||||
Array.isArray((e as { issues: unknown }).issues)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useApi = <
|
export const useApi = <
|
||||||
S extends z.ZodTypeAny,
|
S extends z.ZodTypeAny,
|
||||||
T extends z.infer<S> = z.infer<S>,
|
T extends z.infer<S> = z.infer<S>,
|
||||||
@ -97,7 +82,12 @@ export const useApi = <
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const responseInterceptor = responseInterceptors.use(
|
const responseInterceptor = responseInterceptors.use(
|
||||||
nextResponse => nextResponse,
|
r => {
|
||||||
|
if (options?.log) {
|
||||||
|
console.debug(JSON.stringify(r, null, 2))
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
},
|
||||||
(thrownError: unknown) => {
|
(thrownError: unknown) => {
|
||||||
if (isCancel(thrownError)) {
|
if (isCancel(thrownError)) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
@ -105,45 +95,68 @@ export const useApi = <
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isValidationError(thrownError)) {
|
if (thrownError instanceof ValidationError) {
|
||||||
if (thrownError.issues.length <= 0) {
|
const e = thrownError
|
||||||
|
console.error(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
name: e.name,
|
||||||
|
status: e.response.status,
|
||||||
|
data: e.response.data,
|
||||||
|
config: e.response.config,
|
||||||
|
headers: e.response.headers,
|
||||||
|
issues: e.issues,
|
||||||
|
url: `${e.response.config.baseURL}${e.response.config.url}`,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (e.issues.length <= 0) {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new RequestError('数据格式校验错误', 'ValidationError', thrownError),
|
new RequestError('数据格式校验错误', 'ValidationError', e),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstIssue = thrownError.issues[0]
|
const firstIssue = e.issues[0]
|
||||||
if (firstIssue.path.length > 0 && firstIssue.path[0] === 'status') {
|
if (firstIssue.path.length > 0 && firstIssue.path[0] === 'status') {
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new RequestError(
|
new RequestError(
|
||||||
thrownError.response?.data?.message ?? '接口返回失败',
|
e.response.data?.message ?? '接口返回失败',
|
||||||
'ValidationError',
|
'ValidationError',
|
||||||
thrownError,
|
e,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new RequestError(firstIssue.message, 'ValidationError', thrownError),
|
new RequestError(firstIssue.message, 'ValidationError', e),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAxiosError(thrownError)) {
|
if (isAxiosError(thrownError)) {
|
||||||
|
console.error(
|
||||||
|
JSON.stringify(thrownError, null, 2),
|
||||||
|
`${thrownError.config?.baseURL}${thrownError.config?.url}`,
|
||||||
|
)
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new RequestError('网络请求失败', 'AxiosError', thrownError),
|
new RequestError('网络请求失败', 'AxiosError', thrownError),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
JSON.stringify(thrownError, null, 2),
|
||||||
|
)
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
new RequestError('网络请求失败', 'OtherError', thrownError),
|
new RequestError('网络请求失败', 'OtherError', thrownError),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
responseInterceptors.eject(responseInterceptor)
|
responseInterceptors.eject(responseInterceptor)
|
||||||
}
|
}
|
||||||
}, [responseInterceptors])
|
}, [options?.log, responseInterceptors])
|
||||||
|
|
||||||
const transformedFetchAsync = useCallback(
|
const transformedFetchAsync = useCallback(
|
||||||
async (nextConfig?: AxiosRequestConfig<D>) => {
|
async (nextConfig?: AxiosRequestConfig<D>) => {
|
||||||
|
|||||||
@ -7,7 +7,8 @@ import { z } from 'zod'
|
|||||||
import { useApi } from './useApi'
|
import { useApi } from './useApi'
|
||||||
|
|
||||||
type List<T> = {
|
type List<T> = {
|
||||||
records: T[]
|
records?: T[]
|
||||||
|
list?: T[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Root<T> = T[]
|
type Root<T> = T[]
|
||||||
@ -41,7 +42,8 @@ export const usePageApi = <
|
|||||||
const [status, setStatus] = useState<'loaded' | 'reload' | 'loadmore'>('loaded')
|
const [status, setStatus] = useState<'loaded' | 'reload' | 'loadmore'>('loaded')
|
||||||
|
|
||||||
const listSchema: z.ZodSchema<List<T>> = z.object({
|
const listSchema: z.ZodSchema<List<T>> = z.object({
|
||||||
records: z.array(validationSchema ?? z.any()),
|
records: z.array(validationSchema ?? z.any()).optional(),
|
||||||
|
list: z.array(validationSchema ?? z.any()).optional(),
|
||||||
})
|
})
|
||||||
const rootSchema: z.ZodSchema<Root<T>> = z.array(validationSchema ?? z.any())
|
const rootSchema: z.ZodSchema<Root<T>> = z.array(validationSchema ?? z.any())
|
||||||
|
|
||||||
@ -58,7 +60,12 @@ export const usePageApi = <
|
|||||||
status === 'loadmore'
|
status === 'loadmore'
|
||||||
? Math.ceil((data ?? []).length / (options?.pageSize ?? 10)) + 1
|
? Math.ceil((data ?? []).length / (options?.pageSize ?? 10)) + 1
|
||||||
: 1,
|
: 1,
|
||||||
|
pageNum:
|
||||||
|
status === 'loadmore'
|
||||||
|
? Math.ceil((data ?? []).length / (options?.pageSize ?? 10)) + 1
|
||||||
|
: 1,
|
||||||
size: options?.pageSize ?? 10,
|
size: options?.pageSize ?? 10,
|
||||||
|
pageSize: options?.pageSize ?? 10,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
...params,
|
...params,
|
||||||
@ -90,9 +97,11 @@ export const usePageApi = <
|
|||||||
case 'root':
|
case 'root':
|
||||||
setData(result as Root<T>)
|
setData(result as Root<T>)
|
||||||
break
|
break
|
||||||
default:
|
default: {
|
||||||
setData((result as List<T>).records)
|
const listResult = result as List<T>
|
||||||
|
setData(listResult.records ?? listResult.list)
|
||||||
break
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setStatus('loaded')
|
setStatus('loaded')
|
||||||
return
|
return
|
||||||
@ -102,12 +111,14 @@ export const usePageApi = <
|
|||||||
case 'root':
|
case 'root':
|
||||||
setData(previous => [...(previous ?? []), ...(result as Root<T>)])
|
setData(previous => [...(previous ?? []), ...(result as Root<T>)])
|
||||||
break
|
break
|
||||||
default:
|
default: {
|
||||||
|
const listResult = result as List<T>
|
||||||
setData(previous => [
|
setData(previous => [
|
||||||
...(previous ?? []),
|
...(previous ?? []),
|
||||||
...(result as List<T>).records,
|
...(listResult.records ?? listResult.list ?? []),
|
||||||
])
|
])
|
||||||
break
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setStatus('loaded')
|
setStatus('loaded')
|
||||||
})
|
})
|
||||||
|
|||||||
@ -18,11 +18,15 @@ export interface RequestOptions {
|
|||||||
manual?: boolean
|
manual?: boolean
|
||||||
/** 延迟多少毫秒后才将 loading 置为 true(默认 0)。*/
|
/** 延迟多少毫秒后才将 loading 置为 true(默认 0)。*/
|
||||||
loadingDelay?: number
|
loadingDelay?: number
|
||||||
|
/** 请求标签,用于日志或错误追踪。*/
|
||||||
|
tag?: string
|
||||||
|
/** true = 在响应拦截器中打印完整响应。*/
|
||||||
|
log?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 内部 ValidationError(对齐 @szyx-mobile/use-axios 的 ValidationError)────
|
// ─── 内部 ValidationError(对齐 @szyx-mobile/use-axios 的 ValidationError)────
|
||||||
|
|
||||||
class ValidationError<T = unknown, D = unknown> extends z.ZodError {
|
export class ValidationError<T = unknown, D = unknown> extends z.ZodError {
|
||||||
constructor(
|
constructor(
|
||||||
issues: z.ZodIssue[],
|
issues: z.ZodIssue[],
|
||||||
public readonly response: AxiosResponse<T, D>,
|
public readonly response: AxiosResponse<T, D>,
|
||||||
@ -127,6 +131,28 @@ export function useRequest<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeRequestData(base: unknown, override: unknown): unknown {
|
||||||
|
if (override === undefined || override === null) {
|
||||||
|
return base
|
||||||
|
}
|
||||||
|
|
||||||
|
if (base === undefined || base === null) {
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof base === 'object' &&
|
||||||
|
typeof override === 'object' &&
|
||||||
|
!(override instanceof FormData) &&
|
||||||
|
!(override instanceof ArrayBuffer) &&
|
||||||
|
!Array.isArray(override)
|
||||||
|
) {
|
||||||
|
return {...base as Record<string, unknown>, ...override as Record<string, unknown>}
|
||||||
|
}
|
||||||
|
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
|
||||||
const fetchAsync = useCallback(
|
const fetchAsync = useCallback(
|
||||||
async (overrideConfig?: AxiosRequestConfig<D>): Promise<R> => {
|
async (overrideConfig?: AxiosRequestConfig<D>): Promise<R> => {
|
||||||
const loadingDelay = optionsRef.current?.loadingDelay ?? 0
|
const loadingDelay = optionsRef.current?.loadingDelay ?? 0
|
||||||
@ -152,8 +178,12 @@ export function useRequest<
|
|||||||
...overrideConfig,
|
...overrideConfig,
|
||||||
url,
|
url,
|
||||||
method: axiosMethod,
|
method: axiosMethod,
|
||||||
params: isGet ? params : (overrideConfig?.params ?? config?.params),
|
params: isGet
|
||||||
data: isGet ? (overrideConfig?.data ?? config?.data) : params,
|
? (overrideConfig?.params ?? config?.params ?? params)
|
||||||
|
: (overrideConfig?.params ?? config?.params),
|
||||||
|
data: isGet
|
||||||
|
? (overrideConfig?.data ?? config?.data)
|
||||||
|
: mergeRequestData(config?.data, overrideConfig?.data ?? params),
|
||||||
headers: {
|
headers: {
|
||||||
...methodHeaders,
|
...methodHeaders,
|
||||||
...(config?.headers as Record<string, string> | undefined),
|
...(config?.headers as Record<string, string> | undefined),
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export {
|
|||||||
useRequest,
|
useRequest,
|
||||||
useApi,
|
useApi,
|
||||||
usePageApi,
|
usePageApi,
|
||||||
|
ValidationError,
|
||||||
} from './api'
|
} from './api'
|
||||||
export type { RequestOptions as UseRequestOptions, ApiMethod } from './api'
|
export type { RequestOptions as UseRequestOptions, ApiMethod } from './api'
|
||||||
export type { PageOptions } from './api'
|
export type { PageOptions } from './api'
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户