让后端开发赞不绝口的 API 封装技巧:用 Axios 实现高效前端请求管理

1,741 阅读7分钟

一、简介

在开发中,你可能遇到过 API 调用杂乱无章、代码维护困难的问题。别担心,今天我们聊聊如何用 TypeScript 和 Axios 打造一个让后端开发也不得不点赞的 API 封装。无论你是初学者还是老手,这篇文章都会带你走上更优雅、更高效的开发之路!

二、了解后端如何编写 RESTful API

1. 了解 RESTful API 的路径表示

  • 资源的层次结构:解释如何使用路径来表示资源的层次结构,如 /users/{userId}/posts 表示某个用户的所有帖子。
  • 使用复数命名资源:讨论为何使用复数形式(如 /users 而非 /user)来命名资源,使路径更具一致性。
  • 通过路径传递状态或动作:介绍如何通过路径表示不同的状态或动作,例如 /orders/{orderId}/cancel 表示取消订单操作。
  • 路径中的版本控制:探讨如何在路径中添加版本信息,如 /v1/users,以便将来的 API 更新能更平稳地进行。
  • 避免动词:强调路径中应避免使用动词,保持 RESTful 风格,例如使用 /orders/{orderId} 而不是 /getOrderById

2. 了解后端如何编写的 RESTful API

接下来介绍 SpringBoot 和 Node 编写后端接口的大致案例 ( 注:比较简化 )

2.1 SpringBoot 编写的后端

@RestController
@RequestMapping("/users")
public class DemoController {

    // 根据用户ID获取单个用户
    @GetMapping("/{userId}")
    public String getUserById(@PathVariable String userId) {
        return "获取用户ID为 " + userId + " 的用户";
    }

    // 删除用户
    @DeleteMapping("/{userId}")
    public String deleteUser(@PathVariable String userId) {
        return "删除用户ID为 " + userId + " 的用户";
    }

    // 获取指定用户的所有帖子
    @GetMapping("/{userId}/posts")
    public String getUserPosts(@PathVariable String userId) {
        return "获取用户ID为 " + userId + " 的所有帖子";
    }

    // 取消指定订单
    @PostMapping("/orders/{orderId}/cancel")
    public String cancelOrder(@PathVariable String orderId) {
        return "取消订单ID为 " + orderId + " 的订单";
    }
}

2.2 Node.js 编写的后端

const express = require('express');
const router = express.Router();

// 添加路径前缀 /users
router.use('/users', (req, res, next) => {
  next();
});

// 根据用户ID获取单个用户
router.get('/:userId', (req, res) => {
  const userId = req.params.userId;
  res.send(`获取用户ID为 ${userId} 的用户`);
});

// 删除用户
router.delete('/:userId', (req, res) => {
  const userId = req.params.userId;
  res.send(`删除用户ID为 ${userId} 的用户`);
});

// 获取指定用户的所有帖子
router.get('/:userId/posts', (req, res) => {
  const userId = req.params.userId;
  res.send(`获取用户ID为 ${userId} 的所有帖子`);
});

// 取消指定订单
router.post('/orders/:orderId/cancel', (req, res) => {
  const orderId = req.params.orderId;
  res.send(`取消订单ID为 ${orderId} 的订单`);
});

module.exports = router;

2.3 分析接口相同点

  • 基础路径和路径前缀,/users
  • 动态路径参数,orderIduserId 为动态变更

三、分析之前使用 axios 请求的优缺点

1. 简单看一下之前在项目中 api 的使用

  • 接口定义
/** 接口描述 */
export function XXX(body: XXXType) {
  return request<XXXResponse>('uri', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    data: body
  });
}

/** 接口描述 */
export function XXX(avatar: File) {

  const formData = new FormData();
  formData.append('avatar', avatar);
  
  return request<XXXResponse>('uri', {
    method: 'GET',
    headers: {
      'Content-Type': 'multipart/form-data',
    },
    data: formData
  });
}
  • 接口使用
<template>
    <div v-permission="['XXX:permission']"> </div>
</template>

<script lang="ts" setup>
// 引入后调用
XXX(body).then(({data}) => {
    
})
</script>

优点

  1. 快捷方便:直接使用 request 方法可以快速发起请求,代码简洁,开发速度快,适合简单的场景。

缺点

  1. 接口权限不清晰:由于权限信息没有明确绑定在接口上,导致在发起请求时难以清楚地了解所需权限,增加了开发和维护的难度。
  2. 接口管理不便:路径和方法定义分散,修改接口路径或管理多个接口时比较麻烦,不利于代码的一致性和可维护性。
  3. 不适合RESTful接口:在处理RESTful风格的接口时,代码结构不够清晰,容易混淆请求方法、路径和参数。
  4. 模块信息不明确:无法通过代码直观地看到接口所在的模块或用途,导致项目的可读性和组织性较差。

四、TypeScript 与 Axios 前端封装的实践

在进行 TypeScript 和 Axios 前端封装时,可以通过以下改进方法来解决之前所提到的问题。

问题分析

  1. 权限绑定:可以通过定义接口类,将接口路径和权限信息绑定在一起,确保在使用接口时清晰了解所需的权限。
  2. 路径剥离:将接口路径集中管理,避免路径和方法定义分散,提升代码的一致性和可维护性。
  3. 模块划分:使用接口类将接口划分为不同模块,明确各接口的用途和所在模块,提高项目的可读性和组织性。

1. 代码实践

改进的代码示例

const API_BASE = '/user';

const API_SUFFIXES = {
    /** 获取当前登录的用户信息 */
    ME: '/me',
    /** 分页获取用户数据 */
    PAGE: '/page',
    /** 获取表单数据 */
    FORM: '/{userId}/form',
    /** 新增用户 ( POST 请求 ) */
    SAVE: '',
    /** 删除用户 */
    DELETE: '/{userIds}',
    /** 修改用户 */
    UPDATE: '/{userId}',
    /** 修改用户状态 */
    UPDATE_STATUS: '/{userId}/status',
    /** 管理员修改用户密码 */
    ADMIN_RESET_PASSWORD: '/{userId}/reset-password',
};

// 定义 USER_API 类
export class UserAPI {
    // 其他接口省略...

    /**
     * 获取用户表单数据
     * @param userId 用户Id
     */
    static FORM = {
        endpoint: (userId: string): string => {
            return `${API_BASE}${API_SUFFIXES.FORM.replace("{userId}", userId)}`;
        },
        permission: "system:user:update",
        request: (userId: string): AxiosPromise<UserForm> => {
            return request<UserForm>({
                url: UserAPI.FORM.endpoint(userId),
                method: "get",
            })
        }
    }
}

再看一个接口:

const API_BASE = '/user/profile';

const API_SUFFIXES = {
    /** 获取个人信息 */
    INFO: "",
    /** 修改个人信息 */
    UPDATE: "",
    /** 绑定第三方账号 */
    BIND_THIRD_PARTY: "/{type}/bind-third-party",
    /** 解绑第三方账号 */
    UNBIND_THIRD_PARTY: "/{oauthId}/unbind-third-party",
    /** 修改密码 */
    UPDATE_PASSWORD: "/password",
    /** 修改用户头像 */
    UPLOAD_AVATAR: "/avatar",
};

export class UserProfileAPI {
    // 其他接口省略 ...

    /**
     * 修改个人信息
     */
    static UPDATE = {
        endpoint: `${API_BASE}${API_SUFFIXES.UPDATE}`,
        request: (userProfileForm: UserProfileForm): AxiosPromise<void> => {
            return request<void>({
                url: UserProfileAPI.UPDATE.endpoint,
                method: "put",
                data: userProfileForm
            })
        }
    }
    /**
     * 修改头像
     */
    static UPLOAD_AVATAR = {
        endpoint: `${API_BASE}${API_SUFFIXES.UPLOAD_AVATAR}`,
        maxFileSize: 10 * 1024 * 1024, // 10M
        allowedFileTypes: ['image/bmp', 'image/png', 'image/jpeg', 'image/gif'],
        request: (avatar: File): AxiosPromise<string> => {
            // 1. 创建一个FormData对象并附加文件
            const formData = new FormData();
            formData.append('avatar', avatar);
            // 2. 请求更改头像
            return request<string>({
                url: UserProfileAPI.UPLOAD_AVATAR.endpoint,
                method: "patch",
                data: formData,
                headers: {
                    'Content-Type': 'multipart/form-data'
                }
            })
        }
    }
}

2. 封装后使用

当我们使用接口时

<template>
    <!-- 权限校验 -->
    <div v-permission="[XXXAPI.SAVE.permission]"> </div>
</template>

<script lang="ts" setup>
// 引入后使用
XXXAPI.SAVE.request(body).then(({data}) => {
    
})
// 接口配置校验
XXXAPI.UPLOAD.maxFileSize

3. 分析优缺点

优点:

  • 模块清晰:通过使用接口类(如 UserProfileAPI),可以明确接口所属的模块。每个接口都被归类到相关模块中,使代码结构更加清晰,便于维护。

  • 配置直观: 接口的配置信息(如路径、权限、文件大小限制等)作为类的属性公开,这样可以在类定义中直观地看到接口的相关信息。开发者无需深入到代码内部,便可了解接口的详细配置。

  • 路径管理方便: 接口路径被集中管理在 API_SUFFIXES 中,这样当路径需要修改时,只需更新对应的常量值,避免了路径散落在代码中的情况,提高了代码的一致性和可维护性。

  • 接口权限清晰:通过将权限信息绑定到具体的接口配置中(如 permission 属性),可以在使用接口时清楚地知道所需的权限,从而减少开发和维护的复杂性。

  • 灵活的请求配置: 每个接口的请求逻辑都可以独立配置和扩展,比如 UPLOAD_AVATAR 接口不仅定义了路径,还包含了文件大小限制和类型限制,这种配置的灵活性提高了代码的可扩展性和复用性。

缺点:

  • 学习成本增加:这种方式需要开发者理解和掌握封装接口类的概念,对于习惯于直接使用 request 方法的开发者来说,可能需要一段时间适应。

  • 增加了一些代码复杂度:将接口定义为类属性,虽然提高了代码的组织性,但也增加了部分复杂度,尤其对于小型项目来说,这种设计可能显得过于复杂。

五、源码

源码地址 | 👀 在线演示 | 觉得不错可以给个start

前端源码位置 : yf/ yf-vue-admin / src / api

注意事项 :

    1. 平台一人一号,账号可以通过邮箱、第三方平台自动注册。用户名密码方式登录请联系管理员手动添加、手机号不可用。(敏感数据以做信息脱敏)
    1. 在线聊天功能(消息已做脏词过滤,群发、系统、AI消息不会被平台记录)
    1. 欢迎大家提出意见,欢迎畅聊与项目相关问题

六、结束语

每个人在设计代码时,都会根据自身的项目需求和经验采取不同的方案,正如莎士比亚所言:“一万个人心中有一万个哈姆雷特”。有人追求代码的简洁与快速开发,有人则更注重结构化与可扩展性。无论选择哪种方式,都没有绝对的对与错,只有最适合当前项目和团队的方案。希望通过这些分析与实践,能够为你提供新的思路,帮助你更好地权衡与选择适合自己的开发方式。