@hootool/http-client
v1.0.3
Published
hootool http-client
Readme
@hootool/http-client
Koa 风格中间件的 fetch 客户端,可选接入 TanStack Query 与 WebSocket。
安装
pnpm add @hootool/http-client使用 react-query / hook 时再装 peer:
pnpm add @tanstack/react-query react| 入口 | 用途 |
|---|---|
| @hootool/http-client | createHttp、中间件、ApiError |
| @hootool/http-client/react-query | createFetcher / createMutationFn |
| @hootool/http-client/websocket | WebSocketService |
| @hootool/http-client/use-web-socket | useWebSocket |
HttpClient
import { createHttp, ApiError, TimeoutError } from '@hootool/http-client';
const http = createHttp({
baseURL: '/api',
timeout: 10_000,
headers: { 'X-App': 'web' },
});
const list = await http.get<{ items: string[] }>('/users', { page: 1, ids: [1, 2] });
await http.post('/users', { name: 'sai' });
await http.put('/user/1', { name: 'sai' });
await http.patch('/user/1', { name: 'sai' });
await http.delete('/item', { id: 1 });
await http.head('/item', { id: 1 });
const file = await http.get<Blob>('/report.pdf', undefined, { responseType: 'blob' });
const child = http.create({ baseURL: '/upload' });get/delete/head:(path, params?, options?)post/put/patch:(path, data?, options?);FormData原样发送timeout为0或不传表示不限制(仍可用signal);超时抛TimeoutErrorhttp.create()派生实例,浅拷贝 defaults 与中间件
responseType
| 值 | 行为 |
|---|---|
| auto(默认) | 按 Content-Type:json → json();无 CT 先试 JSON,失败当文本;text/* / xml → text;其余 → blob |
| json | 强制 response.json() |
| text / blob / arrayBuffer / formData | 对应原生解析 |
中间件
洋葱模型,先注册的更靠外。metadata 只给中间件 / 业务用,不进 fetch。
http.use(async (ctx, next) => {
ctx.metadata.traceId = 'x';
ctx.init.headers = { ...ctx.init.headers, Authorization: 'Bearer token' };
await next();
console.log(ctx.data, ctx.json); // json 为对象或数组
});扩展 metadata 类型:
declare module '@hootool/http-client' {
interface RequestMetadata {
traceId?: string;
}
}错误
HTTP 非 2xx 抛 ApiError,只带 status 与 statusText,不解析错误 body。业务信封(200 + 业务码)用 createBizCodeMiddleware:
import {
createHttp,
ApiError,
TimeoutError,
BizError,
createBizCodeMiddleware,
createErrorMiddleware,
isAbortWithoutTimeout,
} from '@hootool/http-client';
const http = createHttp({
middlewares: [
createErrorMiddleware({
handleError: (error) => {
if (error instanceof ApiError && error.status === 401) logout();
},
showError: (error, ctx) => {
if (isAbortWithoutTimeout(error, ctx)) return;
if (error instanceof TimeoutError) toast('超时');
else if (error instanceof ApiError) toast(`网络错误 ${error.status}`);
else if (error instanceof BizError) toast(error.message);
},
}),
createBizCodeMiddleware({
codeField: 'code', // 默认
messageField: 'message', // 默认
successCode: ['0', '0000'],
}),
],
});
// 关闭本次校验
http.get('/export', undefined, { metadata: { successCode: false } });
// 覆盖本次成功码(会强转 string)
http.get('/special', undefined, { metadata: { successCode: [1, '00'] } });默认读 code / message 并强转 string,成功码默认 0。成功时返回完整 JSON,不拆 data。需要内层数据时用 transform 或自己取。
createErrorMiddleware 须放在最外。handleError 每次失败都会调(401 登出等);showError 受中间件 defaultShowError 与 metadata.showError 控制。metadata.showError 可为 false / true,或传入函数覆盖全局提示。取消是否提示由业务用 isAbortWithoutTimeout 自行判断。超时是 TimeoutError,isAbortWithoutTimeout 为 false。React Query 的 CancelledError 在 query 层,不走这条。
createErrorMiddleware({ defaultShowError: false, showError }); // 默认不提示,handleError 仍会调
http.get('/poll', undefined, { metadata: { showError: false } }); // 本次不提示
http.get('/pay', undefined, { metadata: { showError: true } }); // 本次强制用全局 showError
http.get('/upload', undefined, { metadata: { showError: (e) => toast(String(e)) } }); // 本次自定义提示try {
await http.get('/users');
} catch (e) {
if (e instanceof ApiError) console.log(e.status);
if (e instanceof TimeoutError) console.log(e.timeout);
if (e instanceof BizError) console.log(e.code, e.message);
}react-query
import { createReactQueryProvider } from '@hootool/http-client/react-query';
const { createFetcher, createMutationFn, runMutation, toQueryFn } = createReactQueryProvider({
request: (path, options) => http.request(path, options),
queryClient,
});
const getUser = createFetcher<User, { id: string }>('/users/:id');
const createUser = createMutationFn<User, { name: string }>('/users', { method: 'POST' });
useQuery({ queryKey: ['user', id], queryFn: toQueryFn(getUser, { id }) });
await runMutation(createUser, { name: 'sai' });- path 支持
:id/{id},用过的 key 从 variables 里剔除,不再进 query/body - 函数 path 也要返回带占位的字符串(
'/users/:id'),自己拼/users/${id}不会剥id - GET / DELETE / OPTIONS 的 variables →
params,其余 →data - 可在 fetcher 上写默认
params/data transform:(json, variables) => TData
无限列表:
const listItems = createFetcher<Item[], { keyword?: string }, number>('/items', {
params: { pageSize: 20 },
mapPageParam: (page) => ({ params: { page } }),
});合并顺序(后者覆盖):options.params/data → meta.reqOpt → variables → mapPageParam。
单次覆盖走 TanStack meta.reqOpt,HTTP metadata 挂在 reqOpt.metadata:
runMutation(createUser, { name: 'sai' }, {
reqOpt: { timeout: 5000, metadata: { traceId: 'x' } },
});mutationFn(variables) 可不传第二参。
WebSocket
消息约定:{ event, data } JSON;心跳为文本 ping / pong。
import { WebSocketService } from '@hootool/http-client/websocket';
type Events = {
chat: { text: string };
online: { count: number };
};
const ws = new WebSocketService<Events>({
pingInterval: 15_000,
serverTimeout: 8_000,
maxReconnectAttempts: 8,
maxReconnectDelay: 15_000,
});
ws.connect('wss://example.com/ws');
ws.subscribe('chat', (msg) => console.log(msg.text));
await ws.send({ event: 'chat', data: { text: 'hi' } });
ws.disconnect(); // 会清掉排队中的重连React:
import { useWebSocket, createUseWebSocket } from '@hootool/http-client/use-web-socket';
const { sendMessage } = useWebSocket(ws, 'chat', (msg) => {
console.log(msg.text);
});
const useAppWS = createUseWebSocket(ws);
useAppWS('chat', (msg) => console.log(msg));onMessage 换引用不会反复订阅。
License
MIT
