一、简介
在开发中,你可能遇到过 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
- 动态路径参数,
orderId
、userId
为动态变更
三、分析之前使用 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>
优点
- 快捷方便:直接使用
request
方法可以快速发起请求,代码简洁,开发速度快,适合简单的场景。
缺点
- 接口权限不清晰:由于权限信息没有明确绑定在接口上,导致在发起请求时难以清楚地了解所需权限,增加了开发和维护的难度。
- 接口管理不便:路径和方法定义分散,修改接口路径或管理多个接口时比较麻烦,不利于代码的一致性和可维护性。
- 不适合RESTful接口:在处理RESTful风格的接口时,代码结构不够清晰,容易混淆请求方法、路径和参数。
- 模块信息不明确:无法通过代码直观地看到接口所在的模块或用途,导致项目的可读性和组织性较差。
四、TypeScript 与 Axios 前端封装的实践
在进行 TypeScript 和 Axios 前端封装时,可以通过以下改进方法来解决之前所提到的问题。
问题分析:
- 权限绑定:可以通过定义接口类,将接口路径和权限信息绑定在一起,确保在使用接口时清晰了解所需的权限。
- 路径剥离:将接口路径集中管理,避免路径和方法定义分散,提升代码的一致性和可维护性。
- 模块划分:使用接口类将接口划分为不同模块,明确各接口的用途和所在模块,提高项目的可读性和组织性。
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
方法的开发者来说,可能需要一段时间适应。 -
增加了一些代码复杂度:将接口定义为类属性,虽然提高了代码的组织性,但也增加了部分复杂度,尤其对于小型项目来说,这种设计可能显得过于复杂。
五、源码
前端源码位置 : yf/ yf-vue-admin / src / api
注意事项 :
- 平台一人一号,账号可以通过邮箱、第三方平台自动注册。用户名密码方式登录请联系管理员手动添加、手机号不可用。(敏感数据以做信息脱敏)
- 在线聊天功能(消息已做脏词过滤,群发、系统、AI消息不会被平台记录)
- 欢迎大家提出意见,欢迎畅聊与项目相关问题
六、结束语
每个人在设计代码时,都会根据自身的项目需求和经验采取不同的方案,正如莎士比亚所言:“一万个人心中有一万个哈姆雷特”。有人追求代码的简洁与快速开发,有人则更注重结构化与可扩展性。无论选择哪种方式,都没有绝对的对与错,只有最适合当前项目和团队的方案。希望通过这些分析与实践,能够为你提供新的思路,帮助你更好地权衡与选择适合自己的开发方式。