--- 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(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(options)` | [references/error-handling.md](references/error-handling.md) | | SSE 流式请求 | `sse(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 { 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 = { // 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 { 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 `。 > > 失效标识:`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; 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 extends Promise> { __abort?: () => void; } export interface ApiResponse { code: number; message: string; data: T; } export interface UploadOptions { url: string; file: File | string; // 通用前端用 File;uniapp 用文件路径 string name?: string; formData?: Record; header?: Record; timeout?: number; onProgress?: (progress: number) => void; // 0-100 } export function request(options: RequestOptions): RequestPromise; export function get( url: string, data?: any, options?: Omit ): RequestPromise; export function post( url: string, data?: any, options?: Omit ): RequestPromise; export function put( url: string, data?: any, options?: Omit ): RequestPromise; export function del( url: string, data?: any, options?: Omit ): RequestPromise; export function upload(options: UploadOptions): Promise; // 直接返回业务 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`) - 支持精确匹配 `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(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`,否则前后端无法桥接。