第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

最佳实践

  1. 接口模块化

    • 按照业务模块来组织API接口
    • 每个模块对应一个接口文件
    • 使用统一的入口文件导出所有接口
  2. 类型安全

    • 在TypeScript项目中为API接口添加类型定义
    • 为请求参数和响应数据定义类型
    • 使用类型别名和接口来提高代码的可维护性
  3. Mock数据

    • 在开发环境中使用Mock数据模拟后端API
    • 使用Mock.js或Vite Plugin Mock来生成Mock数据
    • 根据不同环境配置不同的Mock策略
  4. 环境配置

    • 使用环境变量配置不同环境的API基础URL
    • 配置跨域代理,方便本地开发
    • 区分开发环境、测试环境和生产环境
  5. 错误处理

    • 在Axios拦截器中统一处理错误
    • 为不同类型的错误提供不同的处理方式
    • 显示友好的错误信息给用户
  6. 代码规范

    • 遵循统一的代码规范
    • 使用ESLint和Prettier来检查和格式化代码
    • 为接口函数添加注释,说明功能和参数

小结

本节我们学习了API接口管理,包括:

  • 接口模块化组织,按照业务模块来划分不同的接口文件
  • TypeScript接口类型定义,为API请求和响应添加类型
  • Mock数据与开发环境配置,使用Mock.js和Vite Plugin Mock来模拟后端API
  • 开发环境配置,包括环境变量和跨域配置

通过合理管理API接口,我们可以提高开发效率,减少重复代码,同时提高代码的可维护性和可扩展性。在实际项目中,我们可以根据具体需求选择合适的API管理方式,以满足项目的需要。

思考与练习

  1. 按照业务模块组织API接口文件。
  2. 为API接口添加TypeScript类型定义。
  3. 使用Mock.js模拟后端API的响应。
  4. 配置不同环境的API基础URL。
  5. 配置Vite代理解决跨域问题。
« 上一篇 29-axios-encapsulation 下一篇 » 31-unit-testing