下面以最常用的 axios 为例(即使换成 fetch,思路也完全一样),给出一个可以直接复用的封装方案。
1. 创建统一请求实例
首先创建一个 request.ts 文件,配置基础域名、超时时间、默认请求头等。
import axios from 'axios'
const request = axios.create({
baseURL: '/api', // 所有请求统一前缀,如 Tauri IPC 可改为自定义 scheme
timeout: 10000, // 10 秒超时
headers: {
'Content-Type': 'application/json'
}
})
export default request
为什么用 /api?
如果后端是 Tauri 自带的 Rust 命令,通常不在前端发 HTTP 请求;但若你的应用需要访问远程服务器,可以把 baseURL 设为实际域名,如 https://api.example.com。对于本地开发,也可通过 Vite 代理来避免跨域。
2. 请求拦截器:统一注入 Token
每一次发送请求时,自动贴上登录后的 Token,避免每个接口调用都手动传参。
request.interceptors.request.use(config => {
const token = localStorage.getItem('auth_token') // 或来自状态管理
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
}, error => {
return Promise.reject(error)
})
延伸:也可以在这里添加请求时间戳、设备 ID、防重放签名字段等。
3. 响应拦截器:统一错误处理
后端返回的格式假设是 { code: 0, data: {...}, message: '' },当 code !== 0 或 HTTP 状态码异常时,统一提示用户。
request.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 0) {
// 业务错误
console.error(`[${res.code}] ${res.message}`)
// 可以按需弹出全局提示(如 Element Plus 的 ElMessage)
return Promise.reject(new Error(res.message || '业务错误'))
}
return res.data // 只返回 data,调用者不用再解包
},
error => {
// 网络或服务器错误
let message = '网络异常,请稍后重试'
if (error.response) {
const status = error.response.status
switch (status) {
case 401:
message = '登录已过期,请重新登录'
// 可以触发跳转到登录页
break
case 403:
message = '没有权限访问'
break
case 500:
message = '服务器异常'
break
}
}
console.error(message)
return Promise.reject(error)
}
)
注意:在 Tauri 环境下,如果使用 Rust 后端通过 invoke 调用,上面的 HTTP 拦截器并不适用于 invoke(它走的是 Tauri IPC,不是 HTTP)。对于 invoke,可以另行封装一个统一的 tauriRequest 函数。
4. API 模块封装
有了上面的 request,每个业务接口就可以写成非常简洁的函数。
// api/user.ts
import request from '@/utils/request'
export const login = (params: { username: string; password: string }) => {
return request.post('/user/login', params) // 返回 Promise<Token>
}
export const getUserInfo = () => {
return request.get('/user/info') // 返回 Promise<UserInfo>
}
在组件中使用时,直接调用这些函数,无需再关心 Token 拼接或错误弹出。
<script setup lang="ts">
import { login } from '@/api/user'
const handleLogin = async () => {
try {
const token = await login({ username: 'admin', password: '123456' })
localStorage.setItem('auth_token', token)
// 跳转到主页
} catch (e) {
// 错误已在拦截器中统一处理,这里可只做必要的事
}
}
</script>
5. 配合 Tauri 的本地请求
如果你的请求对象是 Tauri 的 Rust 命令(通过 @tauri-apps/api 的 invoke),则需要单独封装一个 invokeWithErrorHandler:
import { invoke } from '@tauri-apps/api/core'
export async function tauriRequest<T>(cmd: string, args?: Record<string, unknown>): Promise<T> {
try {
const result = await invoke<T>(cmd, args)
return result
} catch (error) {
console.error(`Tauri command [${cmd}] failed:`, error)
// 这里可以根据 error 类型做统一提示
throw error
}
}
前后端通信方式虽然不同,但“拦截与封装”的思想一致:集中管理、调用清爽、维护方便。
总结
- 用
axios或fetch封装一个实例,一次性配置baseURL、超时、请求头。 - 请求拦截器:自动注入 Token、公共参数。
- 响应拦截器:剥离出
data,统一捕获业务错误和 HTTP 错误。 - 单独处理 Tauri 的
invoke,确保所有通信都有一致的错误提示。
这样一来,团队成员只需关注业务接口的调用,不用再为“token 怎么传?”“异常怎么弹?”这类细节分心,代码也更易于维护和重构。