第11章 HTTP请求与API交互
第31节 API接口管理
11.31.1 接口模块化组织
在实际项目中,我们会有很多API接口,如果把所有接口都写在一个文件中,会导致文件变得非常大,难以维护。因此,我们需要对API接口进行模块化组织,按照功能或业务模块来划分不同的接口文件。
接口文件结构
通常,我们会在src/api目录下创建不同的接口模块文件,例如:
src/
├── api/
│ ├── user.js # 用户相关接口
│ ├── product.js # 产品相关接口
│ ├── order.js # 订单相关接口
│ ├── cart.js # 购物车相关接口
│ └── index.js # 接口入口文件
├── utils/
│ └── request.js # Axios封装
└── ...接口模块示例
我们可以按照业务模块来创建不同的接口文件,例如用户相关接口:
// src/api/user.js
import request from '@/utils/request'
export const userApi = {
// 获取用户列表
getUsers(params) {
return request.get('/users', { params })
},
// 获取用户详情
getUserById(id) {
return request.get(`/users/${id}`)
},
// 创建用户
createUser(data) {
return request.post('/users', data)
},
// 更新用户
updateUser(id, data) {
return request.put(`/users/${id}`, data)
},
// 删除用户
deleteUser(id) {
return request.delete(`/users/${id}`)
},
// 用户登录
login(data) {
return request.post('/login', data)
},
// 用户登出
logout() {
return request.post('/logout')
},
// 获取当前用户信息
getCurrentUser() {
return request.get('/current-user')
}
}同样,我们可以创建产品相关接口:
// src/api/product.js
import request from '@/utils/request'
export const productApi = {
// 获取产品列表
getProducts(params) {
return request.get('/products', { params })
},
// 获取产品详情
getProductById(id) {
return request.get(`/products/${id}`)
},
// 创建产品
createProduct(data) {
return request.post('/products', data)
},
// 更新产品
updateProduct(id, data) {
return request.put(`/products/${id}`, data)
},
// 删除产品
deleteProduct(id) {
return request.delete(`/products/${id}`)
},
// 获取产品分类
getProductCategories() {
return request.get('/product-categories')
}
}接口入口文件
为了方便使用,我们可以创建一个入口文件,将所有接口模块导出:
// src/api/index.js
// 导出所有接口模块
export * from './user'
export * from './product'
export * from './order'
export * from './cart'这样,我们在组件中就可以通过一个导入语句来使用所有接口:
// 在组件中使用
import { userApi, productApi } from '@/api'
// 使用用户接口
userApi.login({ username: 'admin', password: '123456' })
// 使用产品接口
productApi.getProducts({ page: 1, pageSize: 10 })接口调用示例
在组件中,我们可以这样使用接口:
<template>
<div class="user-list">
<h3>用户列表</h3>
<el-table :data="users" style="width: 100%" border>
<el-table-column prop="id" label="ID" width="80"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column prop="email" label="邮箱"></el-table-column>
<el-table-column prop="role" label="角色"></el-table-column>
<el-table-column prop="createdAt" label="创建时间"></el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { userApi } from '@/api'
const users = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const loading = ref(false)
// 获取用户列表
const getUsers = async () => {
loading.value = true
try {
const result = await userApi.getUsers({
page: currentPage.value,
pageSize: pageSize.value
})
users.value = result.list
total.value = result.total
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
loading.value = false
}
}
// 编辑用户
const handleEdit = (row) => {
console.log('编辑用户:', row)
// 跳转到编辑页面或打开编辑对话框
}
// 删除用户
const handleDelete = async (id) => {
try {
await userApi.deleteUser(id)
// 重新获取用户列表
getUsers()
} catch (error) {
console.error('删除用户失败:', error)
}
}
// 页码变化
const handleSizeChange = (size) => {
pageSize.value = size
getUsers()
}
// 页数变化
const handleCurrentChange = (current) => {
currentPage.value = current
getUsers()
}
// 组件挂载时获取用户列表
onMounted(() => {
getUsers()
})
</script>
<style scoped>
.user-list {
max-width: 1200px;
margin: 0 auto;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>11.31.2 TypeScript接口类型定义
在TypeScript项目中,我们可以为API接口定义类型,提高代码的类型安全性和开发体验。
接口类型定义示例
我们可以创建一个类型定义文件,为API请求和响应定义类型:
// src/types/api.ts
// 用户相关类型
export interface User {
id: number
name: string
email: string
role: 'admin' | 'user' | 'guest'
createdAt: string
updatedAt: string
}
export interface UserListResponse {
list: User[]
total: number
page: number
pageSize: number
}
export interface LoginRequest {
username: string
password: string
}
export interface LoginResponse {
token: string
user: User
}
// 产品相关类型
export interface Product {
id: number
name: string
price: number
category: string
description: string
imageUrl: string
createdAt: string
updatedAt: string
}
export interface ProductListResponse {
list: Product[]
total: number
page: number
pageSize: number
}
// 通用类型
export interface PaginationParams {
page: number
pageSize: number
}
export interface ResponseData<T = any> {
code: number
message: string
data: T
}TypeScript接口模块
在TypeScript项目中,我们可以为API接口添加类型定义:
// src/api/user.ts
import request from '@/utils/request'
import type { User, UserListResponse, LoginRequest, LoginResponse, PaginationParams } from '@/types/api'
export const userApi = {
// 获取用户列表
getUsers(params: PaginationParams): Promise<UserListResponse> {
return request.get('/users', { params })
},
// 获取用户详情
getUserById(id: number): Promise<User> {
return request.get(`/users/${id}`)
},
// 创建用户
createUser(data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
return request.post('/users', data)
},
// 更新用户
updateUser(id: number, data: Partial<User>): Promise<User> {
return request.put(`/users/${id}`, data)
},
// 删除用户
deleteUser(id: number): Promise<void> {
return request.delete(`/users/${id}`)
},
// 用户登录
login(data: LoginRequest): Promise<LoginResponse> {
return request.post('/login', data)
},
// 用户登出
logout(): Promise<void> {
return request.post('/logout')
},
// 获取当前用户信息
getCurrentUser(): Promise<User> {
return request.get('/current-user')
}
}Axios类型扩展
我们还可以扩展Axios的类型,为响应数据添加类型定义:
// src/types/axios.d.ts
import type { ResponseData } from './api'
declare module 'axios' {
interface AxiosInstance {
request<T = any>(config: AxiosRequestConfig): Promise<T>
get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
head<T = any>(url: string, config?: AxiosRequestConfig): Promise<T>
post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>
put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>
patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T>
}
}11.31.3 Mock数据与开发环境配置
在前端开发过程中,后端API可能还没有开发完成,或者我们需要在本地进行测试。这时,我们可以使用Mock数据来模拟后端API的响应。
使用Mock.js
Mock.js是一个用于生成随机数据的库,可以用来模拟后端API的响应。
安装Mock.js
npm install mockjs -D使用Mock.js模拟API
我们可以创建一个Mock数据文件,用来模拟后端API的响应:
// src/mock/index.js
import Mock from 'mockjs'
// 模拟用户列表数据
Mock.mock('/api/users', 'get', (options) => {
const { page = 1, pageSize = 10 } = JSON.parse(options.body || '{}')
const total = 100
const list = Mock.mock({
'list|10': [{
'id|+1': 1,
'name': '@cname',
'email': '@email',
'role|1': ['admin', 'user', 'guest'],
'createdAt': '@datetime',
'updatedAt': '@datetime'
}]
}).list
return {
code: 0,
message: 'success',
data: {
list,
total,
page: Number(page),
pageSize: Number(pageSize)
}
}
})
// 模拟用户详情数据
Mock.mock(/\/api\/users\/\d+/, 'get', (options) => {
const id = options.url.match(/\d+/)[0]
return {
code: 0,
message: 'success',
data: {
id: Number(id),
name: Mock.Random.cname(),
email: Mock.Random.email(),
role: Mock.Random.pick(['admin', 'user', 'guest']),
createdAt: Mock.Random.datetime(),
updatedAt: Mock.Random.datetime()
}
}
})
// 模拟登录接口
Mock.mock('/api/login', 'post', (options) => {
const { username, password } = JSON.parse(options.body)
if (username === 'admin' && password === '123456') {
return {
code: 0,
message: '登录成功',
data: {
token: 'mock-token-' + Date.now(),
user: {
id: 1,
name: '管理员',
email: 'admin@example.com',
role: 'admin',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
}
}
} else {
return {
code: 401,
message: '用户名或密码错误',
data: null
}
}
})然后在项目入口文件中引入Mock数据:
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './store'
// 开发环境下引入Mock数据
if (import.meta.env.DEV) {
import('./mock/index.js')
}
const app = createApp(App)
app.use(router)
app.use(pinia)
app.mount('#app')使用Vite Plugin Mock
Vite Plugin Mock是一个Vite插件,可以更方便地使用Mock数据。
安装Vite Plugin Mock
npm install vite-plugin-mock mockjs -D配置Vite Plugin Mock
在Vite配置文件中添加Mock插件:
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig({
plugins: [
vue(),
viteMockServe({
// Mock文件目录
mockPath: './src/mock',
// 开发环境启用Mock
localEnabled: true,
// 生产环境禁用Mock
prodEnabled: false,
// 注入Mock脚本
injectCode: `
import { setupProdMockServer } from './mockProdServer';
setupProdMockServer();
`
})
]
})创建Mock文件
我们可以在src/mock目录下创建Mock文件:
// src/mock/user.js
export default [
{
url: '/api/users',
method: 'get',
response: (request) => {
const { page = 1, pageSize = 10 } = request.query
const total = 100
const list = []
for (let i = 0; i < Number(pageSize); i++) {
list.push({
id: (Number(page) - 1) * Number(pageSize) + i + 1,
name: `用户${i + 1}`,
email: `user${i + 1}@example.com`,
role: ['admin', 'user', 'guest'][Math.floor(Math.random() * 3)],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
})
}
return {
code: 0,
message: 'success',
data: {
list,
total,
page: Number(page),
pageSize: Number(pageSize)
}
}
}
},
{
url: '/api/users/:id',
method: 'get',
response: (request) => {
const { id } = request.params
return {
code: 0,
message: 'success',
data: {
id: Number(id),
name: `用户${id}`,
email: `user${id}@example.com`,
role: ['admin', 'user', 'guest'][Math.floor(Math.random() * 3)],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
}
}
}
]开发环境配置
在开发过程中,我们可以根据不同的环境配置不同的API基础URL。
环境变量配置
我们可以在项目根目录下创建不同的环境变量文件:
# .env # 公共环境变量
VITE_APP_NAME=Vue3Demo
# .env.development # 开发环境变量
VITE_API_BASE_URL=http://localhost:3000/api
# .env.production # 生产环境变量
VITE_API_BASE_URL=https://api.example.com
# .env.test # 测试环境变量
VITE_API_BASE_URL=https://test-api.example.com使用环境变量
在Axios配置文件中使用环境变量:
// src/utils/request.js
import axios from 'axios'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
export default request跨域配置
在开发过程中,我们可能会遇到跨域问题。这时,我们可以在Vite配置文件中配置代理。
Vite代理配置
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
// 配置代理
'/api': {
target: 'http://localhost:3000', // 后端API地址
changeOrigin: true, // 允许跨域
rewrite: (path) => path.replace(/^\/api/, '') // 重写路径
}
}
}
})这样,当我们请求/api/users时,Vite会将请求代理到http://localhost:3000/users。
最佳实践
接口模块化:
- 按照业务模块来组织API接口
- 每个模块对应一个接口文件
- 使用统一的入口文件导出所有接口
类型安全:
- 在TypeScript项目中为API接口添加类型定义
- 为请求参数和响应数据定义类型
- 使用类型别名和接口来提高代码的可维护性
Mock数据:
- 在开发环境中使用Mock数据模拟后端API
- 使用Mock.js或Vite Plugin Mock来生成Mock数据
- 根据不同环境配置不同的Mock策略
环境配置:
- 使用环境变量配置不同环境的API基础URL
- 配置跨域代理,方便本地开发
- 区分开发环境、测试环境和生产环境
错误处理:
- 在Axios拦截器中统一处理错误
- 为不同类型的错误提供不同的处理方式
- 显示友好的错误信息给用户
代码规范:
- 遵循统一的代码规范
- 使用ESLint和Prettier来检查和格式化代码
- 为接口函数添加注释,说明功能和参数
小结
本节我们学习了API接口管理,包括:
- 接口模块化组织,按照业务模块来划分不同的接口文件
- TypeScript接口类型定义,为API请求和响应添加类型
- Mock数据与开发环境配置,使用Mock.js和Vite Plugin Mock来模拟后端API
- 开发环境配置,包括环境变量和跨域配置
通过合理管理API接口,我们可以提高开发效率,减少重复代码,同时提高代码的可维护性和可扩展性。在实际项目中,我们可以根据具体需求选择合适的API管理方式,以满足项目的需要。
思考与练习
- 按照业务模块组织API接口文件。
- 为API接口添加TypeScript类型定义。
- 使用Mock.js模拟后端API的响应。
- 配置不同环境的API基础URL。
- 配置Vite代理解决跨域问题。