Skip to content

简介与设计原则 ​

@snail-js/api 是一个 TypeScript 优先的 HTTP 客户端:用装饰器描述请求,用声明表达类型, 用插件承载可选能力,而传输层完全交给 axios。

ts
import { Api, Get, Params, Server, SnailServer } from "@snail-js/api";

@Server({ baseURL: "/api", timeout: 10000 })
class BackEnd extends SnailServer {}
export const Service = new BackEnd();

@Api("/user")
class UserApi {
  @Get("/:id")
  getUser(@Params("id") id: string): Promise<{ id: number; name: string }> {
    return null!;
  }
}

export const userApi = Service.createApi(UserApi);
ts
const { data } = await userApi.getUser("42").send();
//      ^? { id: number; name: string }

五条设计原则 ​

1. 核心只拥有三样东西 ​

核心拥有:装饰器写入的元数据、请求管线、插件生命周期。除此之外什么都没有。 缓存、版本管理、拦截器、zod 校验、JSON→class 转换全部是插件,构建在 第三方作者拿到的同一套公开 API 之上。内置插件没有特权通道:如果某个内置插件需要一个 还不存在的钩子,那个钩子会被加进公开契约,而不是在内部偷偷实现。

框架适配器是这条原则唯一的例外:它不是插件,而是 @Server({ stateAdapter }) 选项 —— 一次声明同时决定 method.meta 上的句柄与每个 use* 策略返回的状态,见 框架适配器。

2. 装饰器只写元数据,方法体永不执行 ​

ts
@Api("/user")
class UserApi {
  @Get("/:id")
  getUser(@Params("id") id: string): Promise<User> {
    return null!; // 永远不会运行
  }
}

被请求方式装饰器标记的方法不会被调用。createApi 把它换成一个返回请求对象的工厂,方法体 只剩两个职责:声明参数类型和返回类型。因此 return null!; 是约定写法(只写 return null! 也可以,什么都不声明则返回类型视为 unknown)。

3. 可选能力皆插件,注册即校验 ​

ts
Service.use(Interceptor()).use(Versioning({ defaultVersion: "1.0.0" }));

use() 是同步且可链式的。它在当场校验插件名、重复注册和 dependsOn,失败时立刻抛出 SnailPluginError —— 而不是等到第一个请求才崩。插件的 install 可以是异步的,那样它会在 第一次请求前被 pluginManager.ready 统一等待。

4. 一个方向不够,所以有两个 ​

链式钩子分两个方向执行:

阶段方向原因
beforeRequest、requestInterceptor 等正向(优先级高者先)拦截器(100)必须在缓存(-100)计算缓存键之前改写请求
afterResponse、responseInterceptor 等反向(优先级低者先)收拢洋葱:离网络最近的插件先看到响应

一条规则概括:正向钩子从最外层进,反向钩子从最内层出。

5. 没有 reflect-metadata ​

类型元数据不是这个库的输入:装饰器只读写自己写入的元数据,从来不需要推断构造参数类型。 因此内置的元数据仓库(WeakMap<owner, Map<slot, Map<key, value>>>)完全够用,而 reflect-metadata 带来的只有一个必须在应用入口 import 的 polyfill。

在 TypeScript 7 里 emitDecoratorMetadata 已经不再真的产出 design:* 元数据,这个取舍 只是把一件本来就没在用的事说清楚。详见 TypeScript 配置。

与「自己封装一层 axios」的对比 ​

关注点手写 axios 封装@snail-js/api
请求声明手写函数 + 拼字符串装饰器声明路径与参数,:id 占位符自动 URL 编码
参数归位每个函数自己组 params / data / headers@Params() / @Query() / @Data() / @HeaderValue() 统一归位,顺序与优先级固定
响应解包每处 res.data.datasend() 解析 { code, message, data },data 已拆包
业务码校验每个调用点 if 判断一条 validateCode 规则,拒绝即抛 SnailResponseError
类型手动标注泛型由方法声明的返回类型推断 data 类型
横切能力层层手写函数包装插件 + 两条方向的链式钩子,注册顺序可预测
错误直接抛 AxiosError稳定 code 的错误层次,取消被单独区分为 SnailCancelledError
实时通信自己写 SSE / WebSocket 重连@Sse / @WebSocket / @HttpStream + 退避重连
打包体积取决于你写了多少核心从包根导入,可选能力从子路径按需导入

它不试图取代 axios

axios 的 AxiosRequestConfig 是请求方式装饰器第二个参数的基类型:超时、responseType、 adapter、withCredentials、signal …… 任何 axios 支持的字段都能按方法设置。 这个库只负责「怎么描述、怎么编排、怎么解包」。

适合什么、不适合什么 ​

适合:中大型前端应用;后端统一返回 { code, message, data } 类信封;需要缓存、版本、 拦截等横切能力并且希望它们可插拔;团队已经用装饰器(NestJS 风格)思考问题。

不适合:只发一两个请求的脚本(直接用 axios 更省事);后端返回结构完全无规律的接口; 需要在同一处混用多种 HTTP 库的场景。

接下来 ​

基于 MIT 许可发布