axios凭什么成为前端异步请求首选?从原理到封装的完全指南
发布时间:2026/10/1 4:40:55来源:尧图网络
1. 项目概述从一次面试回答说起先讲个我实际经历过的场景。团队招前端我问候选人“项目里发请求用什么”对方很痛快地回答“axios”。“那为什么不用fetch它可是浏览器原生的。”候选人愣了一下想了半天说“因为…大家都用axios”这个回答不能说错但没说到点子上。前端异步请求这件事真正做项目的人都知道选型从来不是“哪个新用哪个”而是“哪个能帮我少踩坑、多干活”。axios能在2026年的今天依然是绝大多数前端项目的默认选择靠的不是营销而是一套设计得相当成熟的能力体系。这篇文章我想换个角度不写官方文档的翻译而是从实际开发、面试考点、二次封装这几个维度把axios从头到尾拆一遍说清楚它凭什么成为前端异步请求的首选也把那些文档里不会写、但实战里一定会碰到的细节一并交代出来。适合谁看如果你正在学前端想把网络请求这块学透这篇文章可以帮你建立起从原生XHR到axios的完整认知链条如果你工作了两三年正准备跳槽面试第4章的面试考点和排查思路直接能用如果你已经在项目里使用axios那第3章的封装思路和第2章的原理剖析应该能帮你解决一些“用着没问题但说不清为什么”的困惑。2. 核心特性解析axios到底比原生XHR和fetch好在哪里2.1 从XHR到Promise异步编程体验的质变要理解axios为什么是首选得先回头看它解决的是什么问题。在axios出现之前浏览器里发HTTP请求的标准方式就是XMLHttpRequest这玩意儿写起来有多痛苦经历过那个年代的人都懂。// 原生XHR的典型写法 const xhr new XMLHttpRequest(); xhr.open(GET, /api/user, true); xhr.onreadystatechange function() { if (xhr.readyState 4) { if (xhr.status 200) { console.log(JSON.parse(xhr.responseText)); } else { console.error(请求失败); } } }; xhr.send();这段代码有几个很明显的问题。一是回调地狱一个页面里并发三四个请求就得在onreadystatechange里嵌套判断代码越写越深逻辑越来越乱。二是状态判断太原始readyState和status是两层概念你得自己记状态码表。三是代码冗长每个请求都要重写一套模板。axios把这一切打包成Promise风格上面这段逻辑变成一行const res await axios.get(/api/user); console.log(res.data);这就是质变。Promise语法让异步流程从“回调嵌套”变成了“线性书写”配合async/await之后前端代码的阅读体验跟同步代码几乎一样。别小看这一层语法糖的进化它直接决定了团队协作时的心智负担。一个从XHR时代走过来的人看到现在的代码应该都会感慨异步请求的门槛被axios拉低了不止一个台阶。2.2 与fetch的对比原生并不一定好用有人可能会说fetch也是Promise风格浏览器原生支持何必用axios。这个观点本身没问题fetch确实是现代浏览器的原生能力而且支持流式读取、Service Worker等高级特性。但在真实业务场景里fetch有几个让人头疼的地方。其一fetch对HTTP错误状态码不拦截。这可能是最反直觉的设计。fetch只有在网络连接失败时才会reject只要服务器返回了响应哪怕是404、500fetch都会resolve只是response.ok为false。这意味着你需要手动在每一处请求后面检查状态码const res await fetch(/api/user); if (!res.ok) { // 还得自己处理错误 throw new Error(请求失败); } const data await res.json();axios的逻辑是内置的2xx范围内的状态码走resolve其余一律走reject你可以在catch里统一处理。这个差异在项目里放大以后非常明显不加封装直接用fetch每个请求都要重复写错误判断团队规范很难统一。其二fetch默认不带cookie。虽然可以通过credentials: include开启但这个默认值坑了无数新人。axios在浏览器端默认withCredentials的语义更符合日常使用直觉开发者很少因为cookie丢失排查半天。其三axios提供了真正的请求拦截。请求取消、超时控制、上传进度这些能力fetch不是没有但要么需要借助AbortController手动拼接要么根本不支持比如上传进度。axios把这些都做成了开箱即用的配置项。最后一个点也是最容易被忽略的axios同时支持浏览器和Node.js。前端项目跑测试时经常需要在Node环境里模拟HTTP请求axios的双端适配能力让开发、测试、生产可以共用同一套请求代码。fetch在Node 18以后虽然也有了global版本但很多细节行为例如对流的处理、对表单数据的支持和浏览器端并不完全一致。axios的底层适配器机制会在浏览器使用XMLHttpRequest在Node使用http模块自动帮你切换。这套设计对于全栈工程师和测试工程师来说价值是实打实的。2.3 拦截器机制请求管线的核心设计如果说axios只能选一个最值得称道的特性我选拦截器。拦截器本质上是一个洋葱模型式的管线每个请求在发出之前和每个响应在返回之后都会依次经过一组注册好的函数。// 请求拦截器在请求发出之前做统一处理 axios.interceptors.request.use(config { // 给每个请求自动带上token config.headers.Authorization Bearer ${localStorage.getItem(token)}; return config; }); // 响应拦截器在拿到响应之后做统一处理 axios.interceptors.response.use( response { // 状态码2xx直接返回data解构掉外层包裹 return response.data; }, error { // 处理401跳登录、网络错误提示等一系列统一逻辑 if (error.response?.status 401) { router.push(/login); } return Promise.reject(error); } );这段代码看起来简单但它背后解决了一个非常实际的工程问题如何让所有请求遵守同一套规则。试想一下一个项目里有几十个页面、几百个接口如果没有拦截器每个接口的调用处都得写一遍“读取token、拼接headers、判断状态码、处理错误弹窗”那代码就不是人写的了。有了拦截器规则只需要写一次全局生效。从原理上讲axios的拦截器执行顺序是请求拦截器后注册的先执行入栈顺序是倒序→ 实际发送HTTP请求 → 响应拦截器先注册的先执行。这个顺序有讲究不是随便定的。请求拦截器要遵循后进先出是借鉴了中间件模型这样可以在不改变已有拦截器的情况下往链路上追加新的前置逻辑。响应拦截器保持注册顺序则符合直觉先注册的响应处理逻辑比如统一解包应该先跑。2.4 数据转换器与Config配置优先级axios内部有两个自动执行的转换器transformRequest和transformResponse。transformRequest负责在请求发送前对data做处理常见的操作是如果data是普通对象自动序列化成JSON字符串并设置Content-Type为application/json。transformResponse负责在响应返回后把字符串解析成JSON对象。默认行为可以用这段伪代码理解// axios源码中默认的transformRequest核心逻辑 if (isObject(data)) { // 自动JSON序列化 return JSON.stringify(data); } // 默认的transformResponse核心逻辑 if (typeof data string) { try { // 尝试解析JSON return JSON.parse(data); } catch (e) { return data; // 解析失败就原样返回 } }这个“自动JSON化”的能力日常工作里感知不强因为它太顺滑了以至于大家都忘了自己在用。但如果用原生fetch你必须手动调用JSON.stringify和res.json()少一步都不行。这就是框架的意义帮你把繁琐的常规动作消化掉让你把精力放在业务上。再来看配置优先级这也是面试里喜欢抠的一个点。axios的配置有三个层级从低到高分别是全局默认配置axios.defaults.baseURL所有实例和请求的基础设置实例配置axios.create({ baseURL: /api })创建实例时传入的配置单次请求配置axios.get(/user, { timeout: 5000 })每次请求时临时覆盖的配置优先级规则很朴素后设置的覆盖先设置的。单次请求 实例配置 全局默认。这套规则的设计意图在于多层灵活性——全局配置管最通用的部分实例配置管某一类服务的差异单次请求管特殊场景。举个实际例子你的项目同时对接两个后端服务一个用/api开头一个用/open-api开头而且两个服务的鉴权方式不同。你只需要创建两个axios实例const apiClient axios.create({ baseURL: /api, timeout: 10000, }); apiClient.interceptors.request.use(config { config.headers.Authorization Bearer ${store.token}; return config; }); const openClient axios.create({ baseURL: /open-api, timeout: 30000, // 开放接口允许慢一点 });只要不混淆两个实例的配置互不干扰。这就是多实例的威力。如果你只用一个裸axios所有接口混在一起后续想拆分就要重构不少代码。2.5 取消请求与超时控制真实场景的可靠性保障前端异步请求里一个高频痛点就是请求取消。用户在搜索框里打字每打一个字符就触发一次搜索上一次请求还没返回下一次已经发出去了或者用户点击了保存按钮页面还没跳转就想退出。如果不对请求做取消控制轻则出现竞态导致数据混乱重则浪费服务器资源。axios的取消机制经历了两代演进。最早的CancelToken是基于CancelToken.source()实现的后来也支持了Web标准的AbortController。现在的建议是优先用AbortController方式因为它跟fetch是同一套底层机制没有历史包袱// 现代推荐方式AbortController const controller new AbortController(); axios.get(/api/search, { signal: controller.signal }); // 需要取消时 controller.abort();我再分享一个实际项目里的思路利用取消机制做防重复提交。定义一个Map存储请求标识到AbortController的映射请求发出前检查是否已有相同标识的请求如果有就先把上一次取消掉再发新的。这样一个简单的逻辑就能解决表单重复提交、搜索竞态一类的问题代码量不超过二十行。超时控制同样重要。timeout配置的值是毫秒超过这个时间请求会被自动终止。这里的细节在于timeout要区分场景查询接口可以给10秒上传接口需要更久上传大文件甚至可能超过一分钟。如果全局统一一个timeout值小接口超时时间设置太长会拖慢错误反馈大文件接口设置太短又容易误杀。所以合理的做法是在axios.create的实例配置里给一个适中的默认值然后在特殊请求上单独覆盖。3. 工程实践从会用axios到会封装axios3.1 为什么要做二次封装看了一圈GitHub上的前端项目几乎但凡有点规模的中后台系统都会在axios上面再做一层封装。这层封装解决的是业务代码与HTTP细节之间的耦合问题。如果没有封装业务代码里会有这些问题每一次请求都要写完整的URL路径比如axios.get(/api/user/list)一旦后端接口改名全项目搜索替换每个页面的错误处理风格不一致有的人弹alert有的人console.log有的人直接白屏接口返回结构没有解构业务代码里到处是res.data.data.list这种三层嵌套的取数逻辑header里的公共参数token、时间戳、签名每个请求都带漏带一个就出bug封装的本质是把这些横切关注点收拢到一层。我见过最简单的封装是封装一个request.js文件里面做这么几件事创建实例、配置基础URL、定义请求拦截器加token、加签名、定义响应拦截器解包数据、统一错误码。业务代码只需要导入封装后的request函数调用时只需要关心路径和参数。下面这个代码结构是我个人偏好的一个封装模板不算标准的答案但能覆盖大多数中后台项目的需求// request.js import axios from axios; const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || /api, timeout: 15000, }); // 请求拦截器 service.interceptors.request.use( (config) { // 自动携带token const token localStorage.getItem(access_token); if (token) { config.headers[Authorization] Bearer ${token}; } // 用于重复提交检测的标记后续会用到 return config; }, (error) Promise.reject(error) ); // 响应拦截器 service.interceptors.response.use( (response) { const res response.data; // 后端通常有自己的业务状态码比如code 0表示成功 if (res.code ! 0) { // 业务层面的错误展示错误信息 showToast(res.message || 请求失败); return Promise.reject(new Error(res.message)); } // 成功直接返回业务数据业务代码里就不需要再取res.data.data了 return res.data; }, (error) { let message 网络异常请稍后重试; if (error.response) { // 根据HTTP状态码映射错误提示 const statusMap { 400: 请求参数错误, 401: 登录状态已过期请重新登录, 403: 没有权限执行此操作, 404: 请求的资源不存在, 500: 服务器内部错误, }; message statusMap[error.response.status] || message; } showToast(message); return Promise.reject(error); } ); // 外部导出统一的request方法 export default service;3.2 处理Content-TypeJSON、表单与multipart的对与错Content-Type的设置是我在工作中见过翻车率最高的点值得单独拎出来讲讲。axios默认把普通对象序列化成JSON所以默认Content-Type是application/json。但很多后端接口的设计风格是表单形式比如登录接口要求application/x-www-form-urlencoded或者文件上传要用multipart/form-data。场景一application/x-www-form-urlencoded传统表单模式这个格式的特点是keyvaluekey2value2这种URL编码串。如果你在后端接口文档里看到“参数类型form”就得用这个格式。在axios里传参要借助URLSearchParamsconst params new URLSearchParams(); params.append(username, admin); params.append(password, 123456); const res await axios.post(/api/login, params); // axios检测到URLSearchParams实例会自动设置Content-Type为application/x-www-form-urlencoded这里有个细节容易踩坑如果直接传普通的JavaScript对象而Content-Type又没设置axios会把它当JSON发送后端按表单解析就会收不到参数报参数缺失。之前有同事排查了半天才发现是没转URLSearchParams。场景二multipart/form-data文件上传上传文件时浏览器要求使用这个格式。axios里推荐使用FormData对象const formData new FormData(); formData.append(file, file); // file来自input元素 formData.append(description, 这是一张图片); const res await axios.post(/api/upload, formData, { headers: { // 这里千万不要手动设置Content-Type // axios会依据FormData自动生成带boundary的Content-Type // 手动设置会导致boundary丢失后端无法解析 } });特别提醒一下手动给FormData请求设置Content-Type是上传功能最常见的bug来源之一。因为multipart格式需要生成随机的boundary分隔符这个分隔符必须由浏览器自动生成并拼接进Content-Type里。如果你手动设置成application/json或者写死multipart/form-data后端的解析器大概率会直接懵掉。正确姿势是让axios自己处理或者删掉headers里的Content-Type属性。场景三文件上传的进度显示axios原生支持onUploadProgress回调这是原生fetch不具备的能力也是我选择axios做上传功能的核心原因之一const res await axios.post(/api/upload, formData, { onUploadProgress: (progressEvent) { const percent Math.round((progressEvent.loaded / progressEvent.total) * 100); console.log(上传进度${percent}%); // 把percent更新到进度条组件里 }, });如果你做的是大文件上传这个能力配合前端Worker做分片几乎就是标配方案。浏览器主线程不能直接读文件二进制做切割File对象有slice方法可以切但计算和组装放在主线程会卡UI实际项目里一般会结合Web Worker做文件分片、计算hash、断点续传然后用axios的分片上传接口一个个发每片都带上进度回调。底层传输的可靠性axios已经替我们兜好了。3.3 大文件上传Worker分片与axios的配合既然提到了大文件上传这里展开讲一下。我做过一个视频素材上传功能单个文件动辄2GB以上直接一整块上传有两个问题一是网络不稳定时一个失败就全部重来二是浏览器对单请求超时、内存占用都有隐性限制。分片上传的思路很直接用File.slice把大文件切成固定大小的块比如每片5MB用Web Worker计算每片内容的MD5或hash值用于服务端校验完整性每个分片通过axios独立上传携带片序号和文件标识所有分片传完后后端根据分片序号合并文件下面是分片上传的关键代码骨架// 主线程中 const CHUNK_SIZE 5 * 1024 * 1024; // 5MB const file fileInput.files[0]; const chunkCount Math.ceil(file.size / CHUNK_SIZE); for (let i 0; i chunkCount; i) { const chunk file.slice(i * CHUNK_SIZE, (i 1) * CHUNK_SIZE); const formData new FormData(); formData.append(chunk, chunk); formData.append(index, i); formData.append(fileId, fileId); // 每个分片独立发送互不影响 await axios.post(/api/upload/chunk, formData, { timeout: 60000, // 大文件接口超时时间长一些 onUploadProgress: (e) { // 更新总体进度 (已完成的片数 当前片进度) / 总片数 } }); // 将已完成的分片进度写入localStorage localStorage.setItem(upload_${fileId}_${i}, done); }注意这里的断点续传逻辑每次分片上传成功后在localStorage里标记一下。再次打开页面时先检查哪些分片已经传过了只传缺失的。这个方案能应对大部分网络中断场景。再补充一个关于cancel的细节用户在上传过程中点击“取消”你怎么处理正确的做法不是简单地controller.abort()因为后端可能已经把某些分片写磁盘了。更稳妥的方案是先调用后端接口告知“取消上传”后端清理已接收的分片同时前端再abort掉所有还在传输中的请求。异步架构里的资源回收永远是“前端取消”和“后端清理”两手抓。3.4 多环境配置与请求日志封装axios时还有一个容易被忽视但生产环境必踩的坑环境区分。本地开发环境接口是http://localhost:8080测试环境是https://test-api.example.com生产环境是https://api.example.com。如果baseURL是写死的那么每次切换环境都要改代码重新打包。正确做法是使用构建工具的环境变量机制。拿Vite举例根目录下维护.env.development、.env.test、.env.production三个文件各自定义VITE_API_BASE_URL然后在封装文件里读取// .env.development VITE_API_BASE_URLhttp://localhost:8080 // .env.production VITE_API_BASE_URLhttps://api.example.com // request.js中 const service axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, });这样配置以后本地跑dev构建test发布prodbaseURL自动切换完全不用动代码。请求日志也是一个被低估的调试工具。在拦截器里加上console.log开发模式下打印请求方法和URL响应返回时打印耗时和状态码。定位问题的时候有日志和没日志的区别是十分钟和三秒钟的区别。但这个日志功能一定要用环境变量控制只开启在开发环境生产环境关掉避免日志刷屏和隐私信息泄露。4. 常见问题排查与面试高频考点4.1 前端异步请求问题速查表这里把我排查过的一些典型问题整理成表格按“现象→原因→解决方案”来组织方便大家直接查。现象常见原因解决方案请求发出去了但后端说没收到参数传了普通对象但后端接口要求表单格式用URLSearchParams包装参数让axios自动设置表单Content-Type上传文件后端解析失败手动设置了Content-Typeboundary缺失删除headers中手动设置的Content-Type交给axios自动生成接口返回401但页面没有跳转登录响应拦截器没有处理401场景在响应拦截器中对error.response.status做分支判断切换环境后接口全部404baseURL写死在了代码里改用环境变量区分VITE_API_BASE_URL页面数据出现旧请求覆盖新请求没有取消未完成的请求引入AbortController请求竞态时取消上一次同一个接口重复提交后台数据重复缺少防重复提交机制在请求拦截器里做统一标记和拦截响应数据取不到报undefined没有解构response.data.data在响应拦截器里return res.data业务侧直接拿数据跨域接口本地开发不通本地代理未配置Vite/Webpack devServer里配置proxy转发请求超时但耗时明明很短后端处理时间超出timeout阈值增大timeout或对上传场景区分处理两个服务端返回的数据结构不同单axios实例无法区分处理创建多个axios实例各自配置拦截器与数据结构约定4.2 经验教训几个印象深刻的线上问题第一个教训是关于生产环境token泄露。有一次同事直接在请求拦截器里把token拼进URL query参数被监控系统抓到了敏感信息泄漏。正确做法是放headers里这是axios推荐的鉴权头标准位置也是后端网关默认会检查的位置。在封装模板里我已经写明了用config.headers[Authorization]这是底线。第二个教训是响应拦截器里的业务状态码误判。某接口返回的业务code有0和200两种后端文档没说清楚。同事只写了if (res.code ! 0)做错误判断结果code为200时一直走错误分支页面白屏。后来我们约定业务状态码必须在接口文档里明确定义客户端只认一种成功值。如果后端不统一用配置表映射每个接口的成功状态码。第三个教训和并发请求的共享状态有关。有一个需求是用户点按钮后连续发两个请求第二个请求依赖第一个的结果。有人直接在组件里写了两层await看起来没问题。但组件被重复调用时两个请求并不会自动排队而是并发发出导致第二个请求拿到的数据是旧值。这个问题的根治方式是在请求层做依赖编排或者在业务侧使用最小并发控制。axios没有内置请求队列但我们可以在封装层用简单的方式实现维护一个按请求标识分组的Promise队列同一组的请求串行执行。这段经验告诉我们工具的边界在哪里什么时候需要自己动手补逻辑。4.3 面试容易问到的axios问题与答题思路关注前端面试的话会发现axios几乎是必问项。面试官考这个不是因为它是热点而是因为axios的覆盖面广能从微观的源码机制到宏观的工程架构层层追问特别能试探真实水平。之前跟一个阿里的前端组长聊过他说他们面axios的套路通常是三个递进问题。第一个是“axios有哪些核心特性”这题是送分分水岭能说出“拦截器、取消请求、自动JSON转换、双端适配”的算入门。第二个是“拦截器里如果某个请求不想走拦截逻辑怎么处理”这个就开始考实战细节了。得答出可以在请求配置里加自定义标记比如config.skipAuth true然后在拦截器开头判断该标记直接return config。第三个是“axios拦截器的执行顺序以及为什么请求拦截器是后进先出响应拦截器是先进先出”这题考的是对源码的理解。我这里还整理几个高频考点每个都配有答题要点为什么axios能同时兼容浏览器和Node环境核心在于axios的适配器模式。axios内部会根据运行环境选择不同的底层实现浏览器端用XMLHttpRequestNode端用http模块对外暴露统一的Promise接口。源码里通过typeof XMLHttpRequest ! undefined判断环境这就是“适配器”思想的典型应用。axios是怎么实现自动JSON序列化的因为内部默认的transformRequest遇到普通对象时调用了JSON.stringifytransformResponse对字符串做了JSON.parse的尝试。我们可以传入自定义transformRequest来覆盖默认行为比如某些特殊接口需要保留原始data结构时。如何取消一个正在进行的axios请求现代推荐使用AbortController将controller.signal传入请求配置的signal字段调用abort方法即可。老项目里的CancelToken已经不建议新代码使用。面试还要补充一点取消请求后axios会抛出一个CanceledError要在catch里判断并避免当成业务错误处理。axios的拦截器有哪些典型的业务用途请求拦截器可以统一加token、加时间戳防缓存、做签名、统计请求日志响应拦截器可以统一解包数据、统一处理错误码、自动重试比如token过期后刷新token再重新请求一次、处理登录失效跳转。这块答得越具体越加分。如果后端接口同时支持JSON和FormData两种提交方式前端如何选择按语义区分。文件上传用FormData携带二进制数据普通参数传递用JSON更加直观。用FormData时不要手动设置Content-Type交给浏览器自动生成。如果后端要求表单结构则用URLSearchParams。关键是跟后端对齐文档不要自己猜。4.4 实战排查思路一个45分钟的定位过程分享一个真实的排查过程帮助大家建立问题定位的路径依赖。有一次线上反馈用户登录后首页数据加载不出来但刷新三四次又有可能正常。第一反应肯定是看网络面板结果发现接口返回了401。那为什么刷新几次会好因为有时候进入页面时token还没写入localStorage就发了请求拦截器取不到token请求自然被拒。顺着这个思路继续排查会发现这种“偶发性加载失败”多半是时序问题——不是axios的问题也不是后端的问题而是前端代码的执行顺序没有保证。我们当时修复的方式是在全局路由守卫里面先等token初始化完成再放行页面渲染。如果你的项目也遇到类似问题可以按这个顺序查先看Network面板确认请求状态码再看拦截器打印的日志确认config是否携带了token最后检查组件生命周期里token的写入时机。大多数这类问题都能在15分钟内定位。5. 写在最后一些个人的体会与建议做前端这几年axios是我使用频率最高的库之一。说句实在话它不是没有缺点包体积不算小有些设计也带一定历史包袱。但它依然是目前最值得推荐的前端HTTP客户端理由很简单它能覆盖90%以上真实业务场景并且给你留好了封装的扩展点。如果让我给正在学习前端的朋友一个建议我会说不要停留在“会用axios”的层面而是试着从“如果我来设计axios我会怎么做”的角度去理解它。拦截器为什么长成酱紫、适配器为什么能双端通用、配置优先级为什么要分层当你开始琢磨这些问题的时候你对前端工程化的理解会上一个台阶。最后再分享一个小技巧面试或者写技术方案的时候可以把axios的封装设计当作一个可复用的“微型框架”来准备。你不需要背代码而是把你实际项目里如何封装、如何在拦截器里解决认证问题、如何实现上传进度和请求取消讲清楚这就足够展示你的工程能力了。毕竟面试官真正在意的不是你知道多少个API而是遇到问题时你知不知道从哪下手。
网站建设高端定制企业官网