📘 前端 API 调用方式详解:`fetch` vs `callApi` vs `API.invoke`
> **适用读者**:React + TypeScript 开发者,Nx + Django REST Framework 项目使用者
> **目标**:理解三种主流前端 API 调用方式的原理、适用场景与权衡
## 📌 一、背景说明
在现代 Web 应用中,前端需要与后端 API 通信。根据项目架构不同,调用方式也不同。本文以 **Nx + React + Jotai + DRF (Django REST Framework)** 项目为例,对比三种调用方式:
-
**`fetch`**:浏览器原生 API
-
**`callApi`**:项目封装的通用调用函数(基于 RPC 风格)
-
**`API.invoke`**:底层 RPC 方法调用(通常由 `callApi` 封装)
## 🧱 二、1. `fetch`:原生 HTTP 客户端
💡 是什么?
-
浏览器内置的 **原生 HTTP 请求 API**
-
无需任何依赖,所有现代浏览器都支持
🔧 示例代码
```ts
// 删除组织(直接调用 REST API)
const deleteOrganization = async (orgId: number) => {
const response = await fetch(`/api/organizations/${orgId}`, {
method: 'DELETE',
credentials: 'include', // 携带 Cookie(用于认证)
headers: {
'Content-Type': 'application/json',
...(csrfToken && { 'X-CSRFToken': csrfToken }),
}
});
if (!response.ok) {
throw new Error('删除失败');
}
return await response.json(); // 或 response.text()
};
```
✅ 优点
-
**零依赖**:不依赖任何库
-
**完全控制**:可自定义 headers、body、method 等
-
**简单直接**:适合一次性、低频操作(如删除、文件下载)
❌ 缺点
-
**重复代码多**:每次都要写认证、错误处理、CSRF
-
**无统一错误处理**:错误格式不一致
-
**无类型安全**:除非手动定义
-
**不支持方法名抽象**:URL 硬编码,难维护
🎯 适用场景
-
项目初期快速验证
-
一次性操作(如 `DELETE /api/xxx`)
-
不在主业务流程中的边缘功能
## 🧱 三、2. `API.invoke`:RPC 风格底层调用
💡 是什么?
-
项目自定义的 **RPC(远程过程调用)接口**
-
前端调用 **方法名**(如 `"changePassword"`),后端映射到具体函数
-
通常由 DRF 的 `@action` 自动生成
🔧 示例代码
```ts
// 调用后端注册的 "changePassword" 方法
const result = await API.invoke("changePassword", {
body: {
password: "old123",
new_password: "new456"
}
});
// result 结构(由后端约定)
// { $meta: { ok: true }, data: { detail: "成功" } }
```
🔧 后端对应(DRF)
```python
apps/users/views.py
class UserViewSet(viewsets.GenericViewSet):
@action(detail=False, methods=["patch"], url_path="change-password")
def change_password(self, request):
... 逻辑
return Response({"detail": "成功"})
```
> DRF 自动生成方法名 `changePassword`(驼峰)
✅ 优点
-
**语义清晰**:`changePassword` 比 `/api/users/change-password/` 更易读
-
**自动映射**:URL 由后端生成,前端无需关心
-
**类型安全**:配合 TypeScript 泛型
-
**统一响应结构**:如 `{ $meta, data }`
❌ 缺点
-
**耦合生成机制**:依赖 OpenAPI / DRF 自动生成
-
**调试困难**:方法名与 URL 不直观对应
-
**灵活性低**:不能自由指定 headers/method
🎯 适用场景
-
项目已建立 **RPC 风格 API 体系**
-
使用 **DRF ViewSet + @action** 架构
-
需要强类型和统一响应格式
🧱 四、3. `callApi`:项目封装的通用调用层
💡 是什么?
-
对 `API.invoke` 的 **进一步封装**
-
通常提供:
-
自动错误弹窗(toast)
-
全局 loading 状态
-
认证头自动注入
-
错误格式标准化
🔧 示例代码
```ts
// 在 React 组件中使用
import { useApi } from "@/hooks/useApi";
const MyComponent = () => {
const { callApi } = useApi();
const handleChangePassword = async () => {
const response = await callApi("changePassword", {
body: { password: "...", new_password: "..." }
});
if (!response?.ok) {
// 错误会自动显示,无需手动处理
return;
}
// 成功
toast.success("修改成功");
};
};
```
html
// 在 React 组件中使用
import { useApi } from "@/hooks/useApi";
const MyComponent = () => {
const { callApi } = useApi();
const handleChangePassword = async () => {
const response = await callApi("changePassword", {
body: { password: "...", new_password: "..." }
});
if (!response?.ok) {
// 错误会自动显示,无需手动处理
return;
}
// 成功
toast.success("修改成功");
};
};
```
🔧 内部实现(简化版)
html
```ts
// hooks/useApi.ts
export const useApi = () => {
const callApi = async (method: string, options: ApiCallOptions) => {
try {
const response = await API.invoke(method, options);
if (!response.$meta.ok) {
toast.error(response.data?.detail || "操作失败");
return { ok: false, data: response.data };
}
return { ok: true, data: response.data };
} catch (error) {
toast.error("网络错误");
return { ok: false, data: null };
}
};
return { callApi };
};
```
✅ 优点
-
**开发效率高**:自动处理 loading/error/toast
-
**一致性好**:整个项目 API 调用风格统一
-
**解耦业务逻辑**:组件只关注"做什么",不关心"怎么做"
❌ 缺点
-
**黑盒感强**:新手难以理解内部逻辑
-
**定制性差**:特殊需求(如自定义 header)需额外配置
-
**调试需跳转多层**
🎯 适用场景
-
**中大型项目**,需统一 API 调用规范
-
团队协作,避免重复造轮子
-
已有成熟错误处理/认证体系
📊 五、对比总结
| 特性 | `fetch` | `API.invoke` | `callApi` |
|------|--------|--------------|----------|
| **依赖** | 无 | 项目 SDK | 项目 Hook |
| **控制粒度** | 高 | 中 | 低 |
| **错误处理** | 手动 | 手动 | 自动 |
| **认证/CSRF** | 手动 | 自动 | 自动 |
| **类型安全** | 无 | 有(需生成) | 有 |
| **可维护性** | 低(URL 硬编码) | 高(方法名抽象) | 高 |
| **适用阶段** | 原型 / 小项目 | 中大型项目 | 成熟项目 |
🛠 六、如何选择?
| 你的项目状态 | 推荐方式 |
|--------------|----------|
| 刚起步,快速验证 | `fetch` |
| 已用 DRF ViewSet + 自动生成 SDK | `API.invoke` |
| 有统一错误处理、Toast、Loading 体系 | `callApi` |
| 需要最大灵活性(如上传、WebSocket) | `fetch` |
> ✅ **最佳实践**:
> - **主业务流程** → 用 `callApi`
> - **边缘操作**(如删除、导出)→ 用 `fetch`
> - **避免混用**:同一功能不要既用 `fetch` 又用 `callApi`
🔚 七、结语
三种方式不是互斥的,而是 **分层协作**:
```
React Component
↓
callApi() ← 开发者主要调用这里
↓
API.invoke() ← 自动生成的方法调用
↓
fetch() ← 底层 HTTP 请求
```
理解每一层的职责,才能写出**可维护、健壮、高效**的前端代码。
> 📝 **博客建议标题**:
> 《前端 API 调用三剑客:fetch、invoke 与 callApi 的实战对比》
> 《从 fetch 到 callApi:现代前端 API 调用演进之路》
欢迎根据你的项目细节补充示例和截图!如需 Markdown 源文件,我可直接提供。