在近期的一次项目审视中,我发现项目依赖的13KB的Axios库,本质上是浏览器原生fetch() API的包装器。于是,我尝试用约25行原生JavaScript代码实现了其核心功能,将包体积减少了92%。这成为了一个颇具启发性的前端性能优化案例。
过去,启动新项目时,安装Axios几乎成了一个下意识的动作。并非它不好用,而是那种“行业惯例”让人觉得这样做更专业。就像咖啡需要拉花,奶茶需要加料,仿佛缺了它就少了点什么。
然而,当我仔细研读了Axios的源码后,才恍然大悟:我一直在项目中引入一个13KB的“庞然大物”,而它做的核心工作,正是为JavaScript自带的fetch() API披上了一件功能外衣。这种感觉,如同拆开一台全自动咖啡机,发现里面核心就是一个手动磨豆机加了个电子开关。

图:Axios的核心功能是对fetch API进行分层包装
客观地说,Axios确实提供了几个非常实用的特性:自动JSON解析、更友好的错误处理、请求/响应拦截器、请求取消以及超时控制。核心功能主要就是这五项。
但关键在于,现代 JavaScript 中的 fetch() API 本身就具备实现这些功能的能力,只是需要我们更深入地了解其用法。这反映了前端开发中的一个常见现象:我们对第三方库形成了依赖,反而忽略了原生API的强大潜力。
或许你会质疑:“说起来简单,自己实现一个能用和好用的库是两码事。”
实际上,这并没有想象中困难。下面是我编写的一个基于Vanilla JS的HTTP客户端类:
class HTTP {
constructor(baseURL = '', timeout = 5000) {
this.baseURL = baseURL;
this.timeout = timeout;
this.interceptors = { request: [], response: [] };
}
async request(url, options = {}) {
// 应用请求拦截器
let config = { ...options };
for (let interceptor of this.interceptors.request) {
config = await interceptor(config);
}
// 创建超时控制器
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(this.baseURL + url, {
...config,
signal: controller.signal
});
clearTimeout(timeoutId);
// Axios风格的错误处理:非2xx状态码抛出错误
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
// 自动解析JSON响应体
const data = await response.json();
// 应用响应拦截器
let result = { data, status: response.status, headers: response.headers };
for (let interceptor of this.interceptors.response) {
result = await interceptor(result);
}
return result;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
// 便捷方法
get(url, options) {
return this.request(url, { ...options, method: 'GET' });
}
post(url, data, options) {
return this.request(url, {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(data)
});
}
put(url, data, options) {
return this.request(url, {
...options,
method: 'PUT',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(data)
});
}
delete(url, options) {
return this.request(url, { ...options, method: 'DELETE' });
}
// 拦截器支持
addRequestInterceptor(fn) {
this.interceptors.request.push(fn);
}
addResponseInterceptor(fn) {
this.interceptors.response.push(fn);
}
}
export default HTTP;
仔细数一下,去掉空行和注释,核心逻辑代码不到50行。正是这些代码,实现了Axios最常用、最核心的功能。
它的使用方式与Axios高度相似:
const http = new HTTP('https://api.example.com');
// GET请求
const { data } = await http.get('/users');
// POST请求
const { data } = await http.post('/users', {
name: '张三',
email: 'zhangsan@example.com'
});
如果需要为请求自动添加认证Token,使用请求拦截器即可:
http.addRequestInterceptor((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers = {
...config.headers,
'Authorization': `Bearer ${token}`
};
}
return config;
});
诸如401状态码自动跳转登录页、自定义超时时间等功能,实现起来也同样简单明了。
一个常见的问题是:“这个HTTP类能在React或Vue项目里用吗?”
答案是肯定的。因为它就是纯粹的 原生JavaScript,所以具有极高的通用性。无论是 React、Vue、Next.js的前端项目,还是作为 Node.js 后端服务的HTTP客户端,都可以无缝使用。

图:基于原生API的实现,具备优异的框架兼容性
我们来量化一下这样做带来的收益。

图:包体积从13KB降至约1KB,减少92%
依赖包的体积从13KB锐减至大约1KB,降幅高达92%。这在移动端弱网环境下意义重大,13KB可能意味着额外的数百毫秒加载时间。这类性能优化看似微小,但积累起来对用户体验的提升是实实在在的。此外,实现零外部依赖,功能增删由你完全掌控。更重要的是,通过亲手实现,你能真正理解HTTP请求的底层机制。
当然,我们也不能全盘否定Axios。如果你的项目需要支持Internet Explorer浏览器、团队规模庞大亟需统一的开发规范、或者需要文件上传进度跟踪等更复杂的功能,亦或项目历史包袱较重,那么继续使用Axios仍是合理的选择,因为重构需要成本。
但对于绝大多数新启动的现代前端项目而言,你可能真的不需要引入Axios这个额外的依赖。
这件事也引发了一个更深入的思考:许多流行的库本质上都是对原生API或语言特性的封装。Axios封装了fetch(),Lodash封装了数组/对象方法,Moment.js封装了Date对象(现已被Temporal API取代趋势),UUID库封装了crypto.randomUUID()……这并不是说这些库不好,它们确实解决了特定问题,提供了开发者便利。但反过来想,如果你深刻理解了底层的JavaScript,很多场景下你或许并不需要它们。每引入一个库,就增加了一份依赖、一个潜在的安全漏洞,同时也错过了一次深入学习底层原理的机会。
下次开启新项目时,不妨先别急着执行 npm install axios。尝试将上面那几十行代码集成到你的项目中,亲身体验一下。我确信,你不会怀念Axios的。
当你习惯之后,会发现一个有趣的事实:你需要的从来不是Axios,而是对 fetch() API 的透彻理解。
本文探讨了精简依赖、利用原生能力的开发思路。如果你对类似的前端工程化实践感兴趣,欢迎在云栈社区与其他开发者交流探讨。