TS 客户端使用
生成的 TS 客户端是前后端唯一契约:类型直达、错误全部归一为 result<T>、拦截器必经、流式带重连。本章是它的完整使用手册。
创建与基础调用
ts
import { api } from './api/ts/fun'
const c = api.create('/api') // 实际请求 POST ${url}/cell
const r = await c.orderSvc.get({ id: 1 })
if (r.status === 0) {
console.log(r.data!.id, r.data!.amount) // 类型直达,键全部 camelCase
} else if (r.status === 2) {
console.error('业务错误', r.code, r.msg)
}api.create(url)是聚合入口,服务属性首字母小写;- 响应键已被服务端递归转为首字母小写,直接按 camelCase 取值;
- 超过
2^53-1的大整数自动解析为BigInt(雪花 ID 无损)。
state:会话与请求标识
ts
c.setState({ token: 'eyJhbGci...' }) // 全局:每个请求自动带上
// 或每调用覆盖 / 追加
const r = await c.orderSvc.get({ id: 1 }, { state: { requestId: crypto.randomUUID() } })state 是每请求的字符串字典:服务端在 Ctx.State 读到,响应拦截器经 context.requestState 读回(只读快照)。
拦截器
ts
// 请求拦截器:可写 state(如注入会话纪元、请求标识)
c.addRequestInterceptor((serviceName, methodName, state, dto) => {
state.epoch = String(myEpoch())
})
// 响应拦截器:所有结果必经,可替换返回值
c.addResponseInterceptor((serviceName, methodName, result, context) => {
// context.requestState:发起请求时的 state 只读快照
// context.response?: Response —— 原生 Response(头、状态码可读)
if (result.status === 2 && result.code === 4011) {
location.href = '/login'
}
})- 请求拦截器抛异常 → 归一为
status=1的 Result; - 响应拦截器返回新 Result 可替换结果(用于统一错误翻译、重试包装);
- 不存在绕过响应拦截器的错误路径:网络失败、HTTP 错误、HTML 响应、非法 JSON、调用方取消、拦截器自身异常——全部归一为
result并依次经过每个响应拦截器。
错误归一:resultStatus
ts
export type resultStatus = 0 | 1 | 2 | 4 | 5
// 0 success; 1 framework/client protocol error; 2 business error;
// 4 external request error; 5 external timeout| status | 场景 | 处理建议 |
|---|---|---|
0 | 成功 | 取 data |
1 | 框架/协议错误(非法 JSON、意外 HTML——多半是网关拦截页) | 提示重试,上报 |
2 | 业务错误(fun.Error) | 按 code 分支,展示 msg |
4 | 外部请求失败或调用方取消 | 检查网络;取消场景静默 |
5 | 外部超时 | 提示超时 |
永远 await 一个 result,不会有未捕获异常从请求本身抛出——try/catch 只需要包你自己的回调逻辑。
超时与取消
框架自身不设任何超时定时器,超时由调用方用 AbortSignal 表达:
ts
const r = await c.orderSvc.get(
{ id: 1 },
{ signal: AbortSignal.timeout(5000) } // 超时 → status=5
)
const ctrl = new AbortController()
button.onclick = () => ctrl.abort() // 取消 → status=4
const r2 = await c.orderSvc.get({ id: 2 }, { signal: ctrl.signal })ts
export type RequestOptions = { signal?: AbortSignal; state?: Record<string, string> }
export type StreamOptions = { signal?: AbortSignal; state?: Record<string, string>; retry?...; idleTimeoutMs?... }流式调用
ts
const r = await c.chatSvc.chat(
{ prompt: '讲个故事' },
(msg) => appendToUi(msg) // 每行一个 JSON,逐行回调
)
if (r.status !== 0) { /* 流中途失败也归一为 result */ }(T, *Stream, error) 签名的方法:首条消息同样进 onMessage 回调,业务无需区分。
断线重连(v1.3.6)
ts
// dto 传工厂函数:每次重连重新求值——刷新续传游标的关键
c.chatSvc.chat(
() => ({ prompt: '讲个故事', cursor: getCursor() }),
(msg) => appendToUi(msg),
{
retry: {
maxAttempts: 5, // 默认 Infinity
baseDelayMs: 500, // 首次重连延迟
maxDelayMs: 10000, // 退避上限;实际延迟 = 指数退避 × 0.75~1.25 抖动
},
idleTimeoutMs: 60_000, // 空闲看门狗:超时未收到任何字节(含心跳)判定流死亡并重连
}
)retry默认关闭;只有传输失败(status 4/5)会重连,业务错误(status 2)不重试;idleTimeoutMs默认关闭(0 显式关闭);配合服务端 25s 心跳,正常保活的流不会误判;- dto 工厂每次重连重新求值,把「续传游标」放进去即可实现断点续传。
联调:Vite 代理
js
// vite.config.js
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
},
},
}api.create('/api') → 实际 POST /api/cell → 代理转后端 /cell。浏览器直连场景改用服务端 CORS。