This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

210 lines
4.8 KiB
Markdown
Raw Permalink Normal View History

2026-04-25 22:15:39 +08:00
---
tags: [frontend, api, typescript, fetch, abstraction]
create time: 2026-04-25 14:30
---
# 统一封装请求:apiRequest 函数
## 概述
把前端所有接口调用中**重复的逻辑**抽到一个公共函数里,让每个页面只需要关心「调哪个接口、传什么参数」。
> 💡 **想一想**:如果每个页面都自己写一遍 token 拼接、错误处理、JSON 解析,代码会变得怎样?
答案是——重复、易错、难维护。所以业界的标准做法就是:**抽出一个通用请求函数**。
---
## 核心代码
```ts
export async function apiRequest<T>(path: string, init?: RequestInit): Promise<T> {
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<T>
if (!response.ok || result.code !== 200) {
throw new Error(result.msg || '请求失败')
}
return result.data
}
```
---
## 逐行拆解:这段代码在做什么
### 1. 泛型 `<T>` — 让调用方决定返回值类型
```ts
export async function apiRequest<T>(path: string, init?: RequestInit): Promise<T>
```
- `path`:接口路径,比如 `'/users'`
- `init`:fetch 配置项,比如 `method`、`body`
- `<T>`:返回值类型由调用方指定
**好处**:调用时可以自动获得类型提示和类型检查。
### 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<T>
```
默认后端返回格式为:
```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<Course[]>('/courses')
```
提交表单:
```ts
const user = await apiRequest<User>('/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
})
```
调用方不需要关心:
- token 怎么加
- JSON 怎么解析
- 错误怎么处理
---
## 总结:这段封装的本质
> 把**「请求的共性」**抽出来,变成标准入口;
> 把**「接口的差异」**保留给调用参数。
具体体现在四个层面:
| 层面 | 封装内容 |
|------|----------|
| 函数抽象 | `apiRequest<T>` 统一入口 |
| 配置合并 | headers、URL 自动拼接 |
| 错误统一处理 | HTTP 状态 + 业务 code 双重判断 |
| 返回值格式化 | 只返回 `data`,屏蔽外围结构 |
---
## 关联笔记
- [[路由原理]]
- [[前端项目依赖清单]]