---
name: frontend-request-skill
description: Use when designing or reviewing the request layer of a frontend project (web / uni-app / mini-program), including request.ts wrappers, interceptors, deduplication, mocks, error handling, file upload, SSE streaming, or token refresh. Provides both a general frontend specification and a uniapp-specific adapter.
---

# 前端请求层设计 Skill

## 定位

只聚焦**前端请求层设计**：从 `request.ts` 出发，建立统一、健壮、可维护的请求体系。

本 skill 提供**两层规范**：
1. **通用前端规范**：面向 Web/H5/React/Vue 等标准前端项目，基于 `fetch` / `axios` 实现。
2. **uniapp 适配规范**：面向 uni-app 小程序/APP/H5 跨端项目，基于 `uni.request` 实现。

二者核心思想完全一致（统一入口、响应信封、鉴权拦截、错误码映射、Token 刷新队列、SSE、上传），仅底层网络 API 不同。

本 skill 只处理请求相关逻辑，不依赖其他 skill。

## 前后端契约（强约定）

> **本 skill 的所有规范必须与 [`references/api-contract.md`](references/api-contract.md) 严格对齐**。
>
> 该契约文档是前后端桥接的**唯一真理源**，定义：
>
> - **接口存放位置**（vue/uniapp/react 统一 `src/api/`）
> - **登录页/管理页接口契约**
> - **请求头规范**（Authorization / Content-Type / X-Request-ID）
> - **响应信封** `{ code, message, data }`
> - **错误码契约**（`code<0` 异常，`>=0` 成功，**`-1` 特殊 = JWT 鉴权失效**）
> - **showError 参数语义**（默认 `false` 自动弹，`true` 调用方自己处理）
> - **HTTP 500 兜底**（强制 swallow 用户 Toast，交给全局错误处理）

**强约定**：

- 后端新生成业务 → 必须更新后端 `api-contract.md`
- 前端新加接口 → 必须读取契约，按契约定义路径/字段/code
- 契约变更必须前后端同步

## 解决的问题

| 痛点 | 后果 | 本技能方案 |
|------|------|-----------|
| 每个页面各自调原生请求 | 鉴权/错误处理重复 | 统一 request.ts |
| Token 过期无感知 | 用户操作失败 | 响应拦截器识别 401，统一交给 auth service 处理 |
| 重复点击导致重复请求 | 数据异常/资源浪费 | 防抖去重 |
| 后端接口未 ready | 前端阻塞 | Mock 机制 |
| 游客误触敏感接口 | 报错/白屏 | 请求层前置拦截 |
| 错误提示不统一 | 用户体验差 | 统一错误通知 |
| SSE/流式接口不知如何接入 | 聊天/AI 回复无法流式展示 | 跨端 SSE 封装 + 打字机效果 |
| Token 过期后并发请求全部失败 | 用户重复登录、数据丢失 | Token 刷新队列 + 失败请求自动重试 |

## When to Use

- "请求封装"
- "request.ts 怎么写"
- "前端请求统一处理"
- "uniapp 请求统一处理"
- "接口拦截"
- "Token 刷新"（请求层衔接部分，详见 [references/auth-patterns.md](references/auth-patterns.md)）
- "游客模式拦截"
- "Mock 数据配置"
- "接口防抖"
- "错误处理"
- "文件上传"
- "SSE 流式请求"
- "打字机效果"
- "Server-Sent Events"
- "AI 聊天流式回复"

## When NOT to Use

- 需要完整登录鉴权/权限设计 → 本 skill 只提供请求层衔接，完整鉴权体系需单独设计
- 需要项目整体规范化/目录结构诊断 → 不在本 skill 范围内
- 需要跨平台兼容性审计 → 不在本 skill 范围内

## 两层规范速查

| 规范 | 适用场景 | 底层 API | 参考位置 |
|------|----------|----------|----------|
| 通用前端规范 | Web / H5 / React / Vue 等 | `fetch` / `axios` | [references/frontend-spec.md](references/frontend-spec.md) |
| uniapp 适配规范 | 微信小程序 / App / H5 | `uni.request` / `uni.uploadFile` | [references/uniapp-spec.md](references/uniapp-spec.md) |

> 二者仅在「底层网络 API」和「Token 存储方式」上有差异；响应信封、错误码、鉴权拦截、去重、Mock、SSE 解析逻辑完全一致。

## Quick Reference

| 能力 | 关键选项 | 参考位置 |
|------|----------|----------|
| **前后端契约** | 接口位置 / 错误码 / JWT | **[references/api-contract.md](references/api-contract.md)** |
| 统一请求 | `request<T>(options)` | [references/frontend-spec.md](references/frontend-spec.md)、[references/uniapp-spec.md](references/uniapp-spec.md) |
| Token 注入 | `needAuth`、`authMode` | [references/auth-patterns.md](references/auth-patterns.md) |
| 401/403 处理 | `skipAuthHandler` | [references/auth-patterns.md](references/auth-patterns.md) |
| 防抖去重 | `skipDebounce` | [references/request-impl.md](references/request-impl.md) |
| Mock 数据 | `USE_MOCK` | [references/mock-guide.md](references/mock-guide.md) |
| 错误提示 | `showError` | [references/error-handling.md](references/error-handling.md) |
| 文件上传 | `upload<T>(options)` | [references/error-handling.md](references/error-handling.md) |
| SSE 流式请求 | `sse<T>(options, onMessage)` | [references/sse-guide.md](references/sse-guide.md) |
| Token 自动刷新 | `auth.service.ts` 队列 | [references/auth-patterns.md](references/auth-patterns.md) |

## 核心文件结构

> **强约定**：所有前端项目（vue / uniapp / react）接口统一存放在 `src/api/` 下。
>
> 详细目录约定见 [api-contract.md §2](references/api-contract.md)。

```
src/
├── api/                       # 前后端桥接层（frontend-request-skill 管理）
│   ├── request.ts             # 统一请求封装（核心）
│   ├── upload.ts              # 文件上传封装
│   ├── sse.ts                 # SSE 流式请求封装
│   ├── modules/               # 业务模块 API（auth / user / role / menu / ...）
│   ├── _mocks_/               # Mock 数据字典
│   │   ├── index.ts           # MOCK_MAP + MockEntry
│   │   └── *.mock.ts          # 各模块 Mock
│   └── types/                 # 接口请求/响应类型
├── services/
│   └── auth.service.ts        # 鉴权服务：login / logout / handleUnauthorized
├── config/
│   ├── api.config.ts          # BASE_URL / 超时 / Mock / 成功码 / 鉴权失败码 / 重试
│   └── error.config.ts        # 错误码映射（与 api-contract.md 错误码表对齐）
├── utils/
│   ├── auth.ts                # getToken / setToken
│   ├── toast.ts               # 错误提示工具
│   └── error.ts               # 错误信息提取
└── composables/
    ├── useAuth.ts             # 游客判断 Hook
    └── useTypewriter.ts       # 打字机效果 Hook
```

> **禁止**把接口散落到 `pages/api/`、`views/api/`、`utils/api/`、`service/` 等位置。
>
> uniapp 项目同样使用 `src/api/`，H5/小程序/App 三端一致。

## 响应信封与错误约定

本 skill 采用统一的响应结构（响应信封），与后端接口契约严格对齐：

```typescript
export interface ApiResponse<T = any> {
  code: number;    // 业务状态码
  message: string; // 提示信息
  data: T;         // 业务数据
}
```

### 业务状态码约定（与 api-contract.md §7 严格对齐）

| code 范围 | 含义 | 处理方式 |
|-----------|------|----------|
| `code = 0` | 业务成功 | 正常返回 `data` |
| `code < 0` | 业务异常 | 抛出 `RequestError`，错误码为对应的负数值 |
| `code = -1` | **JWT 鉴权失效**（未登录 / Token 无效 / 过期 / 未传递） | 触发 Token 刷新流程，等同 HTTP 401 |
| `code > 0` | 按项目约定（若存在） | 默认也视为业务异常 |

> 本项目示例默认 `SUCCESS_CODES = [0]`、`AUTH_FAILURE_CODES = [-1]`。如果你的后端约定不同，请在 `src/config/api.config.ts` 中调整。

### showError 参数语义（与 api-contract.md §8 严格对齐）

> **核心约定**：默认请求层自动弹 Toast，传 `showError: true` 时**不弹**，调用方自己处理。

| 取值 | 含义 | 默认 |
|------|------|------|
| `false`（默认） | 请求层按 `ERROR_CODE_MAP` 自动弹 Toast | ✅ |
| `true` | 请求层**不弹**，调用方自行 try/catch 处理 | — |

> **设计理由**：默认自动弹覆盖 80% 场景（页面直取数据）；少数特殊场景（轮询、上传队列、批量提交）传 `showError: true` 让调用方自己处理。

### HTTP 500 错误兜底（与 api-contract.md §9 严格对齐）

> HTTP 500 / 502 / 503 / 504 类错误**前端不弹任何用户 Toast**。
>
> 理由：500 是服务端问题，用户弹 Toast 也无法解决；交给全局错误处理（Sentry / 监控平台 / 后端日志）。
>
> 业务调用方仍可感知（用于降级逻辑），但用户**无感**。

### HTTP 状态异常

HTTP 层错误与业务 code 互不干扰，统一由 `statusCode` 判断：

| HTTP 状态 | 错误码 | 场景 |
|-----------|--------|------|
| 401 | `UNAUTHORIZED` | 登录过期，触发 Token 刷新 |
| 403 | `FORBIDDEN` | 权限不足 |
| 400 / 404 / 500 等 | `HTTP_ERROR` | 请求异常 |
| 超时 / 断网 | `TIMEOUT` / `NETWORK_ERROR` | 网络异常 |

### 错误码映射约定

`src/config/error.config.ts` 中的 `ERROR_CODE_MAP` 用于把错误码转成用户友好文案。本 skill 内置的映射仅为**示例**，你必须按自己后端的真实 code 约定替换：

```typescript
export const ERROR_CODE_MAP: Record<string, string> = {
  // HTTP 状态异常（请求层）
  UNAUTHORIZED: '登录已过期，请重新登录',
  FORBIDDEN: '权限不足',
  TIMEOUT: '请求超时，请检查网络',
  NETWORK_ERROR: '网络异常，请稍后重试',

  // 业务异常示例（按 code < 0 约定，需与后端契约保持一致）
  '-1001': '参数校验错误',
  '-1002': '未登录或 Token 无效',
  '-1003': '无权限',
  '-1004': '资源不存在',
  '-1005': '资源冲突',
  '-1006': '请求过于频繁',
  '-2000': '系统繁忙，请稍后再试',
};
```

> **重要**：以上错误码与各 init-skill 内置契约（`fastapi-init-skill`、`springboot-init-skill` 等）保持一致。接入真实项目时，请与后端确认错误码表并替换。

### 分页约定

前后端统一的分页请求/响应格式：

**请求参数**：

| 参数 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `page` | number | 1 | 页码，从 1 开始 |
| `pageSize` | number | 20 | 每页条数，上限 100 |

**响应结构**（在 `data` 内）：

```typescript
export interface PageResponse<T> {
  list: T[];       // 数据列表
  total: number;   // 总条数
  page: number;    // 当前页码
  pageSize: number; // 每页条数
}
```

> 各后端 init-skill（springboot / fastapi / go-gin）的分页格式已对齐此约定。

### Token 响应约定

后端登录接口返回 Token 时，统一使用以下字段：

```typescript
export interface TokenResponse {
  accessToken: string;   // 访问令牌（Java 后端 camelCase，Python 后端 snake_case 兼容）
  refreshToken: string;  // 刷新令牌
  tokenType: string;     // 固定 "Bearer"
  expiresIn: number;     // 过期时间（秒）
}
```

> **注意**：Java 后端使用 camelCase，Python 后端使用 snake_case。前端必须做兼容处理（优先 camelCase，fallback snake_case），详见 [api-contract.md §10](references/api-contract.md)。

### JWT 鉴权约定（所有骨架统一）

> **强约定**：所有后端骨架（Go / Java / Python / NodeJS）必须使用 JWT 做无状态鉴权。
>
> 请求头：`Authorization: Bearer <token>`。
>
> 失效标识：`HTTP 401` 或 `code === -1`。
>
> 完整规范见 [api-contract.md §11](references/api-contract.md)。

## 设计要点

### 1. 统一入口

```typescript
// src/api/request.ts
export interface RequestOptions {
  url: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS' | 'HEAD';
  data?: any;                // 请求体：通常为 plain object
  header?: Record<string, string>;
  timeout?: number;
  needAuth?: boolean;        // 是否需要 Token，默认 true
  showError?: boolean;       // 是否在请求层弹出错误 Toast；默认 false（自动弹）。true 时不弹，调用方自行处理
  skipDebounce?: boolean;    // 是否跳过防抖，默认 false
  skipAuthHandler?: boolean; // 是否跳过 401 处理，默认 false
  prefix?: string;           // API 前缀，默认 DEFAULT_PREFIX
  authMode?: 'bearer' | 'customer-token'; // Token 头格式
  retry?: number;            // 失败重试次数，默认 REQUEST_RETRY_COUNT
}

export interface RequestPromise<T> extends Promise<ApiResponse<T>> {
  __abort?: () => void;
}

export interface ApiResponse<T = any> {
  code: number;
  message: string;
  data: T;
}

export interface UploadOptions {
  url: string;
  file: File | string;       // 通用前端用 File；uniapp 用文件路径 string
  name?: string;
  formData?: Record<string, any>;
  header?: Record<string, string>;
  timeout?: number;
  onProgress?: (progress: number) => void; // 0-100
}

export function request<T = any>(options: RequestOptions): RequestPromise<T>;
export function get<T = any>(
  url: string,
  data?: any,
  options?: Omit<RequestOptions, 'url' | 'method' | 'data'>
): RequestPromise<T>;
export function post<T = any>(
  url: string,
  data?: any,
  options?: Omit<RequestOptions, 'url' | 'method' | 'data'>
): RequestPromise<T>;
export function put<T = any>(
  url: string,
  data?: any,
  options?: Omit<RequestOptions, 'url' | 'method' | 'data'>
): RequestPromise<T>;
export function del<T = any>(
  url: string,
  data?: any,
  options?: Omit<RequestOptions, 'url' | 'method' | 'data'>
): RequestPromise<T>;
export function upload<T = any>(options: UploadOptions): Promise<T>; // 直接返回业务 data，不包 ApiResponse
```

完整实现见 [references/frontend-spec.md](references/frontend-spec.md)（通用前端）与 [references/uniapp-spec.md](references/uniapp-spec.md)（uniapp 适配）。

### 2. 鉴权衔接

本 skill 只负责请求层与鉴权的衔接点：

- 默认请求自动注入 Token
- 支持 `needAuth: false` 跳过（如登录接口本身）
- 支持 `authMode: 'bearer' | 'customer-token'` 切换鉴权头格式
- 401/403 响应交给 `auth.service.ts` 统一处理
- 登录态来源可由项目自行选择：Storage 最小化方案 或 Pinia `userStore` 方案

> **重要**：通用前端示例默认使用 `localStorage`；uniapp 示例默认使用 `uni.getStorageSync('token')`。如果你使用 Pinia 管理登录态，请参考 [references/auth-patterns.md](references/auth-patterns.md) 替换为 `userStore` 方案，请求层代码无需改动。

详细 Token 管理、401/403 处理、Token 刷新队列、登出回跳等见 [references/auth-patterns.md](references/auth-patterns.md)。

### 3. 游客模式

请求层只做最小拦截：

```typescript
import { formatError } from '@/utils/error';

if (options.needAuth !== false && !getToken()) {
  return Promise.reject(formatError('NO_AUTH_TOKEN', '未登录'));
}
```

业务层建议前置检查：

```typescript
const { checkLogin } = useAuth();

function handleLike() {
  if (!checkLogin()) return;
  post('/api/like', { id: itemId });
}
```

### 4. 防抖去重

- 同一 key 的并发请求只发一次，返回同一个 Promise
- 请求完成后清理 pending，释放内存
- 提交类接口可设置 `skipDebounce: true`

### 5. Mock 机制

- 通过全局开关 `USE_MOCK` 控制：开启后所有请求强制走 Mock，关闭后全部走真实接口
- Mock 数据建议按接口字段契约声明类型（`MockEntry<T>`）
- 支持精确匹配 `METHOD:/path` 和 REST 路径参数匹配
- 见 [references/mock-guide.md](references/mock-guide.md)

### 6. 错误处理

- 统一错误信息提取（`message` / `msg` / `error` / `detail`）
- 开发环境 Modal 展示完整错误，生产环境 Toast/Modal 分级提示
- 文件上传单独封装
- 见 [references/error-handling.md](references/error-handling.md)

### 7. SSE 流式请求

- 封装 `sse<T>(options, onMessage, onError?)`，支持 H5 `EventSource` 与小程序 `enableChunked` 双端
- 自动注入 Token、401 识别、手动中断
- 流式 chunk 解析 + 数据行缓存，适用于 AI 聊天、打字机效果
- 见 [references/sse-guide.md](references/sse-guide.md)

### 8. Token 自动刷新与失败重试

- HTTP 401 触发静默刷新
- 刷新期间新请求入队，刷新成功后自动重发
- 刷新失败统一登出，避免用户反复登录
- 见 [references/auth-patterns.md](references/auth-patterns.md)

## Common Mistakes

| 错误 | 后果 | 正确做法 |
|------|------|----------|
| 在请求层写死鉴权跳转逻辑 | 与 auth skill 重复、难以维护 | 请求层只识别 401/403，统一交给 `auth.service.ts` |
| 把 401 重试/Token 刷新在每个 API 里单独实现 | 代码重复、并发刷新导致多次登录 | 使用队列式 Token 刷新，统一收口到 auth.service.ts |
| 并发请求未做去重 | 重复点击导致重复提交 | 用 Map 缓存同一 key 的 pending Promise |
| `JSON.stringify` 直接生成请求 key | 属性顺序不同导致 key 不同，去重失效 | 递归排序 key 后序列化 |
| `statusCode !== 200` 判断成功 | 201/204 等合法状态被误判 | `200 <= statusCode < 300` |
| Mock 数据写进生产包 | 数据泄露、行为异常 | Mock 仅由 `VITE_USE_MOCK` 控制，生产环境设为 `false` |
| 文件上传复用 request 的防抖 | 大文件/多次选择文件被错误去重 | 上传单独封装，不走 request 防抖 |
| SSE 在小程序端使用 H5 的 EventSource | 小程序无原生 EventSource，直接报错 | 使用 `enableChunked` + 手动解析 chunk |
| SSE 不处理连接中断/页面卸载 | 内存泄漏、重复回调 | 返回可中断的 requestTask，页面 onUnload 时调用 |
| 401 时直接重试原请求但不刷新 Token | 重试仍失败，陷入死循环 | 先刷新 Token，再重试队列中的请求 |
| Token 刷新不排队 | 并发刷新导致多次登录请求 | 使用 `isRefreshing` + Promise 队列 |
| 接口散落到 `pages/api/`、`views/api/`、`service/` | 跨项目无法复用，Mock 难统一 | **强约定**：所有前端项目（vue/uniapp/react）统一放 `src/api/`，见 [api-contract.md §2](references/api-contract.md) |
| 凭直觉写接口路径/字段 | 前后端不对齐，联调失败 | **强约定**：新加接口必须读 [api-contract.md](references/api-contract.md)，没有就更新 |
| HTTP 500 给用户弹"服务器繁忙" Toast | 用户看到无意义提示，无法自助解决 | **强约定**：500 类错误请求层 swallow，交给全局错误处理，见 [api-contract.md §9](references/api-contract.md) |
| 把业务异常用 `showError: true` 隐藏 | 用户不知道发生了什么 | `showError: true` 只用于轮询 / 静默重试 / 自定义提示场景 |

## 输出

触发本 skill 时，按以下优先级输出：

1. **契约对齐**：先读 [`references/api-contract.md`](references/api-contract.md)，确认接口位置 / 错误码 / JWT 约定
2. **问题诊断**：当前请求层存在的主要问题（重复代码、缺拦截器、错误处理散落等）
3. **结构方案**：推荐的 `src/api/`、`src/config/`、`src/utils/`、`src/services/` 文件划分
4. **核心代码**：给出或修正 `request.ts`、`upload.ts`、错误处理工具的实现
5. **衔接说明**：明确哪些逻辑属于请求层、哪些应收口到 `auth.service.ts`
6. **进阶能力**：按需补充 SSE 流式请求、Token 自动刷新队列、失败重试
7. **参考引用**：复杂实现直接引用 `references/` 中的对应文档

## 职责边界

| 范畴 | 本 skill 负责 | 本 skill 不负责 |
|------|--------------|----------------|
| 请求封装 | `request.ts`、拦截器、去重、Mock、错误提示、上传、SSE 衔接 | — |
| 鉴权实现 | 请求层 Token 注入、401/403 识别、刷新触发点 | Token 管理、登录态、登出回跳等完整鉴权体系 |
| Token 刷新队列 | — | 推荐由 `auth.service.ts` 统一实现，请求层只负责触发与重试 |
| 项目规范 | — | 目录结构、命名规范等通用规范 |
| 跨平台审计 | — | 多端兼容性检查 |

## 与后端规范的联动

> 本 skill 的响应信封、错误码、JWT、请求头规范以 [`references/api-contract.md`](references/api-contract.md) 为准。

- 后端 `EnvelopeRoute` 输出 `{ code, message, data }`
- 前端 `request.ts` 按相同结构解析
- `ERROR_CODE_MAP` 直接复用契约 §7 错误码表
- JWT 鉴权 + `code === -1` 失效标识由契约 §11 约束

后端 init-skill（`fastapi-init-skill` / `springboot-init-skill` / `go-gin-init-skill` / `nodejs-init-skill`）**必须**生成与 [`references/api-contract.md`](references/api-contract.md) 兼容的 `api-contract.md`，否则前后端无法桥接。
