--- tags: [frontend, api, typescript, fetch, abstraction] create time: 2026-04-25 14:30 --- # 统一封装请求:apiRequest 函数 ## 概述 把前端所有接口调用中**重复的逻辑**抽到一个公共函数里,让每个页面只需要关心「调哪个接口、传什么参数」。 > 💡 **想一想**:如果每个页面都自己写一遍 token 拼接、错误处理、JSON 解析,代码会变得怎样? 答案是——重复、易错、难维护。所以业界的标准做法就是:**抽出一个通用请求函数**。 --- ## 核心代码 ```ts export async function apiRequest(path: string, init?: RequestInit): Promise { function getAuthToken() { return window.localStorage.getItem(TOKEN_STORAGE_KEY) ?? '' } const response = await fetch(`${API_BASE}${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...(getAuthToken() ? { Authorization: `Bearer ${getAuthToken()}` } : {}), ...(init?.headers ?? {}), }, }) const result = (await response.json()) as ApiResponse if (!response.ok || result.code !== 200) { throw new Error(result.msg || '请求失败') } return result.data } ``` --- ## 逐行拆解:这段代码在做什么 ### 1. 泛型 `` — 让调用方决定返回值类型 ```ts export async function apiRequest(path: string, init?: RequestInit): Promise ``` - `path`:接口路径,比如 `'/users'` - `init`:fetch 配置项,比如 `method`、`body` - ``:返回值类型由调用方指定 **好处**:调用时可以自动获得类型提示和类型检查。 ### 2. 自动读取 token ```ts function getAuthToken() { return window.localStorage.getItem(TOKEN_STORAGE_KEY) ?? '' } ``` 从本地存储取出登录凭证。有 token 就返回 token,没有就返回空字符串。 ### 3. 拼接完整请求地址 ```ts const response = await fetch(`${API_BASE}${path}`, { ... }) ``` 把基础地址和接口路径拼在一起: | 变量 | 值 | |------|-----| | `API_BASE` | `'http://localhost:3000/api'` | | `path` | `'/login'` | | **最终 URL** | `'http://localhost:3000/api/login'` | ### 4. 合并请求配置(重点) ```ts { ...init, // 外部传的配置 headers: { 'Content-Type': 'application/json', // 统一 JSON 格式 ...(getAuthToken() ? { Authorization: `Bearer ${getAuthToken()}` } : {}), // 自动加 token ...(init?.headers ?? {}), // 合并外部请求头 }, } ``` 层层展开,体现一个设计思想:**统一封装但允许扩展**。 | 优先级 | 顺序 | 说明 | |--------|------|------| | 1 | 最低 | `'Content-Type'` 统一设为 JSON | | 2 | 中间 | 有 token 就自动带上 `Authorization` | | 3 | 最高 | 调用方自定义的请求头可以覆盖 | ### 5. 统一解析响应 ```ts const result = (await response.json()) as ApiResponse ``` 默认后端返回格式为: ```json { "code": 200, "msg": "success", "data": { ... } } ``` ### 6. 统一判断成功失败 ```ts if (!response.ok || result.code !== 200) { throw new Error(result.msg || '请求失败') } ``` 做了双重判断: - **HTTP 层面**:`response.ok` 检查状态码是否在 200~299 - **业务层面**:`result.code === 200` 检查后端返回的业务状态 任意一个失败就抛错,页面层不用再写重复的判断逻辑。 ### 7. 只返回业务数据 ```ts return result.data ``` 最终直接返回 `data` 字段,调用方拿到的就是最干净的数据。 --- ## 请求流程 ```mermaid flowchart LR A[调用 apiRequest] --> B[读取本地 token] B --> C[拼接完整 URL] C --> D[合并请求配置和请求头] D --> E[发起 fetch 请求] E --> F[解析 JSON 响应] F --> G{是否成功?} G -->|HTTP 或 code 异常| H[抛出错误] G -->|都正常| I[返回 data] H --> J[页面层捕获错误] I --> K[页面层处理业务数据] ``` --- ## 实际调用示例 获取课程列表: ```ts const courses = await apiRequest('/courses') ``` 提交表单: ```ts const user = await apiRequest('/login', { method: 'POST', body: JSON.stringify({ username, password }), }) ``` 调用方不需要关心: - token 怎么加 - JSON 怎么解析 - 错误怎么处理 --- ## 总结:这段封装的本质 > 把**「请求的共性」**抽出来,变成标准入口; > 把**「接口的差异」**保留给调用参数。 具体体现在四个层面: | 层面 | 封装内容 | |------|----------| | 函数抽象 | `apiRequest` 统一入口 | | 配置合并 | headers、URL 自动拼接 | | 错误统一处理 | HTTP 状态 + 业务 code 双重判断 | | 返回值格式化 | 只返回 `data`,屏蔽外围结构 | --- ## 关联笔记 - [[路由原理]] - [[前端项目依赖清单]]