210 lines
4.8 KiB
Markdown
210 lines
4.8 KiB
Markdown
---
|
||
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`,屏蔽外围结构 |
|
||
|
||
---
|
||
|
||
## 关联笔记
|
||
|
||
- [[路由原理]]
|
||
- [[前端项目依赖清单]]
|