返回笔记首页

接口规范与联调 - 深度剖析

主题配置

简历项目经验描述

版本1 - 适合初中级

plain
负责前后端接口对接,制定接口规范
- 制定统一的接口请求响应格式,规范化接口调用,减少对接问题 50%
- 使用 Axios 封装请求库,统一处理错误、超时、重试等逻辑
- 搭建 Mock 服务,实现前后端并行开发,开发效率提升 30%

版本2 - 适合高级

plain
主导前后端接口规范制定,建立高效的联调机制
- 设计 RESTful API 规范,统一接口命名、参数、响应格式,接口一致性 95%+
- 基于 MSW (Mock Service Worker) 搭建 Mock 系统,支持接口录制回放,联调效率提升 60%
- 对接 Swagger/OpenAPI,实现 TypeScript 类型自动生成,类型安全覆盖率 90%+
- 建立接口版本管理机制,支持多版本并存,保障系统平滑升级

版本3 - 适合架构方向

plain
主导接口治理体系建设,建立标准化的前后端协作流程
- 设计接口规范标准,包含命名、参数、错误码、分页等 10+ 项规范,团队遵守率 98%+
- 搭建接口 Mock 平台,支持动态 Mock、场景切换、数据持久化,Mock 覆盖率 100%
- 建立接口自动化测试体系,覆盖核心接口 200+,接口稳定性提升 40%
- 设计接口监控告警系统,实时监测接口性能和错误率,平均响应时间 < 200ms

面试标准回答话术

Q1: 前后端接口规范是怎么定的?

标准回答

"接口规范就是前后端约定的接口格式,让对接更顺畅。我们团队用 RESTful 风格。

我们的接口规范
1. URL 命名规范
plain
基本格式: /api/v1/资源名称

资源名称用复数:
✓ /api/v1/users          (用户列表)
✓ /api/v1/users/123      (单个用户)
✗ /api/v1/user           (错误,应该用复数)

使用名词,不用动词:
✓ GET /api/v1/users      (获取用户列表)
✗ GET /api/v1/getUsers   (错误,不要用动词)

层级不超过3层:
✓ /api/v1/users/123/orders
✗ /api/v1/users/123/orders/456/items  (太深)

使用连字符,不用下划线:
✓ /api/v1/user-profiles
✗ /api/v1/user_profiles
2. HTTP 方法
plain
GET     - 获取资源
POST    - 创建资源
PUT     - 完整更新资源
PATCH   - 部分更新资源
DELETE  - 删除资源

示例:
GET    /api/v1/users          - 获取用户列表
GET    /api/v1/users/123      - 获取单个用户
POST   /api/v1/users          - 创建用户
PUT    /api/v1/users/123      - 更新用户(全部字段)
PATCH  /api/v1/users/123      - 更新用户(部分字段)
DELETE /api/v1/users/123      - 删除用户
3. 请求参数规范
javascript
// 查询参数(GET)
GET /api/v1/users?page=1&pageSize=10&keyword=zhang&status=active

// 路径参数
GET /api/v1/users/123

// 请求体(POST/PUT)
POST /api/v1/users
Content-Type: application/json

{
  "username": "zhangsan",
  "email": "zhang@example.com",
  "age": 25
}

// 参数命名用驼峰
{
  "firstName": "Zhang",      // ✓
  "first_name": "Zhang"      // ✗
}
4. 响应格式规范
javascript
// 统一的响应格式
{
  "code": 0,              // 业务状态码,0 表示成功
  "message": "success",   // 提示信息
  "data": {},            // 业务数据
  "timestamp": 1704096000000  // 时间戳
}

// 成功响应
{
  "code": 0,
  "message": "success",
  "data": {
    "id": 123,
    "username": "zhangsan",
    "email": "zhang@example.com"
  }
}

// 列表响应
{
  "code": 0,
  "message": "success",
  "data": {
    "list": [
      { "id": 1, "name": "User 1" },
      { "id": 2, "name": "User 2" }
    ],
    "total": 100,
    "page": 1,
    "pageSize": 10
  }
}

// 错误响应
{
  "code": 10001,
  "message": "用户名已存在",
  "data": null
}
5. 状态码规范
plain
HTTP 状态码:
200 - 成功
201 - 创建成功
400 - 请求参数错误
401 - 未认证
403 - 无权限
404 - 资源不存在
500 - 服务器错误

业务状态码:
0     - 成功
10001 - 参数错误
10002 - 用户不存在
10003 - 用户名已存在
20001 - 未登录
20002 - 登录过期
20003 - 无权限
6. 分页规范
javascript
// 请求
GET /api/v1/users?page=1&pageSize=10&sortBy=createTime&sortOrder=desc

// 响应
{
  "code": 0,
  "data": {
    "list": [...],
    "total": 100,        // 总条数
    "page": 1,           // 当前页
    "pageSize": 10,      // 每页条数
    "totalPages": 10     // 总页数
  }
}
7. 时间格式
javascript
// 统一用时间戳(毫秒)
{
  "createTime": 1704096000000,
  "updateTime": 1704096000000
}

// 或 ISO 8601 格式
{
  "createTime": "2024-01-15T10:30:00.000Z",
  "updateTime": "2024-01-15T10:30:00.000Z"
}
8. 请求封装
javascript
// utils/request.js
import axios from 'axios'
import { message } from 'ant-design-vue'

const service = axios.create({
  baseURL: '/api/v1',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 添加 token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }

    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    const { code, message: msg, data } = response.data

    // 业务成功
    if (code === 0) {
      return data
    }

    // 业务失败
    message.error(msg || '请求失败')
    return Promise.reject(new Error(msg))
  },
  error => {
    // HTTP 错误
    if (error.response) {
      const { status, data } = error.response

      switch (status) {
        case 401:
          message.error('未登录或登录已过期')
          // 跳转到登录页
          window.location.href = '/login'
          break
        case 403:
          message.error('无权限访问')
          break
        case 404:
          message.error('请求的资源不存在')
          break
        case 500:
          message.error('服务器错误')
          break
        default:
          message.error(data?.message || '请求失败')
      }
    } else if (error.request) {
      message.error('网络错误,请检查网络连接')
    } else {
      message.error(error.message)
    }

    return Promise.reject(error)
  }
)

export default service
9. API 封装
javascript
// api/user.js
import request from '@/utils/request'

// 获取用户列表
export function getUserList(params) {
  return request({
    url: '/users',
    method: 'get',
    params
  })
}

// 获取单个用户
export function getUserDetail(id) {
  return request({
    url: `/users/${id}`,
    method: 'get'
  })
}

// 创建用户
export function createUser(data) {
  return request({
    url: '/users',
    method: 'post',
    data
  })
}

// 更新用户
export function updateUser(id, data) {
  return request({
    url: `/users/${id}`,
    method: 'put',
    data
  })
}

// 删除用户
export function deleteUser(id) {
  return request({
    url: `/users/${id}`,
    method: 'delete'
  })
}
10. 组件中使用
vue
<script setup>
import { ref, onMounted } from 'vue'
import { getUserList, createUser } from '@/api/user'

const users = ref([])
const loading = ref(false)

async function fetchUsers() {
  loading.value = true

  try {
    const data = await getUserList({
      page: 1,
      pageSize: 10
    })

    users.value = data.list
  } catch (error) {
    console.error('获取用户列表失败:', error)
  } finally {
    loading.value = false
  }
}

async function handleCreate(userData) {
  try {
    await createUser(userData)
    message.success('创建成功')
    fetchUsers()
  } catch (error) {
    // 错误已在拦截器处理
  }
}

onMounted(() => {
  fetchUsers()
})
</script>

这套规范在我们团队执行了 2 年,接口对接效率提升很多,很少出现理解偏差。"

Q2: Mock 方案是怎么做的?MSW 怎么用?

标准回答

"Mock 就是模拟后端接口,让前端不依赖后端也能开发。我们用 MSW (Mock Service Worker)。

MSW 的优势
  1. 在 Service Worker 层拦截请求,不侵入业务代码
  2. 支持浏览器和 Node.js 环境
  3. 可以模拟真实的网络延迟
  4. 调试方便,可以在 DevTools 看到请求
MSW 使用步骤
1. 安装
bash
npm install -D msw
2. 初始化
bash
# 生成 Service Worker 文件
npx msw init public/ --save

这会在 public 目录生成 mockServiceWorker.js 文件。

3. 创建 Mock 处理器
javascript
// mocks/handlers.js
import { http, HttpResponse } from 'msw'

export const handlers = [
  // 获取用户列表
  http.get('/api/v1/users', ({ request }) => {
    const url = new URL(request.url)
    const page = Number(url.searchParams.get('page')) || 1
    const pageSize = Number(url.searchParams.get('pageSize')) || 10

    // 模拟数据
    const list = Array.from({ length: pageSize }, (_, i) => ({
      id: (page - 1) * pageSize + i + 1,
      username: `user${(page - 1) * pageSize + i + 1}`,
      email: `user${(page - 1) * pageSize + i + 1}@example.com`,
      createTime: Date.now()
    }))

    return HttpResponse.json({
      code: 0,
      message: 'success',
      data: {
        list,
        total: 100,
        page,
        pageSize
      }
    })
  }),

  // 获取单个用户
  http.get('/api/v1/users/:id', ({ params }) => {
    const { id } = params

    return HttpResponse.json({
      code: 0,
      message: 'success',
      data: {
        id: Number(id),
        username: `user${id}`,
        email: `user${id}@example.com`,
        age: 25,
        createTime: Date.now()
      }
    })
  }),

  // 创建用户
  http.post('/api/v1/users', async ({ request }) => {
    const body = await request.json()

    // 验证参数
    if (!body.username) {
      return HttpResponse.json({
        code: 10001,
        message: '用户名不能为空',
        data: null
      }, { status: 400 })
    }

    // 模拟创建成功
    return HttpResponse.json({
      code: 0,
      message: 'success',
      data: {
        id: Math.floor(Math.random() * 1000),
        ...body,
        createTime: Date.now()
      }
    }, { status: 201 })
  }),

  // 更新用户
  http.put('/api/v1/users/:id', async ({ params, request }) => {
    const { id } = params
    const body = await request.json()

    return HttpResponse.json({
      code: 0,
      message: 'success',
      data: {
        id: Number(id),
        ...body,
        updateTime: Date.now()
      }
    })
  }),

  // 删除用户
  http.delete('/api/v1/users/:id', ({ params }) => {
    return HttpResponse.json({
      code: 0,
      message: 'success',
      data: null
    })
  }),

  // 模拟网络延迟
  http.get('/api/v1/slow-api', async () => {
    await new Promise(resolve => setTimeout(resolve, 3000))

    return HttpResponse.json({
      code: 0,
      data: { result: 'slow response' }
    })
  }),

  // 模拟错误
  http.get('/api/v1/error-api', () => {
    return HttpResponse.json({
      code: 10001,
      message: '模拟错误',
      data: null
    }, { status: 500 })
  })
]
4. 启动 Mock
javascript
// mocks/browser.js
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)
5. 在应用中启用
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'

// 开发环境启用 Mock
if (import.meta.env.MODE === 'development') {
  const { worker } = await import('./mocks/browser')

  worker.start({
    onUnhandledRequest: 'bypass'  // 未匹配的请求直接放行
  })
}

createApp(App).mount('#app')
6. 动态数据生成

使用 faker.js 生成更真实的数据:

bash
npm install -D @faker-js/faker
javascript
// mocks/handlers.js
import { faker } from '@faker-js/faker'

export const handlers = [
  http.get('/api/v1/users', () => {
    const list = Array.from({ length: 10 }, () => ({
      id: faker.string.uuid(),
      username: faker.internet.userName(),
      email: faker.internet.email(),
      avatar: faker.image.avatar(),
      age: faker.number.int({ min: 18, max: 60 }),
      address: faker.location.streetAddress(),
      createTime: faker.date.past().getTime()
    }))

    return HttpResponse.json({
      code: 0,
      data: { list, total: 100 }
    })
  })
]
7. 场景切换
javascript
// mocks/scenarios.js
export const scenarios = {
  // 正常场景
  success: {
    http.get('/api/v1/users', () => {
      return HttpResponse.json({
        code: 0,
        data: { list: [...] }
      })
    })
  },

  // 空数据场景
  empty: {
    http.get('/api/v1/users', () => {
      return HttpResponse.json({
        code: 0,
        data: { list: [], total: 0 }
      })
    })
  },

  // 错误场景
  error: {
    http.get('/api/v1/users', () => {
      return HttpResponse.json({
        code: 10001,
        message: '服务器错误'
      }, { status: 500 })
    })
  }
}

// 切换场景
let currentScenario = 'success'

export function setScenario(name) {
  currentScenario = name
  // 重新注册 handlers
  worker.use(...scenarios[name])
}
8. Mock 管理面板
vue
<!-- components/MockPanel.vue -->
<template>
  <div v-if="showPanel" class="mock-panel">
    <h3>Mock 控制面板</h3>

    <div class="scenarios">
      <h4>场景切换</h4>
      <button
        v-for="(_, name) in scenarios"
        :key="name"
        @click="switchScenario(name)"
        :class="{ active: currentScenario === name }"
      >
        {{ name }}
      </button>
    </div>

    <div class="delay">
      <h4>网络延迟</h4>
      <input
        v-model.number="delay"
        type="range"
        min="0"
        max="5000"
        step="100"
      />
      <span>{{ delay }}ms</span>
    </div>

    <div class="logs">
      <h4>请求日志</h4>
      <div v-for="log in logs" :key="log.id" class="log-item">
        <span class="method">{{ log.method }}</span>
        <span class="url">{{ log.url }}</span>
        <span class="status">{{ log.status }}</span>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const showPanel = ref(true)
const currentScenario = ref('success')
const delay = ref(0)
const logs = ref([])
</script>
优点
  • 前后端并行开发,不用等后端接口
  • 可以模拟各种场景(成功、失败、慢速)
  • 可以离线开发
  • 不侵入业务代码

我们项目用 MSW 后,前后端联调时间减少了 60%,开发效率提升明显。"

Q3: 如何对接 Swagger/OpenAPI?怎么自动生成类型?

标准回答

"Swagger/OpenAPI 是后端提供的接口文档,可以自动生成前端的类型定义和请求函数。

实现方案
1. 安装工具
bash
npm install -D openapi-typescript swagger-typescript-api
2. 从 Swagger 生成类型
bash
# 从 URL 生成
npx openapi-typescript https://api.example.com/swagger.json -o src/types/api.d.ts

# 从本地文件生成
npx openapi-typescript ./swagger.json -o src/types/api.d.ts

生成的类型文件:

typescript
// src/types/api.d.ts
export interface paths {
  "/api/v1/users": {
    get: {
      parameters: {
        query: {
          page?: number
          pageSize?: number
          keyword?: string
        }
      }
      responses: {
        200: {
          content: {
            "application/json": {
              code: number
              data: {
                list: User[]
                total: number
              }
            }
          }
        }
      }
    }
    post: {
      requestBody: {
        content: {
          "application/json": {
            username: string
            email: string
            age?: number
          }
        }
      }
      responses: {
        201: {
          content: {
            "application/json": {
              code: number
              data: User
            }
          }
        }
      }
    }
  }
}

export interface User {
  id: number
  username: string
  email: string
  age?: number
  createTime: number
}
3. 生成 API 请求函数
bash
npx swagger-typescript-api \
  -p https://api.example.com/swagger.json \
  -o src/api \
  -n api.js \
  --axios

生成的 API 文件:

javascript
// src/api/api.js
import axios from 'axios'

export class Api {
  http = axios.create({
    baseURL: '/api/v1'
  })

  users = {
    // 获取用户列表
    getUserList: (params) => {
      return this.http.request({
        url: '/users',
        method: 'GET',
        params
      })
    },

    // 获取单个用户
    getUserDetail: (id) => {
      return this.http.request({
        url: `/users/${id}`,
        method: 'GET'
      })
    },

    // 创建用户
    createUser: (data) => {
      return this.http.request({
        url: '/users',
        method: 'POST',
        data
      })
    }
  }
}

export const api = new Api()
4. 在组件中使用(带类型提示)
vue
<script setup>
import { ref } from 'vue'
import { api } from '@/api/api'

const users = ref([])

async function fetchUsers() {
  try {
    const response = await api.users.getUserList({
      page: 1,
      pageSize: 10,
      keyword: 'zhang'  // 类型安全,会有智能提示
    })

    users.value = response.data.data.list
  } catch (error) {
    console.error(error)
  }
}
</script>
5. 自定义生成模板

如果默认生成的代码不满足需求,可以自定义模板:

javascript
// scripts/generate-api.js
const { generateApi } = require('swagger-typescript-api')
const path = require('path')

generateApi({
  name: 'api.js',
  output: path.resolve(process.cwd(), './src/api'),
  url: 'https://api.example.com/swagger.json',

  // 自定义模板
  templates: path.resolve(process.cwd(), './api-templates'),

  // 生成选项
  httpClientType: 'axios',
  generateClient: true,
  generateRouteTypes: true,

  // 自定义类型名称
  modular: true,
  cleanOutput: true,

  // 钩子函数
  hooks: {
    onCreateComponent: (component) => {
      // 自定义组件生成逻辑
    },
    onCreateRoute: (routeData) => {
      // 自定义路由生成逻辑
    }
  }
})
6. 定时更新
json
// package.json
{
  "scripts": {
    "api:generate": "openapi-typescript https://api.example.com/swagger.json -o src/types/api.d.ts",
    "api:update": "swagger-typescript-api -p https://api.example.com/swagger.json -o src/api"
  }
}

配置 Git Hooks 自动更新:

bash
# .husky/post-merge
#!/bin/sh

# 合并代码后自动更新 API
npm run api:update
7. Mock 数据同步

从 Swagger 生成 Mock 数据:

javascript
// scripts/generate-mock.js
const fs = require('fs')
const { faker } = require('@faker-js/faker')

async function generateMock(swaggerUrl) {
  const response = await fetch(swaggerUrl)
  const swagger = await response.json()

  const handlers = []

  // 遍历所有接口
  Object.entries(swagger.paths).forEach(([path, methods]) => {
    Object.entries(methods).forEach(([method, config]) => {
      const mockData = generateMockData(config.responses['200'])

      handlers.push({
        method: method.toUpperCase(),
        path: path,
        response: mockData
      })
    })
  })

  fs.writeFileSync(
    'mocks/generated-handlers.js',
    `export const handlers = ${JSON.stringify(handlers, null, 2)}`
  )
}

function generateMockData(schema) {
  // 根据 schema 生成模拟数据
  // ...
}
优点
  • 类型安全,编译时就能发现错误
  • 接口文档即代码,保持同步
  • 减少手写类型定义的工作量
  • 支持智能提示和自动补全

我们团队对接了 20+ 个微服务的 Swagger 文档,类型安全覆盖率达到 90%+,接口调用错误减少了 70%。"

Q4: 接口版本管理怎么做?

标准回答

"接口版本管理就是支持多个版本的接口同时存在,保证系统平滑升级。

版本管理方案
方案1: URL 版本号

最常用的方式,把版本号放在 URL 里:

javascript
// v1 版本
GET /api/v1/users

// v2 版本
GET /api/v2/users

// 配置
const API_VERSIONS = {
  v1: '/api/v1',
  v2: '/api/v2'
}

// 使用
import request from '@/utils/request'

export function getUserList(params, version = 'v2') {
  return request({
    url: `${API_VERSIONS[version]}/users`,
    method: 'get',
    params
  })
}
方案2: Header 版本号

通过请求头指定版本:

javascript
// utils/request.js
const service = axios.create({
  baseURL: '/api',
  headers: {
    'API-Version': '2.0'
  }
})

// 动态切换版本
export function setApiVersion(version) {
  service.defaults.headers['API-Version'] = version
}
方案3: 多版本并存

不同的 API 实例使用不同版本:

javascript
// api/v1/user.js
import { createRequest } from '@/utils/request'

const request = createRequest('/api/v1')

export function getUserList(params) {
  return request.get('/users', { params })
}

// api/v2/user.js
import { createRequest } from '@/utils/request'

const request = createRequest('/api/v2')

export function getUserList(params) {
  return request.get('/users', { params })
}

// utils/request.js
export function createRequest(baseURL) {
  return axios.create({ baseURL })
}
方案4: 版本切换器
javascript
// composables/useApi.js
import { ref, computed } from 'vue'
import * as apiV1 from '@/api/v1'
import * as apiV2 from '@/api/v2'

const apiVersion = ref('v2')

const apiMap = {
  v1: apiV1,
  v2: apiV2
}

export function useApi() {
  const api = computed(() => apiMap[apiVersion.value])

  function setVersion(version) {
    if (apiMap[version]) {
      apiVersion.value = version
      localStorage.setItem('api-version', version)
    }
  }

  return {
    api,
    apiVersion,
    setVersion
  }
}

// 使用
const { api, setVersion } = useApi()

// 调用 v2 接口
const users = await api.user.getUserList()

// 切换到 v1
setVersion('v1')
const usersV1 = await api.user.getUserList()
方案5: 渐进式迁移

提供适配器,兼容新旧接口:

javascript
// adapters/user-api-adapter.js
export class UserApiAdapter {
  constructor(version = 'v2') {
    this.version = version
    this.api = version === 'v1' ? apiV1 : apiV2
  }

  async getUserList(params) {
    const response = await this.api.user.getUserList(params)

    // v1 返回格式:
    // { users: [...], count: 100 }

    // v2 返回格式:
    // { list: [...], total: 100 }

    if (this.version === 'v1') {
      return {
        list: response.users,
        total: response.count
      }
    }

    return response
  }
}

// 使用
const adapter = new UserApiAdapter('v1')
const data = await adapter.getUserList()
// 返回统一格式,无论底层是 v1 还是 v2
方案6: 版本检测和提示
javascript
// utils/version-detect.js
export async function detectApiVersion() {
  try {
    // 调用版本检测接口
    const response = await fetch('/api/version')
    const { version } = await response.json()

    const supportedVersions = ['v1', 'v2']

    if (!supportedVersions.includes(version)) {
      console.warn(`API version ${version} is not supported`)
      return 'v2'  // 默认版本
    }

    return version
  } catch (error) {
    console.error('Failed to detect API version:', error)
    return 'v2'
  }
}

// main.js
const apiVersion = await detectApiVersion()
app.provide('apiVersion', apiVersion)
版本废弃流程
javascript
// 1. 标记废弃
/**
 * @deprecated 此接口已废弃,请使用 v2/users
 */
export function getUserListV1(params) {
  console.warn('API v1/users is deprecated, use v2/users instead')
  return request.get('/api/v1/users', { params })
}

// 2. 设置过期时间
const API_DEPRECATION = {
  'v1/users': {
    deprecatedAt: '2024-01-01',
    removedAt: '2024-06-01',
    migration: 'https://docs.example.com/migration/users'
  }
}

// 3. 运行时检测
function checkDeprecation(endpoint) {
  const info = API_DEPRECATION[endpoint]

  if (info) {
    const now = new Date()
    const removed = new Date(info.removedAt)

    if (now >= removed) {
      throw new Error(`API ${endpoint} has been removed. See: ${info.migration}`)
    }

    console.warn(
      `API ${endpoint} is deprecated and will be removed on ${info.removedAt}. ` +
      `Migration guide: ${info.migration}`
    )
  }
}

通过这套版本管理机制,我们支持了 3 个大版本同时运行,保证了系统的平滑升级,没有出现过因接口变更导致的线上故障。"


核心难点与解决方案

难点1: 接口并发和竞态问题

问题描述: 用户快速切换页面或多次点击按钮,导致多个请求同时发出,后发送的请求先返回,造成数据错乱。

解决方案

"我用了几种方法解决并发和竞态问题:

1. 请求取消(Axios CancelToken)
javascript
// composables/useRequest.js
import { ref } from 'vue'
import axios from 'axios'

export function useRequest(requestFn) {
  const loading = ref(false)
  const data = ref(null)
  const error = ref(null)
  let cancelToken = null

  async function execute(...args) {
    // 取消上一个请求
    if (cancelToken) {
      cancelToken.cancel('Request cancelled')
    }

    // 创建新的取消令牌
    cancelToken = axios.CancelToken.source()

    loading.value = true
    error.value = null

    try {
      const result = await requestFn(...args, {
        cancelToken: cancelToken.token
      })

      data.value = result
      return result
    } catch (err) {
      if (!axios.isCancel(err)) {
        error.value = err
        throw err
      }
    } finally {
      loading.value = false
    }
  }

  function cancel() {
    if (cancelToken) {
      cancelToken.cancel('User cancelled')
    }
  }

  return {
    loading,
    data,
    error,
    execute,
    cancel
  }
}

// 使用
const { loading, data, execute } = useRequest(getUserList)

// 每次调用会自动取消上一次
await execute({ page: 1 })
await execute({ page: 2 })  // 第一个请求会被取消
2. 请求去重
javascript
// utils/request-dedup.js
const pendingRequests = new Map()

function getRequestKey(config) {
  const { method, url, params, data } = config
  return `${method}_${url}_${JSON.stringify(params)}_${JSON.stringify(data)}`
}

export function addPendingRequest(config) {
  const key = getRequestKey(config)

  // 如果已有相同请求,取消当前请求
  if (pendingRequests.has(key)) {
    config.cancelToken = new axios.CancelToken(cancel => {
      cancel('Duplicate request')
    })
  } else {
    config.cancelToken = new axios.CancelToken(cancel => {
      pendingRequests.set(key, cancel)
    })
  }
}

export function removePendingRequest(config) {
  const key = getRequestKey(config)
  pendingRequests.delete(key)
}

// 在拦截器中使用
axios.interceptors.request.use(config => {
  addPendingRequest(config)
  return config
})

axios.interceptors.response.use(
  response => {
    removePendingRequest(response.config)
    return response
  },
  error => {
    removePendingRequest(error.config)
    return Promise.reject(error)
  }
)
3. 请求队列(串行执行)
javascript
// utils/request-queue.js
class RequestQueue {
  constructor() {
    this.queue = []
    this.running = false
  }

  async add(requestFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({
        requestFn,
        resolve,
        reject
      })

      this.process()
    })
  }

  async process() {
    if (this.running || this.queue.length === 0) {
      return
    }

    this.running = true

    const { requestFn, resolve, reject } = this.queue.shift()

    try {
      const result = await requestFn()
      resolve(result)
    } catch (error) {
      reject(error)
    } finally {
      this.running = false
      this.process()  // 处理下一个
    }
  }
}

const queue = new RequestQueue()

// 使用
export function queuedRequest(requestFn) {
  return queue.add(requestFn)
}

// 示例
async function saveData(data) {
  return queuedRequest(() => api.save(data))
}

// 多次调用会串行执行
saveData(data1)
saveData(data2)
saveData(data3)
4. 请求节流(Throttle)
javascript
// composables/useThrottledRequest.js
import { ref } from 'vue'

export function useThrottledRequest(requestFn, delay = 1000) {
  const loading = ref(false)
  const data = ref(null)
  let timer = null
  let lastArgs = null

  async function execute(...args) {
    lastArgs = args

    if (timer) {
      return  // 节流中,忽略请求
    }

    loading.value = true

    try {
      const result = await requestFn(...args)
      data.value = result
    } finally {
      loading.value = false

      // 设置节流定时器
      timer = setTimeout(() => {
        timer = null

        // 如果有新的调用,执行最后一次
        if (lastArgs !== args) {
          execute(...lastArgs)
        }
      }, delay)
    }
  }

  return {
    loading,
    data,
    execute
  }
}
5. 请求序列号(忽略旧请求)
javascript
// composables/useLatestRequest.js
import { ref } from 'vue'

export function useLatestRequest(requestFn) {
  const loading = ref(false)
  const data = ref(null)
  let requestId = 0

  async function execute(...args) {
    const currentId = ++requestId
    loading.value = true

    try {
      const result = await requestFn(...args)

      // 只处理最新的请求结果
      if (currentId === requestId) {
        data.value = result
      }
    } finally {
      if (currentId === requestId) {
        loading.value = false
      }
    }
  }

  return {
    loading,
    data,
    execute
  }
}

// 使用
const { data, execute } = useLatestRequest(searchUsers)

// 快速搜索时,只有最后一次的结果会显示
execute('a')      // 请求 1
execute('ab')     // 请求 2
execute('abc')    // 请求 3,只有这个结果会显示

通过这些方法,我们解决了搜索、分页等场景的竞态问题,用户体验好了很多。"

难点2: 接口错误处理和重试机制

问题描述: 网络不稳定或服务器偶尔超时,导致请求失败,用户体验差。

解决方案

"我建立了完整的错误处理和重试机制:

1. 错误分类
javascript
// utils/error-handler.js
export class ApiError extends Error {
  constructor(message, code, response) {
    super(message)
    this.name = 'ApiError'
    this.code = code
    this.response = response
  }
}

export function handleError(error) {
  if (axios.isCancel(error)) {
    // 请求被取消
    return {
      type: 'cancel',
      message: 'Request cancelled'
    }
  }

  if (error.response) {
    // 服务器返回错误
    const { status, data } = error.response

    if (status >= 500) {
      return {
        type: 'server',
        message: '服务器错误,请稍后重试',
        status,
        data
      }
    }

    if (status === 401) {
      return {
        type: 'auth',
        message: '登录已过期,请重新登录',
        status
      }
    }

    if (status === 403) {
      return {
        type: 'permission',
        message: '无权限访问',
        status
      }
    }

    if (status >= 400) {
      return {
        type: 'client',
        message: data.message || '请求参数错误',
        status,
        data
      }
    }
  }

  if (error.request) {
    // 网络错误
    return {
      type: 'network',
      message: '网络连接失败,请检查网络',
    }
  }

  // 其他错误
  return {
    type: 'unknown',
    message: error.message
  }
}
2. 自动重试
javascript
// utils/retry.js
export async function retryRequest(
  requestFn,
  options = {}
) {
  const {
    maxRetries = 3,
    retryDelay = 1000,
    retryCondition = (error) => {
      // 默认只重试网络错误和 5xx 错误
      return !error.response || error.response.status >= 500
    }
  } = options

  let lastError

  for (let i = 0; i < maxRetries; i++) {
    try {
      return await requestFn()
    } catch (error) {
      lastError = error

      // 检查是否需要重试
      if (!retryCondition(error)) {
        throw error
      }

      // 最后一次不延迟
      if (i < maxRetries - 1) {
        // 指数退避
        const delay = retryDelay * Math.pow(2, i)
        await new Promise(resolve => setTimeout(resolve, delay))
      }
    }
  }

  throw lastError
}

// 使用
const data = await retryRequest(
  () => getUserList({ page: 1 }),
  {
    maxRetries: 3,
    retryDelay: 1000
  }
)
3. Axios 拦截器集成
javascript
// utils/request.js
import axios from 'axios'
import { retryRequest } from './retry'

const service = axios.create({
  baseURL: '/api/v1',
  timeout: 10000
})

// 响应拦截器
service.interceptors.response.use(
  response => response,
  async error => {
    const { config } = error

    // 如果配置了重试
    if (config.retry) {
      return retryRequest(
        () => axios(config),
        {
          maxRetries: config.retry.times || 3,
          retryDelay: config.retry.delay || 1000,
          retryCondition: config.retry.condition
        }
      )
    }

    return Promise.reject(error)
  }
)

// 使用
const data = await service.get('/users', {
  retry: {
    times: 3,
    delay: 1000
  }
})
4. 错误恢复
javascript
// composables/useRequestWithFallback.js
import { ref } from 'vue'

export function useRequestWithFallback(
  requestFn,
  fallbackFn
) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)

  async function execute(...args) {
    loading.value = true
    error.value = null

    try {
      // 尝试正常请求
      data.value = await requestFn(...args)
    } catch (err) {
      error.value = err

      try {
        // 失败后使用降级方案
        data.value = await fallbackFn(...args)
      } catch (fallbackErr) {
        // 降级也失败,抛出原始错误
        throw err
      }
    } finally {
      loading.value = false
    }
  }

  return {
    data,
    loading,
    error,
    execute
  }
}

// 使用
const { execute } = useRequestWithFallback(
  // 主接口
  () => api.getUserList(),

  // 降级方案:从缓存读取
  () => {
    const cached = localStorage.getItem('userList')
    return cached ? JSON.parse(cached) : []
  }
)
5. 错误上报
javascript
// utils/error-reporter.js
export function reportError(error, context) {
  // 上报到监控平台
  if (window.tracker) {
    window.tracker.reportError({
      message: error.message,
      stack: error.stack,
      context,
      timestamp: Date.now(),
      url: window.location.href,
      userAgent: navigator.userAgent
    })
  }

  // 打印到控制台(开发环境)
  if (import.meta.env.DEV) {
    console.error('[API Error]', error, context)
  }
}

// 在拦截器中使用
service.interceptors.response.use(
  response => response,
  error => {
    reportError(error, {
      url: error.config?.url,
      method: error.config?.method,
      params: error.config?.params
    })

    return Promise.reject(error)
  }
)

通过这套错误处理机制,我们的接口成功率从 95% 提升到 99%+,用户几乎感觉不到偶尔的网络波动。"

难点3: 接口性能优化和缓存策略

问题描述: 有些接口数据变化不频繁,每次都请求浪费带宽和时间,影响性能。

解决方案

"我设计了多层缓存策略:

1. 内存缓存
javascript
// utils/cache.js
class MemoryCache {
  constructor() {
    this.cache = new Map()
  }

  get(key) {
    const item = this.cache.get(key)

    if (!item) return null

    // 检查是否过期
    if (item.expireAt && Date.now() > item.expireAt) {
      this.cache.delete(key)
      return null
    }

    return item.value
  }

  set(key, value, ttl = 60000) {  // 默认 60 秒
    this.cache.set(key, {
      value,
      expireAt: ttl ? Date.now() + ttl : null
    })
  }

  delete(key) {
    this.cache.delete(key)
  }

  clear() {
    this.cache.clear()
  }
}

export const memoryCache = new MemoryCache()
2. 请求缓存拦截器
javascript
// utils/request.js
import { memoryCache } from './cache'

service.interceptors.request.use(config => {
  // 只缓存 GET 请求
  if (config.method === 'get' && config.cache !== false) {
    const cacheKey = `${config.url}_${JSON.stringify(config.params)}`
    const cached = memoryCache.get(cacheKey)

    if (cached) {
      // 返回缓存数据
      return Promise.resolve({
        data: cached,
        config,
        headers: {},
        status: 200,
        statusText: 'OK (from cache)'
      })
    }

    // 保存 cache key 用于后续存储
    config.cacheKey = cacheKey
  }

  return config
})

service.interceptors.response.use(response => {
  // 缓存响应
  if (response.config.cacheKey) {
    const ttl = response.config.cacheTTL || 60000
    memoryCache.set(response.config.cacheKey, response.data, ttl)
  }

  return response
})

// 使用
const data = await service.get('/users', {
  cache: true,      // 启用缓存
  cacheTTL: 300000  // 缓存 5 分钟
})
3. SWR (Stale-While-Revalidate) 策略
javascript
// composables/useSWR.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useSWR(key, fetcher, options = {}) {
  const {
    revalidateOnFocus = true,
    revalidateOnReconnect = true,
    refreshInterval = 0
  } = options

  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)
  let timer = null

  async function fetchData() {
    // 先返回缓存数据
    const cached = memoryCache.get(key)
    if (cached) {
      data.value = cached
    } else {
      loading.value = true
    }

    try {
      // 后台重新验证
      const fresh = await fetcher()
      data.value = fresh
      memoryCache.set(key, fresh)
      error.value = null
    } catch (err) {
      // 如果有缓存,保留缓存数据
      if (!cached) {
        error.value = err
      }
    } finally {
      loading.value = false
    }
  }

  function revalidate() {
    fetchData()
  }

  onMounted(() => {
    fetchData()

    // 窗口聚焦时重新验证
    if (revalidateOnFocus) {
      window.addEventListener('focus', revalidate)
    }

    // 网络重连时重新验证
    if (revalidateOnReconnect) {
      window.addEventListener('online', revalidate)
    }

    // 定时刷新
    if (refreshInterval > 0) {
      timer = setInterval(revalidate, refreshInterval)
    }
  })

  onUnmounted(() => {
    window.removeEventListener('focus', revalidate)
    window.removeEventListener('online', revalidate)
    if (timer) clearInterval(timer)
  })

  return {
    data,
    error,
    loading,
    revalidate
  }
}

// 使用
const { data: users, revalidate } = useSWR(
  '/users',
  () => getUserList(),
  {
    refreshInterval: 30000  // 每 30 秒刷新
  }
)
4. LocalStorage 持久化
javascript
// utils/persistent-cache.js
class PersistentCache {
  constructor(prefix = 'api_cache_') {
    this.prefix = prefix
  }

  get(key) {
    const item = localStorage.getItem(this.prefix + key)

    if (!item) return null

    try {
      const { value, expireAt } = JSON.parse(item)

      if (expireAt && Date.now() > expireAt) {
        this.delete(key)
        return null
      }

      return value
    } catch {
      return null
    }
  }

  set(key, value, ttl = 3600000) {  // 默认 1 小时
    const item = {
      value,
      expireAt: ttl ? Date.now() + ttl : null
    }

    try {
      localStorage.setItem(this.prefix + key, JSON.stringify(item))
    } catch (error) {
      // LocalStorage 满了,清理过期数据
      this.cleanup()

      try {
        localStorage.setItem(this.prefix + key, JSON.stringify(item))
      } catch {
        console.error('LocalStorage is full')
      }
    }
  }

  delete(key) {
    localStorage.removeItem(this.prefix + key)
  }

  cleanup() {
    const keys = Object.keys(localStorage)

    keys.forEach(key => {
      if (key.startsWith(this.prefix)) {
        const item = this.get(key.slice(this.prefix.length))
        if (!item) {
          localStorage.removeItem(key)
        }
      }
    })
  }
}

export const persistentCache = new PersistentCache()
5. 缓存失效策略
javascript
// utils/cache-invalidation.js
class CacheInvalidation {
  constructor() {
    this.dependencies = new Map()
  }

  // 注册依赖关系
  addDependency(resource, dependencies) {
    this.dependencies.set(resource, dependencies)
  }

  // 使资源失效
  invalidate(resource) {
    // 删除该资源的缓存
    memoryCache.delete(resource)
    persistentCache.delete(resource)

    // 递归失效依赖该资源的其他资源
    this.dependencies.forEach((deps, key) => {
      if (deps.includes(resource)) {
        this.invalidate(key)
      }
    })
  }
}

export const cacheInvalidation = new CacheInvalidation()

// 注册依赖
cacheInvalidation.addDependency('/user-detail', ['/users'])
cacheInvalidation.addDependency('/user-orders', ['/users', '/orders'])

// 更新用户后,使相关缓存失效
await updateUser(userId, data)
cacheInvalidation.invalidate(`/users/${userId}`)
6. 预加载
javascript
// composables/usePrefetch.js
export function usePrefetch(routes) {
  const prefetched = new Set()

  function prefetch(route) {
    if (prefetched.has(route)) return

    prefetched.add(route)

    // 使用 requestIdleCallback 在空闲时预加载
    requestIdleCallback(() => {
      // 预加载数据
      const fetcher = routes[route]
      if (fetcher) {
        fetcher().catch(() => {})
      }
    })
  }

  // 鼠标悬停时预加载
  function handleHover(route) {
    setTimeout(() => prefetch(route), 100)
  }

  return {
    prefetch,
    handleHover
  }
}

// 使用
const { handleHover } = usePrefetch({
  '/user': () => getUserList(),
  '/product': () => getProductList()
})
效果
  • 接口响应时间:从 500ms 降至 50ms (缓存命中)
  • 带宽节省:60%+
  • 用户体验:页面切换更流畅

通过这套缓存策略,我们的应用性能提升了 3 倍。"


完整技术实现

1. 统一的请求封装

javascript
// utils/request.js
import axios from 'axios'
import { message } from 'ant-design-vue'
import { memoryCache } from './cache'
import { retryRequest } from './retry'

const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL || '/api/v1',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    // 添加 token
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }

    // 添加请求 ID
    config.headers['X-Request-ID'] = generateRequestId()

    // 检查缓存
    if (config.method === 'get' && config.cache !== false) {
      const cacheKey = getCacheKey(config)
      const cached = memoryCache.get(cacheKey)

      if (cached) {
        console.log('[Cache Hit]', cacheKey)

        return Promise.reject({
          isCache: true,
          data: cached,
          config
        })
      }

      config.cacheKey = cacheKey
    }

    // 请求去重
    removePendingRequest(config)
    addPendingRequest(config)

    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => {
    removePendingRequest(response.config)

    const { code, message: msg, data } = response.data

    // 缓存响应
    if (response.config.cacheKey) {
      const ttl = response.config.cacheTTL || 60000
      memoryCache.set(response.config.cacheKey, response.data, ttl)
    }

    if (code === 0) {
      return data
    }

    message.error(msg || '请求失败')
    return Promise.reject(new Error(msg))
  },
  async error => {
    removePendingRequest(error.config || {})

    // 处理缓存
    if (error.isCache) {
      return error.data.data
    }

    // 自动重试
    if (error.config && error.config.retry) {
      try {
        return await retryRequest(
          () => axios(error.config),
          error.config.retry
        )
      } catch (retryError) {
        error = retryError
      }
    }

    // 错误处理
    handleError(error)

    return Promise.reject(error)
  }
)

// 辅助函数
function getCacheKey(config) {
  const { url, params } = config
  return `${url}_${JSON.stringify(params || {})}`
}

function generateRequestId() {
  return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
}

function handleError(error) {
  if (axios.isCancel(error)) {
    return
  }

  if (error.response) {
    const { status, data } = error.response

    switch (status) {
      case 401:
        message.error('登录已过期')
        window.location.href = '/login'
        break
      case 403:
        message.error('无权限访问')
        break
      case 404:
        message.error('请求的资源不存在')
        break
      case 500:
        message.error('服务器错误')
        break
      default:
        message.error(data?.message || '请求失败')
    }
  } else if (error.request) {
    message.error('网络错误,请检查网络连接')
  } else {
    message.error(error.message)
  }
}

// 请求去重
const pendingRequests = new Map()

function getRequestKey(config) {
  const { method, url, params, data } = config
  return `${method}_${url}_${JSON.stringify(params)}_${JSON.stringify(data)}`
}

function addPendingRequest(config) {
  const key = getRequestKey(config)

  if (pendingRequests.has(key)) {
    config.cancelToken = new axios.CancelToken(cancel => {
      cancel('Duplicate request')
    })
  } else {
    config.cancelToken = new axios.CancelToken(cancel => {
      pendingRequests.set(key, cancel)
    })
  }
}

function removePendingRequest(config) {
  const key = getRequestKey(config)
  if (pendingRequests.has(key)) {
    pendingRequests.get(key)('Request cancelled')
    pendingRequests.delete(key)
  }
}

export default service

2. API 模块化管理

javascript
// api/modules/user.js
import request from '@/utils/request'

export default {
  // 获取用户列表
  getList(params) {
    return request({
      url: '/users',
      method: 'get',
      params,
      cache: true,
      cacheTTL: 300000  // 5 分钟
    })
  },

  // 获取用户详情
  getDetail(id) {
    return request({
      url: `/users/${id}`,
      method: 'get',
      cache: true
    })
  },

  // 创建用户
  create(data) {
    return request({
      url: '/users',
      method: 'post',
      data
    })
  },

  // 更新用户
  update(id, data) {
    return request({
      url: `/users/${id}`,
      method: 'put',
      data
    })
  },

  // 删除用户
  delete(id) {
    return request({
      url: `/users/${id}`,
      method: 'delete',
      retry: {
        times: 3,
        delay: 1000
      }
    })
  }
}

// api/index.js
import user from './modules/user'
import product from './modules/product'
import order from './modules/order'

export default {
  user,
  product,
  order
}

3. 在组件中使用

vue
<!-- views/UserList.vue -->
<template>
  <div class="user-list">
    <a-table
      :dataSource="users"
      :columns="columns"
      :loading="loading"
      :pagination="pagination"
      @change="handleTableChange"
    >
      <template #action="{ record }">
        <a-space>
          <a-button @click="handleEdit(record)">编辑</a-button>
          <a-popconfirm
            title="确定删除?"
            @confirm="handleDelete(record.id)"
          >
            <a-button danger>删除</a-button>
          </a-popconfirm>
        </a-space>
      </template>
    </a-table>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import api from '@/api'

const users = ref([])
const loading = ref(false)
const pagination = ref({
  current: 1,
  pageSize: 10,
  total: 0
})

const columns = [
  { title: 'ID', dataIndex: 'id', key: 'id' },
  { title: '用户名', dataIndex: 'username', key: 'username' },
  { title: '邮箱', dataIndex: 'email', key: 'email' },
  { title: '操作', key: 'action', slots: { customRender: 'action' } }
]

async function fetchUsers() {
  loading.value = true

  try {
    const data = await api.user.getList({
      page: pagination.value.current,
      pageSize: pagination.value.pageSize
    })

    users.value = data.list
    pagination.value.total = data.total
  } catch (error) {
    console.error('获取用户列表失败:', error)
  } finally {
    loading.value = false
  }
}

function handleTableChange(pag) {
  pagination.value.current = pag.current
  pagination.value.pageSize = pag.pageSize
  fetchUsers()
}

async function handleDelete(id) {
  try {
    await api.user.delete(id)
    message.success('删除成功')
    fetchUsers()
  } catch (error) {
    // 错误已在拦截器处理
  }
}

onMounted(() => {
  fetchUsers()
})
</script>

项目经验总结

踩过的坑

  1. 接口地址写死 - 后来改成环境变量配置
  2. 没有请求超时 - 导致页面卡死,加了 timeout
  3. 错误提示不友好 - 统一封装错误处理
  4. 没有请求去重 - 用户快速点击发多个相同请求

性能数据

  • 接口响应时间:平均 < 200ms
  • 缓存命中率:60%+
  • 接口成功率:99%+
  • 重试成功率:85%+

可以吹的点

  • 制定统一的接口规范,团队遵守率 98%+
  • 基于 MSW 搭建 Mock 系统,前后端并行开发
  • 多层缓存策略,性能提升 3 倍
  • 自动重试和错误恢复机制,成功率 99%+