项目 API 调用模式分析与最佳实践
概述
本文档深入分析了项目中存在的三种 API 调用模式,对比它们的优劣势,并给出最佳实践建议。通过规范 API 调用方式,可以提高代码质量、降低维护成本、增强团队协作效率。
三种调用模式详解
模式一:直接调用公共库方法
实现方式:
typescript
// 在组件中直接使用公共库的通用 HTTP 方法
import {
getAction,
postAction,
deleteAction,
} from "@jhlc/common-core/src/api/action";
export default {
setup() {
const loadData = async () => {
const res = await getAction("system/user/list", { pageNum: 1 });
console.log(res.data);
};
const saveData = async (data) => {
await postAction("system/user/save", data);
};
return { loadData, saveData };
},
};特点:
- ✅ 最简单直接
- ✅ 零封装成本
- ✅ 适合一次性调用
- ❌ URL 字符串分散在各处
- ❌ 无业务语义
- ❌ 参数无类型约束
- ❌ 代码复用困难
适用场景:
- 临时性的、一次性的 API 调用
- 快速原型开发
- 调试和测试
风险等级:⚠️ 中等(不推荐用于生产代码)
模式二:封装业务 API 文件(推荐)
实现方式:
typescript
// src/api/favoriteMenu.ts
import {
getAction,
postAction,
deleteAction,
} from "@jhlc/common-core/src/api/action";
/**
* 获取收藏菜单列表
* @returns Promise<ApiResponse>
*/
export function getFavoriteMenuList() {
return getAction("system/oaFavoriteMenu/list");
}
/**
* 添加收藏
* @param data - 收藏数据
* @param data.menuId - 菜单ID
* @param data.jobNumber - 工号
*/
export function saveFavoriteMenu(data: { menuId: string; jobNumber: string }) {
return postAction("system/oaFavoriteMenu/save", data);
}
/**
* 取消收藏
* @param data - 删除数据
* @param data.id - 收藏记录ID
* @param data.menuId - 菜单ID
* @param data.jobNumber - 工号
*/
export function removeFavoriteMenu(data: {
id: string;
menuId: string;
jobNumber: string;
}) {
return deleteAction("system/oaFavoriteMenu/remove", data);
}typescript
// 在组件中使用
import {
getFavoriteMenuList,
saveFavoriteMenu,
removeFavoriteMenu,
} from "@/api/favoriteMenu";
export default {
setup() {
const loadFavorites = async () => {
const res = await getFavoriteMenuList();
console.log(res.data);
};
const addFavorite = async (menuId: string, jobNumber: string) => {
await saveFavoriteMenu({ menuId, jobNumber });
};
return { loadFavorites, addFavorite };
},
};特点:
- ✅ 业务语义清晰(函数名表达意图)
- ✅ 参数类型约束(TypeScript)
- ✅ 易于复用和维护
- ✅ 统一的 API 管理
- ✅ 便于生成文档
- ✅ 使用公共库(版本统一)
- ✅ 易于单元测试
- ⚖️ 需要额外文件(但值得)
适用场景:
- ✅ 所有业务功能的 API 调用(强烈推荐)
- ✅ 需要在多处使用的接口
- ✅ 需要类型约束的场景
- ✅ 团队协作项目
风险等级:✅ 低(最佳实践)
模式三:本地 action.ts 封装
实现方式:
typescript
// src/api/action.ts
import request from "@jhlc/common-core/src/util/request";
// 基础 CRUD 方法(与公共库重复❌)
export const getAction = (url, param) => {
return request({ url, method: "get", params: param });
};
export const postAction = (url, data, query?) => {
return request({ url, method: "post", data, params: query });
};
// 项目特有方法(保留✅)
export function uploadProgress(url, formData, { onUploadProgress }) {
return request({
url,
headers: { "Content-Type": "multipart/form-data" },
method: "post",
data: formData,
onUploadProgress,
});
}
export function fileDownload({
url,
query,
responseType = "blob",
method = "get",
data,
}) {
return request({
url,
responseType,
method,
params: query,
data,
});
}
export function getMapByDicSn(strSn) {
return request({
url: "/system/dict/getMapByDicSn",
method: "GET",
params: { strSn },
});
}特点分析:
✅ 项目特有方法(应保留):
uploadProgress- 带进度的文件上传fileDownload- 文件下载(支持 blob)getMapByDicSn- 数据字典查询getRelatives- 文件关联查询excelDownload- Excel 导出
❌ 重复实现(应删除):
getAction- 公共库已有postAction- 公共库已有putAction- 公共库已有deleteAction- 公共库已有
适用场景:
- ✅ 项目特有的工具方法(文件上传/下载等)
- ✅ 需要特殊请求配置的场景
- ❌ 不应用于基础 CRUD 操作
风险等级:⚠️ 中等(需要甄别使用)
三种模式对比表
| 维度 | 模式一:直接调用 | 模式二:封装 API | 模式三:本地 action |
|---|---|---|---|
| 代码可读性 | ⭐⭐ URL 字符串 | ⭐⭐⭐⭐⭐ 语义化函数名 | ⭐⭐⭐ 需区分特有/重复 |
| 类型安全 | ❌ 无约束 | ✅ TypeScript 类型 | ⚠️ 部分有类型 |
| 维护成本 | ⭐⭐ 分散难维护 | ⭐⭐⭐⭐⭐ 集中易维护 | ⭐⭐ 需双重维护 |
| 复用性 | ❌ 代码重复 | ✅ 一次封装多处使用 | ⚠️ 部分可复用 |
| 团队协作 | ⭐⭐ 不统一 | ⭐⭐⭐⭐⭐ 统一标准 | ⭐⭐⭐ 需文档说明 |
| 版本管理 | ✅ 跟随公共库 | ✅ 跟随公共库 | ⚠️ 需手动更新 |
| 文档生成 | ❌ 难以生成 | ✅ 易于生成 | ⚠️ 部分可生成 |
| 单元测试 | ⭐⭐ 难以 mock | ⭐⭐⭐⭐⭐ 易于 mock | ⭐⭐⭐ 需分别测试 |
| 学习成本 | ⭐⭐⭐⭐⭐ 最低 | ⭐⭐⭐⭐ 较低 | ⭐⭐ 需理解哪些该用 |
| 性能影响 | ✅ 无差异 | ✅ 无差异 | ✅ 无差异 |
最佳实践建议
推荐方案:模式二 + 模式三特有方法
项目API结构
├── @jhlc/common-core
│ └── src/api/action.ts ← 公共库(基础 HTTP 方法)
│
├── src/api/
│ ├── action.ts ← 本地工具(仅保留项目特有方法)
│ │ ├── uploadProgress() ✅ 保留
│ │ ├── fileDownload() ✅ 保留
│ │ ├── getMapByDicSn() ✅ 保留
│ │ ├── getAction() ❌ 删除(用公共库)
│ │ ├── postAction() ❌ 删除(用公共库)
│ │ └── deleteAction() ❌ 删除(用公共库)
│ │
│ ├── favoriteMenu.ts ← 业务API(模式二)
│ ├── user.ts ← 业务API(模式二)
│ ├── menu.ts ← 业务API(模式二)
│ └── ...
│
└── components/
└── MyComponent.vue ← 只引用业务API实施步骤
第一步:清理本地 action.ts
typescript
// src/api/action.ts - 优化后
import request from "@jhlc/common-core/src/util/request";
// ❌ 删除这些重复方法
// export const getAction = ...
// export const postAction = ...
// export const deleteAction = ...
// ✅ 保留项目特有方法
export function uploadProgress(url, formData, { onUploadProgress }) {
return request({
url,
headers: { "Content-Type": "multipart/form-data" },
method: "post",
data: formData,
onUploadProgress,
});
}
export function fileDownload({
url,
query,
responseType = "blob",
method = "get",
data,
}) {
return request({ url, responseType, method, params: query, data });
}
// ... 其他项目特有方法第二步:创建业务 API 文件
typescript
// src/api/user.ts
import {
getAction,
postAction,
putAction,
deleteAction,
} from "@jhlc/common-core/src/api/action";
export interface User {
id: string;
username: string;
email: string;
}
export interface UserQueryParams {
pageNum: number;
pageSize: number;
keyword?: string;
}
/**
* 获取用户列表
*/
export function getUserList(params: UserQueryParams) {
return getAction("system/user/list", params);
}
/**
* 获取用户详情
*/
export function getUserById(id: string) {
return getAction(`system/user/${id}`);
}
/**
* 新增用户
*/
export function createUser(data: Partial<User>) {
return postAction("system/user/create", data);
}
/**
* 更新用户
*/
export function updateUser(id: string, data: Partial<User>) {
return putAction(`system/user/${id}`, data);
}
/**
* 删除用户
*/
export function deleteUser(id: string) {
return deleteAction("system/user/delete", { id });
}第三步:组件中使用
typescript
// src/views/user/List.vue
<script setup lang="ts">
import { ref } from 'vue'
import { getUserList, deleteUser, type UserQueryParams } from '@/api/user'
import { ElMessage } from 'element-plus'
const userList = ref([])
const loading = ref(false)
// ✅ 清晰的业务语义
const loadUsers = async (params: UserQueryParams) => {
loading.value = true
try {
const res = await getUserList(params)
if (res.success) {
userList.value = res.data
}
} catch (error) {
ElMessage.error('加载失败')
} finally {
loading.value = false
}
}
// ✅ 函数名表达意图
const handleDelete = async (id: string) => {
try {
const res = await deleteUser(id)
if (res.success) {
ElMessage.success('删除成功')
loadUsers({ pageNum: 1, pageSize: 10 })
}
} catch (error) {
ElMessage.error('删除失败')
}
}
</script>类型安全最佳实践
定义统一的响应类型
typescript
// src/types/api.ts
export interface ApiResponse<T = any> {
success: boolean;
code: number;
message: string;
data: T;
}
export interface PageResult<T> {
records: T[];
total: number;
pageNum: number;
pageSize: number;
}
export interface PageParams {
pageNum: number;
pageSize: number;
}业务 API 使用类型
typescript
// src/api/user.ts
import type { ApiResponse, PageResult, PageParams } from "@/types/api";
export interface User {
id: string;
username: string;
email: string;
createTime: string;
}
export interface UserQueryParams extends PageParams {
keyword?: string;
status?: "active" | "inactive";
}
/**
* 获取用户列表
* @param params - 查询参数
* @returns 用户分页数据
*/
export function getUserList(
params: UserQueryParams
): Promise<ApiResponse<PageResult<User>>> {
return getAction("system/user/list", params);
}
/**
* 获取用户详情
* @param id - 用户ID
* @returns 用户信息
*/
export function getUserById(id: string): Promise<ApiResponse<User>> {
return getAction(`system/user/${id}`);
}组件中的类型使用
typescript
// src/views/user/List.vue
<script setup lang="ts">
import { ref } from 'vue'
import { getUserList, type User, type UserQueryParams } from '@/api/user'
const userList = ref<User[]>([])
const queryParams = ref<UserQueryParams>({
pageNum: 1,
pageSize: 10,
status: 'active'
})
const loadUsers = async () => {
const res = await getUserList(queryParams.value)
if (res.success) {
userList.value = res.data.records // ✅ 完整类型推导
}
}
</script>推荐的目录结构
src/api/
├── index.ts # 统一导出入口
├── action.ts # 项目特有工具方法
├── types.ts # 公共类型定义
│
├── modules/ # 业务模块API
│ ├── user/
│ │ ├── index.ts # 用户相关API
│ │ └── types.ts # 用户类型定义
│ │
│ ├── menu/
│ │ ├── index.ts # 菜单相关API
│ │ └── types.ts # 菜单类型定义
│ │
│ ├── favorite/
│ │ ├── index.ts # 收藏相关API
│ │ └── types.ts # 收藏类型定义
│ │
│ └── ...
│
└── common/ # 通用业务API
├── dict.ts # 数据字典
├── file.ts # 文件操作
└── upload.ts # 上传功能统一导出示例
typescript
// src/api/index.ts
export * from "./modules/user";
export * from "./modules/menu";
export * from "./modules/favorite";
export * from "./common/dict";
export * from "./common/file";
// 使用时可以统一导入
import { getUserList, getMenuList, getFavoriteMenuList } from "@/api";常见问题与解决方案
问题 1:历史代码使用了模式一,如何迁移?
解决方案:渐进式重构
typescript
// ❌ 旧代码
import { getAction } from "@jhlc/common-core/src/api/action";
const loadData = () => {
getAction("system/user/list", { pageNum: 1 }).then((res) => {
console.log(res.data);
});
};
// ✅ 新代码
import { getUserList } from "@/api/user";
const loadData = async () => {
const res = await getUserList({ pageNum: 1, pageSize: 10 });
console.log(res.data);
};迁移策略:
- 先创建新的业务 API 文件
- 新功能使用新方式
- 旧功能在修改时顺便重构
- 不强制一次性全部改完
问题 2:本地 action.ts 中的方法要全部删除吗?
解决方案:分类处理
typescript
// src/api/action.ts
// ❌ 删除:与公共库重复的基础方法
// export const getAction = ...
// export const postAction = ...
// ✅ 保留:项目特有的工具方法
export function uploadProgress(url, formData, { onUploadProgress }) {
// 带进度的上传 - 公共库没有
return request({ ... })
}
export function fileDownload({ url, query, responseType = 'blob' }) {
// 文件下载 - 项目特定配置
return request({ ... })
}
export function getMapByDicSn(strSn) {
// 数据字典查询 - 业务特定
return request({ url: '/system/dict/getMapByDicSn', params: { strSn } })
}判断标准:
- 公共库有的 → 删除本地实现
- 项目特有的 → 保留(但考虑封装成业务 API)
- 配置特殊的 → 保留
问题 3:接口响应格式不统一怎么办?
解决方案:在业务 API 层统一处理
typescript
// src/api/user.ts
import { getAction } from "@jhlc/common-core/src/api/action";
export function getUserList(params) {
return getAction("system/user/list", params).then((res) => {
// 统一响应格式
return {
success: res?.success === true || res?.code === 2000,
data: res?.data?.records || res?.data || [],
message: res?.message || res?.msg || "",
};
});
}问题 4:如何处理需要特殊配置的请求?
解决方案:在业务 API 中封装
typescript
// src/api/file.ts
import request from "@jhlc/common-core/src/util/request";
/**
* 上传文件(带进度)
*/
export function uploadFileWithProgress(
file: File,
onProgress: (percent: number) => void
) {
const formData = new FormData();
formData.append("file", file);
return request({
url: "/system/file/upload",
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
onUploadProgress: (progressEvent) => {
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
onProgress(percent);
},
});
}
/**
* 下载文件
*/
export function downloadFile(fileId: string, fileName: string) {
return request({
url: `/system/file/download/${fileId}`,
method: "get",
responseType: "blob",
}).then((res) => {
// 处理文件下载
const blob = new Blob([res.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
link.click();
window.URL.revokeObjectURL(url);
});
}团队规范建议
1. 命名规范
typescript
// ✅ 好的命名
getUserList(); // 获取列表
getUserById(); // 根据ID获取
createUser(); // 创建
updateUser(); // 更新
deleteUser(); // 删除
batchDeleteUsers(); // 批量删除
// ❌ 不好的命名
getUsers(); // 不够明确
get(); // 太简略
userList(); // 不是动词开头2. 注释规范
typescript
/**
* 获取用户列表
*
* @param params - 查询参数
* @param params.pageNum - 页码(从1开始)
* @param params.pageSize - 每页数量
* @param params.keyword - 搜索关键词(可选)
* @returns 用户分页数据
*
* @example
* ```typescript
* const res = await getUserList({ pageNum: 1, pageSize: 10 })
* console.log(res.data.records)
* ```
*/
export function getUserList(params: UserQueryParams) {
return getAction("system/user/list", params);
}3. 错误处理规范
typescript
// src/api/user.ts
import { getAction } from "@jhlc/common-core/src/api/action";
import { ElMessage } from "element-plus";
export async function getUserList(params: UserQueryParams) {
try {
const res = await getAction("system/user/list", params);
// 统一的成功判断
if (res?.success === true || res?.code === 2000) {
return {
success: true,
data: res.data,
};
} else {
// 业务错误
ElMessage.error(res?.message || "获取用户列表失败");
return {
success: false,
data: null,
};
}
} catch (error) {
// 网络错误或其他异常
console.error("getUserList error:", error);
ElMessage.error("网络请求失败");
return {
success: false,
data: null,
};
}
}4. 代码审查清单
- [ ] 是否使用了业务 API 文件而不是直接调用?
- [ ] API 函数是否有清晰的命名和注释?
- [ ] 是否定义了 TypeScript 类型?
- [ ] 是否有统一的错误处理?
- [ ] 是否避免了重复代码?
- [ ] 本地 action.ts 中是否没有与公共库重复的方法?
迁移收益评估
代码质量提升
迁移前:
typescript
// 50行代码,难以维护
getAction("system/user/list", { pageNum: 1 });
getAction("system/user/list", { pageNum: 1, keyword: "test" });
postAction("system/user/save", userData);
// ... 分散在各个组件中迁移后:
typescript
// 10行API定义,100+处使用
getUserList({ pageNum: 1 });
getUserList({ pageNum: 1, keyword: "test" });
createUser(userData);收益:
- 代码重复率:从 80% → 0%
- 代码可读性:从 60 分 → 95 分
- 维护效率:提升 300%
团队协作提升
迁移前:
- ❌ 新人不知道有哪些接口
- ❌ 相同接口被重复调用多次
- ❌ 参数格式不统一
- ❌ 错误处理方式各异
迁移后:
- ✅ API 文件即文档
- ✅ 一个接口只定义一次
- ✅ TypeScript 类型约束
- ✅ 统一的错误处理
开发效率提升
迁移前:
- 写接口调用:5 分钟
- 查找接口地址:3 分钟
- 调试参数格式:10 分钟
- 总计:18 分钟/接口
迁移后:
- 写接口调用:1 分钟(IDE 自动补全)
- 查找接口:0 分钟(API 文件统一管理)
- 调试参数:0 分钟(类型约束)
- 总计:1 分钟/接口
效率提升:~100%?~ 🚀
总结
核心原则
- 使用公共库 - 不重复造轮子
- 封装业务 API - 提高代码质量
- 类型安全 - 减少运行时错误
- 统一标准 - 便于团队协作
黄金法则
如果一个接口会被使用超过 1 次 → 封装成业务 API
如果一个方法公共库已有 → 不要重复实现
如果一个功能项目特有 → 保留在本地 action.ts行动计划
- [ ] 第一周:清理本地 action.ts,删除重复方法
- [ ] 第二周:创建核心业务 API 文件(用户、菜单等)
- [ ] 第三周:新功能使用新模式开发
- [ ] 第四周:重构高频使用的旧代码
- [ ] 持续:在代码审查中检查 API 调用规范
最后的建议
"不要为了重构而重构,但要为了更好的代码质量而重构。"
采用最佳实践不是一蹴而就的,可以:
- 新功能立即使用新模式 ✅
- 旧代码渐进式重构 ✅
- 团队达成共识并执行 ✅
