Skip to content

CRUD 接口类 ​

一个覆盖增删改查的 api 类,加上调用侧的各种用法:列表分页、路径参数、静态与动态请求头、 错误分级、取消。

1. server ​

ts
// src/service.ts
import { Server, SnailServer } from "@snail-js/api";

@Server({
  baseURL: "/api",
  timeout: 10000,
  codeKey: "code",
  messageKey: "message",
  dataKey: "data",
  validateCode: (code) => code === 0 || code === 200
})
class BackEnd extends SnailServer {}

export const Service = new BackEnd();

2. api 类 ​

ts
// src/api/article.api.ts
import {
  Api,
  Data,
  Delete,
  Get,
  Header,
  HeaderValue,
  Params,
  Patch,
  Post,
  Put,
  Query
} from "@snail-js/api";
import { Service } from "../service";

export interface Article {
  id: number;
  title: string;
  body: string;
  tags: string[];
  updatedAt: string;
}

export interface ArticleQuery {
  page: number;
  size: number;
  keyword?: string;
  tag?: string;
}

export interface Page<T> {
  total: number;
  items: T[];
}

export type ArticleDraft = Pick<Article, "title" | "body" | "tags">;

@Api("/article")
@Header({ "x-client": "web" })                 // 类级静态头
class ArticleApi {
  /** 列表:无 key 的 @Query 展开整个对象 */
  @Get("/")
  list(@Query() query: ArticleQuery): Promise<Page<Article>> {
    return null!;
  }

  /** 详情:路径占位符 */
  @Get("/:id")
  detail(@Params("id") id: number): Promise<Article> {
    return null!;
  }

  /** 新建:静态体之外再补一个字段 */
  @Post("/", { data: { published: false } })
  create(@Data() draft: ArticleDraft): Promise<Article> {
    return null!;
  }

  /** 整体更新 */
  @Put("/:id")
  replace(
    @Params("id") id: number,
    @Data() draft: ArticleDraft,
    @HeaderValue("if-match") etag: string
  ): Promise<Article> {
    return null!;
  }

  /** 局部更新:keyed @Data 只改一个字段 */
  @Patch("/:id")
  rename(@Params("id") id: number, @Data("title") title: string): Promise<Article> {
    return null!;
  }

  /** 删除:后端返回 data: null */
  @Delete("/:id")
  remove(@Params("id") id: number): Promise<null> {
    return null!;
  }

  /** 导出:非 JSON 响应直接穿透 */
  @Get("/export", { responseType: "blob", timeout: 60000 })
  exportAll(@Query() query: Omit<ArticleQuery, "page" | "size">): Promise<Blob> {
    return null!;
  }
}

export const articleApi = Service.createApi(ArticleApi);

装饰器堆叠的顺序

@Api 在最上、@Header 紧随其后都是合法的 —— @Header 同时支持类与方法。 方法上的 @Header 会覆盖类上的同名 key,见参数装饰器。

3. 列表与分页 ​

ts
import { articleApi } from "./api/article.api";

async function loadPage(page: number, size = 20) {
  const method = articleApi.list({ page, size, keyword: "" });

  // 先订阅,再发送:api.getX() 不会发出任何请求
  method.onSuccess((result) => {
    console.log(`第 ${page} 页,共 ${result.data.total} 条`);
  });
  method.onCodeError(({ code, payload }) => {
    console.warn("业务失败", code, payload);
  });

  return method.send();
}

const { data, code, message } = await loadPage(1);
console.log(data.items, code, message);

4. 详情、缓存标记与取消 ​

ts
import { SnailCancelledError } from "@snail-js/api";
import { articleApi } from "./api/article.api";

const method = articleApi.detail(42);
method.onHitCache(() => console.log("来自缓存"));

const promise = method.send();

// 用户快速切换了详情页 → 丢掉旧请求
method.abort();

try {
  const { data } = await promise;
  console.log(data);
} catch (error) {
  if (error instanceof SnailCancelledError) {
    console.log("这次请求不再需要了");
  } else {
    throw error;
  }
}

fromCache 需要缓存插件

核心只为 SnailResult.fromCache 与 onHitCache 提供机制(ctx.markCacheHit()), 真正决定「什么请求被缓存」的是 @snail-js/api/plugins 里的 Cache 插件(见 缓存插件)。没有装插件时,fromCache 恒为 false,onHitCache 永不触发。

5. 写操作与错误分级 ​

ts
import { SnailError, SnailResponseError, type SnailResult } from "@snail-js/api";

async function rename(id: number, title: string): Promise<boolean> {
  try {
    const result: SnailResult<
      { code: number; message: string; data: unknown },
      { id: number; title: string }
    > = await articleApi.rename(id, title).send();
    return result.code === 0;
  } catch (error) {
    if (error instanceof SnailResponseError) {
      // 业务失败:HTTP 是 2xx,但业务码不通过
      console.warn("业务提示:", (error.payload as { message?: string }).message);
      return false;
    }
    if (error instanceof SnailError) {
      // 超时 / 取消 / 装饰器错误等
      console.error(error.code, error.message);
      return false;
    }
    // 其余是 axios 的错误(HTTP 4xx/5xx、断网、CORS)
    throw error;
  }
}

6. 带上 ETag 的更新 ​

ts
const article = await articleApi.detail(42).send();

const updated = await articleApi
  .replace(42, { title: "新标题", body: article.data.body, tags: article.data.tags }, "\"v3\"")
  .send();

@HeaderValue("if-match") 把第三个参数写进请求头。类/方法级的静态头用 @Header({...}),动态值一律走 @HeaderValue。

7. 批量并发 ​

SnailMethod 是普通对象,Promise.all 直接可用;每个方法都有独立的上下文与 ctx.state, 所以并发是安全的(这正是把 state 从 meta 里拆出来的原因)。

ts
const ids = [1, 2, 3, 4];

const results = await Promise.all(
  ids.map((id) => articleApi.detail(id).send())
);

const articles = results.map((result) => result.data);

需要「同一时刻只保留最新一次」时

自己 abort 上一次即可(见 §4),或者使用 @snail-js/api/strategies 里的策略 —— 策略内部就是这么做的,并且把 SnailCancelledError 当作预期控制流消化掉。

8. 非 JSON 响应 ​

ts
const blob = (await articleApi.exportAll({ tag: "vue" }).send()).data;
//    ^? Blob

const url = URL.createObjectURL(blob);

responseType: "blob" 时响应体没有 code/message/data,库会整体穿透:result.data 与 result.envelope 都是那个 Blob。详见响应与类型。

基于 MIT 许可发布