各位前端打工人,是不是经常在后端接口文档的汪洋大海中迷失?或者看着自己几个月前写的 fetch 和 axios 代码,忍不住想问一句:"这到底是谁写的?哦,原来是我自己。"
今天,咱们就来聊聊如何优雅地封装和使用 API。不扯高深莫测的架构,只讲让你看了就能直接复制粘贴、老板看了直呼内行的实战技巧。文章会掰开揉碎了讲,保证你不仅知其然,还知其所以然!
第一步:给 Axios 找个"大管家"
很多新手喜欢在每个组件里直接 import axios,然后满世界写 axios.get('/api/users')。这就好比你每次出门都要重新买一辆自行车,骑完就扔,不仅费钱还费事。
我们需要创建一个全局的 Axios 实例,也就是我们的"大管家"。
javascript
// utils/request.ts
import axios from 'axios';
const instance = axios.create({
baseURL: '/api', // 统一前缀,再也不用到处拼字符串了
timeout: 10000,
});
export default instance;
代码拆解与实现原理:
axios.create():这是 Axios 提供的一个工厂方法。它的作用是创建一个全新的 Axios 实例。为什么要新建而不是直接用全局的axios?因为全局配置是单例的,一旦修改会影响整个项目。而独立实例只作用于我们导出的这个对象,方便我们做定制化的配置。baseURL: '/api':这个配置非常关键!它会自动拼接在你后续请求的 URL 前面。比如你写instance.get('/users'),实际发出的请求就是/api/users。配合 Vite 或 Webpack 的 Proxy 代理,可以完美解决跨域问题。timeout: 10000:设置请求超时时间为 10 秒。如果后端接口卡死,10 秒后 Axios 会自动抛出超时错误,避免用户的页面一直转圈圈。
第二步:拦截器,你的全自动"保镖"
为什么每次请求都要手动去 localStorage 里抠 Token?为什么每次 401 都要在各个页面写一遍跳转登录页的逻辑?拦截器就是用来解决这些重复劳动的。
1. 请求拦截器:负责在出门前帮你把通行证(Token)挂好。
ini
instance.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
代码拆解与实现原理:
interceptors.request.use():这是 Axios 的请求拦截器。在每一个请求真正发出去之前,都会先经过这里。config参数:它包含了这次请求的所有配置信息(URL、请求头、请求体等)。config.headers.Authorization:我们在请求头中注入 Token。后端通常会校验这个 Header,确认你是不是合法用户。Bearer是 JWT 的标准前缀,后面跟一个空格和 Token 字符串。return config:注意,这里必须返回 config! 如果你忘了 return,或者返回了 undefined,这个请求就会直接中断,前端会报错。
2. 响应拦截器:负责在回来后帮你把包装拆了,顺便处理全局异常。
ini
instance.interceptors.response.use(
response => response.data, // 剥掉 axios 的外壳,只留核心数据
error => {
if (error.response?.status === 401) {
window.location.href = '/login';
}
return Promise.reject(error);
}
);
代码拆解与实现原理:
response => response.data:Axios 默认返回的数据结构是{ data: {...}, status: 200, headers: {...} }。但在业务代码里,我们 99% 的情况只需要data里面的内容。在这里统一剥壳,以后在组件里拿到数据就可以直接用,不用再写.data.data这种丑陋的代码。error.response?.status === 401:401 代表"未授权"(Token 过期或无效)。使用可选链?.是为了防止网络断开时error.response为 undefined 导致报错。Promise.reject(error):对于非 401 的其他错误,我们不仅要处理,还要把错误继续抛出去,让调用方的.catch()能够捕获到。
第三步:API 模块化,告别"面条代码"
有了封装好的 instance,千万别直接在页面里写 instance.post('/posts')。我们要按业务模块把 API 分门别类。
文章模块 (api/post.ts)
typescript
import axios from '@/utils/request';
export const fetchPosts = (page: number = 1, limit: number = 10) => {
return axios.get('/posts', { params: { page, limit } });
};
代码拆解与实现原理:
params: { page, limit }:这是 GET 请求传参的标准姿势。Axios 会自动将对象序列化为 URL 查询字符串,最终发出的请求就是/posts?page=1&limit=10。如果你手动拼接字符串,不仅容易出错,还容易引发 XSS 风险。
AI 模块 (api/ai.ts)
typescript
import axios from '@/utils/request';
export const askAi = (question: string) => {
return axios.post('/ai/rag', { question });
};
代码拆解与实现原理:
- 第二个参数
{ question }:这是 POST 请求的 Request Body(请求体)。Axios 会自动将其转换为 JSON 格式,并在请求头中加上Content-Type: application/json。后端只要按照 JSON 解析就能拿到数据。
第四步:在组件里"躺平"
当你把上面的工作做完后,你会发现,在 React 或 Vue 组件里写接口调用,简直是一种享受。
javascript
import { useEffect, useState } from 'react';
import { fetchPosts } from '@/api/post';
const PostList = () => {
const [posts, setPosts] = useState([]);
useEffect(() => {
// 直接调用,干净利落
fetchPosts(1, 20).then(res => {
// 因为响应拦截器已经剥壳,这里的 res 就是后端返回的真实数据
setPosts(res.data);
}).catch(err => {
console.log('获取文章失败', err);
});
}, []);
return <div>{/* 渲染你的文章列表 */}</div>;
};
代码拆解与实现原理:
- 业务代码与底层逻辑彻底解耦。如果有一天后端把
/posts改成了/articles,你只需要去api/post.ts里改一行代码,整个项目成百上千个组件完全不需要动!这就是工程化的威力。
进阶彩蛋:无感刷新 Token
如果你的项目对安全性要求很高,Token 过期时间很短,你可以利用"请求队列"机制。当遇到 401 时,先挂起后续所有请求,偷偷用 Refresh Token 换一个新 Token,然后再把挂起的请求全部放行。
这就好比你在过安检时身份证过期了,安检员(拦截器)让你稍等,他跑去后台给你办了个临时通行证,然后把你后面排队的人依次放行,全程丝滑,用户根本感觉不到 Token 刷新过。
总结
优雅的 API 调用其实就三步:
- 建实例:统一 baseURL 和超时时间。
- 加拦截:自动注入 Token,自动解包响应,统一处理报错。
- 分模块:按业务划分 API 文件,保持组件代码的清爽。
记住,代码是写给人看的,顺便给机器运行。把繁琐的底层逻辑封装起来,把优雅和简洁留给业务组件,这就是前端工程化的魅力!如果觉得有用,别忘了点赞收藏,下次写项目直接拿出来抄!