Skip to content

项目 API 调用模式分析与最佳实践

📝 作者
杨晨誉
杨晨誉资深开发工程师
工号:409322信息化部

概述

本文档深入分析了项目中存在的三种 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);
};

迁移策略

  1. 先创建新的业务 API 文件
  2. 新功能使用新方式
  3. 旧功能在修改时顺便重构
  4. 不强制一次性全部改完

问题 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%?~ 🚀

总结

核心原则

  1. 使用公共库 - 不重复造轮子
  2. 封装业务 API - 提高代码质量
  3. 类型安全 - 减少运行时错误
  4. 统一标准 - 便于团队协作

黄金法则

如果一个接口会被使用超过 1 次 → 封装成业务 API
如果一个方法公共库已有 → 不要重复实现
如果一个功能项目特有 → 保留在本地 action.ts

行动计划

  • [ ] 第一周:清理本地 action.ts,删除重复方法
  • [ ] 第二周:创建核心业务 API 文件(用户、菜单等)
  • [ ] 第三周:新功能使用新模式开发
  • [ ] 第四周:重构高频使用的旧代码
  • [ ] 持续:在代码审查中检查 API 调用规范

最后的建议

"不要为了重构而重构,但要为了更好的代码质量而重构。"

采用最佳实践不是一蹴而就的,可以:

  • 新功能立即使用新模式 ✅
  • 旧代码渐进式重构 ✅
  • 团队达成共识并执行 ✅

📚 参考资源

You may not distribute, modify, or sell this software without permission.