HTTP 请求
前端与后端通信的核心手段。浏览器原生提供 Fetch API;生产项目通常使用 Axios 封装,获得拦截器、超时、取消等能力。
Fetch API
浏览器原生,基于 Promise,无需安装。
// GET
const res = await fetch('/api/users')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
// POST(JSON)
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice' })
})
const created = await res.json()携带 Cookie(跨域)
fetch('https://api.example.com/user', {
credentials: 'include' // same-origin(默认)/ include / omit
})后端需同时设置
Access-Control-Allow-Credentials: true且Allow-Origin不能为*。详见 跨域处理。
超时控制
fetch 本身无超时参数,用 AbortController 实现:
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 5000) // 5s 超时
try {
const res = await fetch('/api/data', { signal: controller.signal })
const data = await res.json()
} catch (e) {
if (e.name === 'AbortError') console.error('请求超时')
} finally {
clearTimeout(timer)
}取消请求
const controller = new AbortController()
// 发起请求
fetch('/api/search?q=foo', { signal: controller.signal })
// 任意时刻取消
controller.abort()Axios
npm install axios基本使用
import axios from 'axios'
// GET
const { data } = await axios.get('/api/users', {
params: { page: 1, size: 20 } // 自动拼接查询字符串
})
// POST(自动序列化为 JSON)
const { data: created } = await axios.post('/api/users', { name: 'Alice' })
// PUT / PATCH / DELETE
await axios.put('/api/users/1', { name: 'Bob' })
await axios.patch('/api/users/1', { name: 'Bob' })
await axios.delete('/api/users/1')创建实例(推荐)
将基础配置收拢在一个实例,避免全局污染:
// src/utils/request.js
import axios from 'axios'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: { 'Content-Type': 'application/json' }
})
export default request拦截器
请求拦截:自动注入 Token
request.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)响应拦截:统一解包与错误处理
后端通常返回统一信封格式:
{ "code": 0, "msg": "success", "data": { ... } }在拦截器中统一解包,业务代码直接拿到 data:
request.interceptors.response.use(
(response) => {
const { code, msg, data } = response.data
if (code === 0) return data // 直接返回业务数据
// 业务错误(如 code === 401 token 过期)
if (code === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
return Promise.reject(new Error(msg))
}
return Promise.reject(new Error(msg || '请求失败'))
},
(error) => {
// HTTP 层错误(4xx / 5xx / 网络断开)
const status = error.response?.status
const msgMap = {
400: '请求参数错误',
401: '未登录或登录已过期',
403: '无权限',
404: '资源不存在',
500: '服务器错误,请稍后重试',
}
const msg = msgMap[status] || error.message || '网络异常'
console.error(`[request] ${msg}`)
return Promise.reject(new Error(msg))
}
)Token 认证
JWT Bearer Token
// 登录获取 token
const { token } = await request.post('/auth/login', { username, password })
localStorage.setItem('token', token)
// 后续请求自动携带(见请求拦截器)
// Authorization: Bearer eyJhbGci...Token 刷新(无感续期)
let isRefreshing = false
let waitQueue = [] // 刷新期间挂起的请求
request.interceptors.response.use(
(res) => res,
async (error) => {
const original = error.config
if (error.response?.status !== 401 || original._retry) {
return Promise.reject(error)
}
original._retry = true
if (isRefreshing) {
// 排队等待新 token
return new Promise((resolve) => {
waitQueue.push((token) => {
original.headers.Authorization = `Bearer ${token}`
resolve(request(original))
})
})
}
isRefreshing = true
try {
const refreshToken = localStorage.getItem('refreshToken')
const { token } = await axios.post('/auth/refresh', { refreshToken })
localStorage.setItem('token', token)
waitQueue.forEach((cb) => cb(token))
waitQueue = []
original.headers.Authorization = `Bearer ${token}`
return request(original)
} catch {
localStorage.clear()
window.location.href = '/login'
return Promise.reject(error)
} finally {
isRefreshing = false
}
}
)Cookie 认证(Session)
Session 模式下凭证在 Cookie 中,无需手动处理 Token,但跨域时需额外配置:
const request = axios.create({
baseURL: '/api',
withCredentials: true // 自动携带 Cookie(跨域时生效)
})后端需要:
// Spring Boot
config.setAllowCredentials(true);
config.setAllowedOrigins(List.of("http://localhost:5173")); // 不能用 *文件上传
// 表单选择文件后上传
async function upload(file) {
const form = new FormData()
form.append('file', file)
form.append('type', 'avatar')
const data = await request.post('/api/upload', form, {
headers: { 'Content-Type': 'multipart/form-data' }, // axios 会自动设置 boundary
onUploadProgress: (e) => {
const percent = Math.round((e.loaded / e.total) * 100)
console.log(`上传进度:${percent}%`)
}
})
return data.url
}文件下载
async function download(fileId, filename) {
const res = await request.get(`/api/files/${fileId}`, {
responseType: 'blob'
})
const url = URL.createObjectURL(res)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
}取消请求
// Axios 使用 AbortController(Axios 1.x+ 推荐)
const controller = new AbortController()
request.get('/api/search', {
params: { q: keyword },
signal: controller.signal
})
// 取消(如组件卸载、搜索词变化时)
controller.abort()Vue 中组件卸载自动取消:
import { onUnmounted } from 'vue'
const controller = new AbortController()
onUnmounted(() => controller.abort())
request.get('/api/data', { signal: controller.signal })并发请求
// 同时发起多个请求,全部完成后处理
const [users, roles] = await Promise.all([
request.get('/api/users'),
request.get('/api/roles')
])
// 任一失败不影响其他(如仪表盘多卡片加载)
const results = await Promise.allSettled([
request.get('/api/stats'),
request.get('/api/recent'),
request.get('/api/alerts')
])
results.forEach((r) => {
if (r.status === 'fulfilled') console.log(r.value)
else console.error(r.reason)
})Vue Composable 封装
将请求逻辑封装为可复用的 useRequest:
// src/composables/useRequest.js
import { ref, shallowRef } from 'vue'
export function useRequest(apiFn) {
const data = shallowRef(null)
const loading = ref(false)
const error = ref(null)
async function execute(...args) {
loading.value = true
error.value = null
try {
data.value = await apiFn(...args)
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
return { data, loading, error, execute }
}在组件中使用:
<script setup>
import { onMounted } from 'vue'
import { useRequest } from '@/composables/useRequest'
import { getUserList } from '@/api/user'
const { data: users, loading, error, execute } = useRequest(getUserList)
onMounted(() => execute({ page: 1, size: 20 }))
</script>
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error">{{ error.message }}</div>
<ul v-else>
<li v-for="u in users" :key="u.id">{{ u.name }}</li>
</ul>
</template>API 模块化(推荐目录结构)
src/
├── api/
│ ├── index.js # 统一导出
│ ├── user.js # 用户相关接口
│ ├── article.js # 文章相关接口
│ └── upload.js # 上传相关
└── utils/
└── request.js # axios 实例 + 拦截器
// src/api/user.js
import request from '@/utils/request'
export const getUserList = (params) => request.get('/user/list', { params })
export const getUserById = (id) => request.get(`/user/${id}`)
export const createUser = (data) => request.post('/user', data)
export const updateUser = (id, data) => request.put(`/user/${id}`, data)
export const deleteUser = (id) => request.delete(`/user/${id}`)TypeScript 类型支持
// src/types/api.ts
export interface ApiResponse<T = unknown> {
code: number
msg: string
data: T
}
export interface PageResult<T> {
list: T[]
total: number
page: number
size: number
}
// src/types/user.ts
export interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
createdAt: string
}// src/api/user.ts
import request from '@/utils/request'
import type { User, PageResult } from '@/types'
export const getUserList = (params: { page: number; size: number }) =>
request.get<PageResult<User>>('/user/list', { params })
export const getUserById = (id: number) =>
request.get<User>(`/user/${id}`)开发环境代理(Vite)
本地开发时避免跨域,通过 Vite 代理转发到后端:
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, '') // 如后端无 /api 前缀
}
}
}
})配置环境变量区分环境:
# .env.development
VITE_API_BASE_URL= # 留空,走 Vite proxy
# .env.production
VITE_API_BASE_URL=https://api.example.com// src/utils/request.js
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api'
})代理仅在开发服务器运行期间有效;生产环境通过 Nginx 反代或正确配置 CORS 解决。
请求重试
async function requestWithRetry(fn, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
return await fn()
} catch (e) {
if (i === retries - 1) throw e
await new Promise((r) => setTimeout(r, delay * (i + 1))) // 指数退避
}
}
}
// 使用
const data = await requestWithRetry(() => request.get('/api/unstable'))