简历项目经验描述
版本1 - 适合初中级
负责前后端接口对接,制定接口规范
- 制定统一的接口请求响应格式,规范化接口调用,减少对接问题 50%
- 使用 Axios 封装请求库,统一处理错误、超时、重试等逻辑
- 搭建 Mock 服务,实现前后端并行开发,开发效率提升 30%
版本2 - 适合高级
主导前后端接口规范制定,建立高效的联调机制
- 设计 RESTful API 规范,统一接口命名、参数、响应格式,接口一致性 95%+
- 基于 MSW (Mock Service Worker) 搭建 Mock 系统,支持接口录制回放,联调效率提升 60%
- 对接 Swagger/OpenAPI,实现 TypeScript 类型自动生成,类型安全覆盖率 90%+
- 建立接口版本管理机制,支持多版本并存,保障系统平滑升级
版本3 - 适合架构方向
主导接口治理体系建设,建立标准化的前后端协作流程
- 设计接口规范标准,包含命名、参数、错误码、分页等 10+ 项规范,团队遵守率 98%+
- 搭建接口 Mock 平台,支持动态 Mock、场景切换、数据持久化,Mock 覆盖率 100%
- 建立接口自动化测试体系,覆盖核心接口 200+,接口稳定性提升 40%
- 设计接口监控告警系统,实时监测接口性能和错误率,平均响应时间 < 200ms
面试标准回答话术
Q1: 前后端接口规范是怎么定的?
标准回答
"接口规范就是前后端约定的接口格式,让对接更顺畅。我们团队用 RESTful 风格。
我们的接口规范
1. URL 命名规范
基本格式: /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 方法
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. 请求参数规范
// 查询参数(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. 响应格式规范
// 统一的响应格式
{
"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. 状态码规范
HTTP 状态码:
200 - 成功
201 - 创建成功
400 - 请求参数错误
401 - 未认证
403 - 无权限
404 - 资源不存在
500 - 服务器错误
业务状态码:
0 - 成功
10001 - 参数错误
10002 - 用户不存在
10003 - 用户名已存在
20001 - 未登录
20002 - 登录过期
20003 - 无权限
6. 分页规范
// 请求
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. 时间格式
// 统一用时间戳(毫秒)
{
"createTime": 1704096000000,
"updateTime": 1704096000000
}
// 或 ISO 8601 格式
{
"createTime": "2024-01-15T10:30:00.000Z",
"updateTime": "2024-01-15T10:30:00.000Z"
}
8. 请求封装
// 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 封装
// 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. 组件中使用
<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 的优势
- 在 Service Worker 层拦截请求,不侵入业务代码
- 支持浏览器和 Node.js 环境
- 可以模拟真实的网络延迟
- 调试方便,可以在 DevTools 看到请求
MSW 使用步骤
1. 安装
npm install -D msw
2. 初始化
# 生成 Service Worker 文件
npx msw init public/ --save
这会在 public 目录生成 mockServiceWorker.js 文件。
3. 创建 Mock 处理器
// 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
// mocks/browser.js
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'
export const worker = setupWorker(...handlers)
5. 在应用中启用
// 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 生成更真实的数据:
npm install -D @faker-js/faker
// 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. 场景切换
// 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 管理面板
<!-- 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. 安装工具
npm install -D openapi-typescript swagger-typescript-api
2. 从 Swagger 生成类型
# 从 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
生成的类型文件:
// 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 请求函数
npx swagger-typescript-api \
-p https://api.example.com/swagger.json \
-o src/api \
-n api.js \
--axios
生成的 API 文件:
// 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. 在组件中使用(带类型提示)
<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. 自定义生成模板
如果默认生成的代码不满足需求,可以自定义模板:
// 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. 定时更新
// 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 自动更新:
# .husky/post-merge
#!/bin/sh
# 合并代码后自动更新 API
npm run api:update
7. Mock 数据同步
从 Swagger 生成 Mock 数据:
// 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 里:
// 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 版本号
通过请求头指定版本:
// 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 实例使用不同版本:
// 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: 版本切换器
// 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: 渐进式迁移
提供适配器,兼容新旧接口:
// 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: 版本检测和提示
// 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)
版本废弃流程
// 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)
// 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. 请求去重
// 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. 请求队列(串行执行)
// 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)
// 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. 请求序列号(忽略旧请求)
// 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. 错误分类
// 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. 自动重试
// 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 拦截器集成
// 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. 错误恢复
// 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. 错误上报
// 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. 内存缓存
// 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. 请求缓存拦截器
// 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) 策略
// 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 持久化
// 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. 缓存失效策略
// 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. 预加载
// 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. 统一的请求封装
// 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 模块化管理
// 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. 在组件中使用
<!-- 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>
项目经验总结
踩过的坑
- 接口地址写死 - 后来改成环境变量配置
- 没有请求超时 - 导致页面卡死,加了 timeout
- 错误提示不友好 - 统一封装错误处理
- 没有请求去重 - 用户快速点击发多个相同请求
性能数据
- 接口响应时间:平均 < 200ms
- 缓存命中率:60%+
- 接口成功率:99%+
- 重试成功率:85%+
可以吹的点
- 制定统一的接口规范,团队遵守率 98%+
- 基于 MSW 搭建 Mock 系统,前后端并行开发
- 多层缓存策略,性能提升 3 倍
- 自动重试和错误恢复机制,成功率 99%+