前后端交互的桥梁:Axios

前后端交互的桥梁:Axios

在 Web 开发中,我们通常采用前后端分离的模式:前端(如 Vue)通过 MVVM 模式负责页面的渲染与交互,后端(如 FastAPI)负责业务逻辑与数据处理。在这两者之间,需要一座桥梁来传递数据,这座桥梁就是 HTTP 请求。前端如何向后端发起请求?在浏览器环境下,我们通常称之为 AJAX(Asynchronous JavaScript and XML)技术。而在 Vue 生态中,实现这一功能的“事实标准”库,便是 Axios

Axios 是一个基于 Promise 的网络请求库,它既可以在浏览器中运行,也可以在 Node.js 环境中使用。相比原生 fetch,Axios 提供了更多强大的功能。最简单的使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
import axios from 'axios';

// 向后端 API 发起请求
axios.get('http://127.0.0.1:8000/items')
.then(response => {
// 请求成功,处理数据
console.log(response.data);
})
.catch(error => {
// 请求失败,处理错误
console.error(error);
});

在实际的项目中,我们的后端接口成百上千,且部署环境(开发、测试、生产)各不相同。如果像上面那样每次都硬编码 URL,代码将变得难以维护。因此,标准做法是封装一个统一的 Axios 实例。我们通常会创建一个单独的文件(例如 src/api/index.ts),在其中配置基础 URL、超时时间以及拦截器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import axios, { type AxiosInstance, type AxiosError, type InternalAxiosRequestConfig } from 'axios'
import router from '@/router' // 引入路由实例,用于跳转

// 定义 API 错误类型(稍后章节详解)
export interface ApiError {
detail: string
status: number
}

// 1. 创建 Axios 实例
const apiClient: AxiosInstance = axios.create({
// 使用环境变量动态获取 API 地址,避免硬编码
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8000/api/v1',
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json'
}
})

// 2. 请求拦截器 (Request Interceptor)
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 在发送请求前,从 localStorage 获取 Token
const token = localStorage.getItem('access_token')
if (token && config.headers) {
// 如果有 Token,自动添加到 Authorization 头部
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)

// 3. 响应拦截器 (Response Interceptor)
apiClient.interceptors.response.use(
(response) => response,
(error: AxiosError<ApiError>) => {
// 统一处理错误响应
if (error.response?.status === 401) {
// 401 说明 Token 过期或无效
localStorage.removeItem('access_token')

// 配合 Vue Router 强制跳转回登录页
// currentRoute.fullPath 可以记录当前页面,登录后方便跳回来
router.push({
path: '/login',
query: { redirect: router.currentRoute.value.fullPath }
})
}
return Promise.reject(error)
}
)

export default apiClient

请求拦截器在 apiClient 发送任何请求之前,它会先检查浏览器的 localStorage 中是否有 access_token。如果找到了,就会自动在 HTTP Header 中加上 Authorization: Bearer <token>。这样我们在具体的业务组件中(比如获取用户列表)只需要关注业务逻辑,完全不需要操心认证的问题,因为拦截器已经默默帮我们做好了。

响应拦截器在后端返回数据前,它会先过一遍。如果后端返回了 401 Unauthorized,说明用户的登录令牌过期了。此时我们需要做两件事:

  1. 清除本地无效的 Token:localStorage.removeItem(...)
  2. 跳转回登录页:这里我们直接导入了 Vue Router 的实例 router,并调用 router.push('/login')。这实现了前端的自动化闭环——用户无需刷新页面,一旦 Token 失效,系统会自动将其踢回登录页。

在上述代码中,我们直接 export default apiClient,然后在其他组件中 import apiClient 直接使用。

在后端服务层开发中,我们习惯定义一个 UserService 类,然后通过依赖注入将其实例化并注入到 Controller 或 Logic 层中。而在现代前端中,模块即单例。当 import apiClient 时,无论你在项目中引用了多少次,引用的都是同一个 apiClient 对象实例。这实际上是一种隐式的单例模式。对于无状态的 HTTP 客户端来说,这非常高效且合理,我们不需要像后端那样复杂的 DI 容器来管理生命周期。

当 Vue 应用启动,浏览器加载 main.ts 并解析依赖树时。一旦执行到 import ... from './api' 这一行,对应的 .ts 文件就会被立即执行,apiClient 实例随之创建。这通常发生在 App.vue 挂载甚至 createApp 执行之前。一旦创建,这个实例会一直驻留在浏览器的内存堆中。直到用户关闭标签页或刷新浏览器(刷新本质上是销毁当前页面应用并重新加载),这个实例才会被销毁。