Skip to content

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.Errorcode 分支,展示 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

下一步

基于 MIT 许可发布