Skip to content

可复用 Middleware

envoi middleware 将一个或多个生命周期 hooks 组织成可复用的请求策略单元;每个生命周期阶段按声明顺序串行执行对应 hooks。

ts
interface HttpHooks {
  onRequest?: Hook | Hook[];
  onRequestError?: Hook | Hook[];
  onResponse?: Hook | Hook[];
  onResponseError?: Hook | Hook[];
  onSuccess?: Hook | Hook[];
  onFinally?: Hook | Hook[];
}

定义 middleware 模块

ts
// src/api/middleware/auth.ts
import { auth, createMiddleware } from "@envoijs/http";

export const authMiddleware = createMiddleware({
  onRequest: auth(() => sessionStorage.getItem("access_token")),
  onResponseError: (ctx) => {
    if (ctx.response.status === 401) clearSessionAndRedirect();
  },
});
ts
// src/api/middleware/locale.ts
export const localeMiddleware = createMiddleware({
  onRequest: (ctx) => {
    ctx.request.headers["Accept-Language"] = getLocale();
    ctx.request.headers["X-Time-Zone"] = Intl.DateTimeFormat().resolvedOptions().timeZone;
  },
});

组合 middleware

ts
import { createHttp, mergeMiddleware } from "@envoijs/http";

export const http = createHttp({
  adapter: "fetch",
  defaults: { baseURL: "/api" },
  hooks: mergeMiddleware(authMiddleware, localeMiddleware, traceMiddleware, errorMiddleware),
});

每个阶段都按声明顺序执行。

text
auth.onRequest
locale.onRequest
trace.onRequest
adapter
error.onResponse

response hooks 也保持声明顺序,不会反向回卷。

只给一个请求接入 middleware

ts
const reportMiddleware = createMiddleware({
  onRequest: (ctx) => {
    ctx.request.headers["X-Report-Version"] = "1";
  },
  onResponse: (ctx) => {
    ctx.response.body = normalizeLegacyReport(ctx.response.body);
  },
});

await http.get("/legacy/report", {
  hooks: reportMiddleware,
});

全局 middleware 先执行,随后执行单请求 middleware。

接入已有状态码定义

ts
export const ApiCode = {
  Ok: 0,
  Unauthorized: 10_001,
  Validation: 20_001,
} as const;

const http = createHttp({
  adapter: "fetch",
  envelope: {
    code: "status",
    msg: "message",
    data: "payload",
    ok: (code) => code === ApiCode.Ok,
    unauthorized: (code) => code === ApiCode.Unauthorized,
    warning: (code) => code === ApiCode.Validation,
  },
  hooks: mergeMiddleware(authMiddleware, errorMiddleware),
});

onResponseError 会收到分类完成的 error:

ts
import { BizError, createMiddleware } from "@envoijs/http";

export const errorMiddleware = createMiddleware({
  onResponseError: (ctx) => {
    if (!(ctx.error instanceof BizError)) return;

    if (ctx.error.kind === "unauthorized") {
      clearSessionAndRedirect();
      return;
    }

    if (ctx.error.code === ApiCode.Validation) {
      showValidationWarning(ctx.error.msg);
      return;
    }

    if (ctx.request.meta.silent !== true) showError(ctx.error.msg);
  },
});

envelope 负责协议状态分类。middleware 根据分类结果执行登录跳转、提示等跨接口行为。

区分 HTTP status 与业务 code

ctx.response.status 是 HTTP status。BizError.code 是映射后的协议 code,BizError.source 标记失败来自哪一层:

ts
onResponseError: (ctx) => {
  if (ctx.response.status === 429) scheduleRetry();

  if (ctx.error instanceof BizError && ctx.error.source === "body")
    reportBusinessCode(ctx.error.code);
};

使用字段映射的 envelope 时,HTTP response 需要先满足 2xx。HTTP 401 分类为 unauthorized,其他非 2xx 状态分类为 error。response body 的成功 code 无法把 HTTP 500 改成成功。

后端对 HTTP status 有特殊语义时,使用 defineEnvelope()。它的 kind(body, response) 可以同时读取 response.status 和 body。middleware 会收到最终分类结果。

从 axios interceptors 迁移

现有 axios 逻辑envoi 接入位置
request interceptoronRequest middleware
request rejection interceptoronRequestError middleware
成功 response interceptoronResponse middleware 或 envelope
HTTP / 业务错误 interceptoronResponseError middleware
返回 response.data.dataenvelope 的 data 映射
根据业务 code rejectenvelope 的 ok/unauthorized/warning

所有接口共用的 response 取值规则放进 envelope。onResponse 用于 envelope 分类前确实需要执行的 response 标准化。

基于 MIT License 发布。