ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Axios 实战指南:从 GET/POST 请求到拦截器与错误处理

Axios 实战指南:从 GET/POST 请求到拦截器与错误处理

做前端开发这些年,被问得最多的问题之一就是“接口怎么调”。尤其是刚入行的朋友,打开编辑器面对一个空文件,知道要发请求拿数据,但具体从哪下手、参数往哪塞、请求头怎么加,脑子里是一团浆糊。axios 这个库就是在这种场景下被反复提起的——它是一个基于 Promise 的 HTTP 客户端,浏览器和 Node.js 环境都能跑,用来发 GET、POST 这类请求,处理请求头、拦截器、错误捕获这些事。说白了,它解决的是“前端怎么跟后端稳定对话”的问题。这篇内容我打算按傻瓜式入门的思路来写,从最基础的请求发起到头部参数添加,再到实际项目里踩过的坑,适合刚接触接口联调的新手,也适合想把手头请求代码理顺的中级开发者参考。下面这些内容都是我在实际项目里反复验证过的写法,你可以直接抄作业。

1. 先搞清楚 axios 到底在干什么

1.1 从一个生活场景理解请求的本质

很多人学 axios 卡住,不是因为代码难,而是因为脑子里没有“请求”这个动作的画面感。你可以把前端想象成一个点外卖的人,后端就是那家餐厅。你想吃宫保鸡丁,得告诉餐厅你要什么(请求参数),餐厅做好之后把餐送过来(响应数据)。GET 请求就像你打电话说“我要一份宫保鸡丁”,信息全挂在嘴上、写在地址栏里;POST 请求更像你填了一张订单表递过去,表上可以写很多内容,比如备注不要辣、加一份米饭,这些内容不会暴露在地址栏上。

axios 就是那个帮你打电话、填订单、收外卖的跑腿小哥。你不用自己处理电话线路(底层 XMLHttpRequest 或 fetch),只要告诉小哥“去这家店、要这个菜、带上这张备注”,剩下的事他帮你办。理解了这个类比,后面所有的参数、配置,其实都是在回答“去哪家店”“要什么”“备注写在哪”这三个问题。

我见过不少新手一上来就背 API,axios.get、axios.post记得滚瓜烂熟,但一到要加个 token 就懵了。原因就是没把请求当成一个“带信息的动作”来理解。所以这一节我特意把画面感放在前面,后面讲参数的时候你就能对号入座。

1.2 axios 相比原生写法的实际优势

原生发请求不是不能做,XMLHttpRequest写起来大概是这样:创建对象、open、setRequestHeader、onreadystatechange、send,一套下来十几行,还得自己判断状态码、自己 JSON.parse。fetch 好一些,但 fetch 有个坑——它对 4xx、5xx 状态码不会 reject,网络错误才 reject,很多人第一次用 fetch 发现接口报 404 了代码还往 then 里走,一脸问号。

axios 把这些琐碎的事包了一层。它默认把响应体做 JSON 解析,状态码非 2xx 直接进 catch,请求和响应都能挂拦截器统一处理,超时时间一个配置项搞定。举个实际对比:同样发一个带 token 的 POST,原生写法要手动拼 header、手动序列化 body、手动判状态;axios 里就是axios.post(url, data, { headers: { Authorization: token } })一行。这不是说原生不能用,而是在业务代码里,axios 能让你少写一半样板代码,出错的地方也更集中。

还有一个容易被忽略的点:axios 在浏览器里用 XHR,在 Node 里用 http 模块,同一套 API 两端通吃。做服务端渲染或者写脚本的时候,这个一致性很省心。当然它也不是银弹,包体积比 fetch 大一些,如果项目极度在意体积且请求逻辑简单,fetch 也够用。但对绝大多数业务项目来说,axios 的便利性远大于那点体积成本。

1.3 安装与最小可运行示例

安装没什么好纠结的,npm 或 yarn 都行:

npm install axios

如果你只是想在 HTML 里快速试一下,用 CDN 引入也可以,但正式项目建议走包管理,版本可控。装完之后,最小示例长这样:

import axios from 'axios' axios.get('https://api.example.com/users') .then(res => { console.log(res.data) }) .catch(err => { console.error(err) })

这里有个细节值得说:res.data才是后端返回的正文,res本身是 axios 包装过的响应对象,里面还有status、headers、config等字段。新手最常见的错误之一就是直接console.log(res)然后以为数据在顶层,结果找不到字段。记住一句话:业务数据在res.data里,这个习惯从第一天就养成,后面能少踩很多坑。

2. GET 请求:参数到底该往哪放

2.1 params 和直接拼 URL 的区别

GET 请求传参有两种写法,一种是手动拼在 URL 后面,一种是交给 axios 的params配置。手动拼是这样:

axios.get('/api/list?page=1&size=10&keyword=vue')

用 params 是这样:

axios.get('/api/list', { params: { page: 1, size: 10, keyword: 'vue' } })

两种都能跑,但实际项目里我强烈建议用params。原因有三个:第一,axios 会自动帮你做 URL 编码,关键词里有中文、空格、特殊符号的时候不会出乱码;第二,参数多的时候结构清晰,改起来不容易漏;第三,配合拦截器做统一日志时,能直接从 config 里拿到结构化参数。手动拼 URL 最容易出的问题就是忘了 encodeURIComponent,搜索词里带个&直接把参数截断了,这种 bug 排查起来很费时间。

提示:params 只对 GET 这类把参数放 query string 的请求生效,POST 的 body 数据要用第二个参数传,别搞混了。

2.2 数组和对象的序列化陷阱

GET 传数组是个经典坑。比如你传params: { ids: [1, 2, 3] },axios 默认序列化出来是ids[]=1&ids[]=2&ids[]=3,但有些后端期望的是ids=1,2,3或者ids=1&ids=2&ids=3。这时候请求发出去了,后端却收不到数据,前端看着 network 面板一脸懵。

解决办法是用paramsSerializer自定义序列化:

axios.get('/api/batch', { params: { ids: [1, 2, 3] }, paramsSerializer: { serialize: params => { return Object.keys(params) .map(key => { const val = params[key] if (Array.isArray(val)) { return val.map(v => `${key}=${encodeURIComponent(v)}`).join('&') } return `${key}=${encodeURIComponent(val)}` }) .join('&') } } })

这段代码看起来有点长,但它是可复用的,建议抽成一个工具函数。我踩过的坑是:某次批量删除功能,前端传数组后端只收到第一个 id,查了半天以为是后端 bug,最后发现是序列化格式对不上。所以联调前先跟后端确认一句“数组参数你们期望什么格式”,能省下半天时间。

2.3 GET 请求的缓存与重复请求问题

浏览器对 GET 请求有缓存机制,有时候你改了数据刷新页面,看到的还是旧内容。这不是 axios 的锅,是浏览器按 URL 缓存了。常见的规避方式是在 URL 后面加时间戳或者随机数:

axios.get('/api/detail', { params: { id: 1001, _t: Date.now() } })

另一个问题是重复请求。用户手快连点两次按钮,两个一样的 GET 请求同时飞出去,返回顺序还不一定,后发的先回就会覆盖前面的结果。这种场景可以用取消令牌或者请求去重来处理。axios 较新版本用AbortController:

const controller = new AbortController() axios.get('/api/search', { params: { q: 'vue' }, signal: controller.signal }).catch(err => { if (axios.isCancel(err)) { console.log('请求被取消') } }) // 需要取消时调用 controller.abort()

实际项目里我会把“同一接口未完成前不发新请求”封装成一个通用方法,尤其是搜索框联想、列表筛选这类高频操作,不做去重体验会很差。

3. POST 请求:body 格式与内容类型匹配

3.1 三种常见 body 格式及对应场景

POST 请求的核心是“你发给后端的数据长什么样”。常见的有三种:JSON、表单、文件。axios 默认把对象序列化成 JSON,并自动设置Content-Type: application/json,这是目前最主流的写法:

axios.post('/api/user', { name: '张三', age: 28 })

如果后端要的是表单格式(application/x-www-form-urlencoded),就不能直接传对象了,得用URLSearchParams或者 qs 库转换:

const params = new URLSearchParams() params.append('name', '张三') params.append('age', '28') axios.post('/api/user', params)

传文件则要用FormData:

const formData = new FormData() formData.append('file', fileInput.files[0]) formData.append('remark', '头像上传') axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } })

这里有个关键点:用 FormData 的时候,不要手动设置 Content-Type 为 multipart/form-data 并自己加 boundary,让浏览器或 axios 自动处理。手动设了反而可能因为 boundary 缺失导致后端解析失败。我见过有人照抄网上的代码手动写死了 Content-Type,结果文件一直传不上去,删掉那行就好了。

3.2 为什么你的 POST 后端收不到数据

“POST 发出去了,后端说没收到参数”——这是联调最高频的问题,没有之一。原因基本集中在 Content-Type 和序列化格式不匹配上。下面这张表可以帮你快速定位:

前端发送方式实际 Content-Type后端期望结果
直接传对象application/jsonapplication/json正常
直接传对象application/jsonx-www-form-urlencoded收不到
URLSearchParamsx-www-form-urlencodedapplication/json解析失败
FormDatamultipart/form-dataapplication/json收不到
手动设错 Content-Type与 body 不符任意解析异常

排查顺序建议是:先看浏览器 Network 面板里请求的 Content-Type 是什么,再看 Request Payload 里的数据长什么样,最后跟后端确认他们用什么方式解析。三步一对,问题基本就现形了。别一上来就怀疑 axios,绝大多数情况是格式没对齐。

3.3 POST 的超时与重试策略

POST 往往涉及写操作,超时设置要比 GET 谨慎。axios 默认没有超时限制,意味着请求可能一直挂着。实际项目里我会给一个全局默认值,比如 10 秒:

const instance = axios.create({ baseURL: '/api', timeout: 10000 })

但要注意,写操作的重试不能无脑做。GET 重试一般安全,POST 重试可能导致重复下单、重复扣款。如果确实需要重试,得保证接口有幂等设计,比如带一个唯一的请求 ID,后端识别到重复 ID 就返回上次结果而不是再执行一次。这个点很多教程不讲,但它是生产环境和 demo 环境的分水岭。

4. 请求头参数:从手动添加到统一管理

4.1 单个请求加 header 的正确姿势

加请求头最直接的方式是在配置对象的headers字段里写:

axios.get('/api/profile', { headers: { 'Authorization': 'Bearer ' + token, 'X-Request-From': 'web' } })

POST 同理,第三个参数就是配置对象:

axios.post('/api/order', orderData, { headers: { 'Authorization': 'Bearer ' + token } })

这里要提醒一个语法坑:axios.post(url, data, config)是三个参数,很多人写成axios.post(url, config)把配置当数据传了,结果后端收到一堆 header 字段当业务数据,哭笑不得。GET 是axios.get(url, config)两个参数,POST 是三个,这个差异记牢。

4.2 用拦截器统一注入 token

每个请求手动加 token 又累又容易漏,正确做法是用请求拦截器统一处理:

const instance = axios.create({ baseURL: '/api', timeout: 10000 }) instance.interceptors.request.use( config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, error => Promise.reject(error) )

这样业务代码里就再也不用关心 token 了。但拦截器有个细节:config.headers在较新版本里是 AxiosHeaders 对象,直接赋值属性没问题,但如果要做删除操作,用delete config.headers.Authorization更稳妥。另外,登录接口本身不需要 token,可以在拦截器里判断 URL 白名单跳过,避免登录时带着过期 token 被后端拒掉。

4.3 响应拦截器与错误统一处理

请求头管的是“发出去”,响应拦截器管的是“收回来”。实际项目里我会在响应拦截器里做三件事:统一解包 data、处理业务错误码、处理 401 跳登录:

instance.interceptors.response.use( response => { const res = response.data if (res.code !== 0) { // 业务错误,统一提示 console.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { // 登录态失效 localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } )

这样业务层拿到的直接就是数据本体,不用每次res.data.data层层剥。但要注意,拦截器里返回res.data之后,调用方拿到的就不是完整响应对象了,如果某些场景需要读响应头(比如分页总数放在 header 里),得在拦截器里特殊处理或者单独放行。这个取舍要在项目初期就定好,中途改会影响所有调用点。

5. 常见问题排查与实战避坑

5.1 跨域、404、401 的快速定位

联调时三类错误最常见,处理思路完全不同。跨域报错的特征是控制台出现 CORS 相关提示,请求可能根本没到后端,这是浏览器同源策略拦的,前端能做的有限,主要是后端加允许头或者走同源代理。404 是路径问题,先检查 baseURL 和接口路径拼接后是否多斜杠或少斜杠,/api加/user拼成/api/user没问题,但如果 baseURL 结尾带斜杠、路径开头也带斜杠,就可能变成//。401 是鉴权问题,先看请求头里 token 有没有带上、格式对不对(Bearer 后面有没有空格),再看 token 是否过期。

我习惯的排查顺序是:打开 Network 面板,看请求 URL 对不对、看请求头有没有、看响应状态码、看响应体内容。四步下来,九成问题能定位。剩下的一成,多半是环境配置或者代理规则的问题。

5.2 常见问题速查表

现象可能原因解决方向
后端收不到 POST 参数Content-Type 与 body 格式不匹配对齐序列化方式
GET 数组参数只收到一个序列化格式不符合后端预期自定义 paramsSerializer
请求头 token 不生效拦截器未生效或格式错误检查 Bearer 与空格
响应数据 undefined取错层级,数据在 res.data统一拦截器解包
请求一直 pending未设超时或后端无响应配置 timeout
重复请求覆盖结果未做去重或取消AbortController 去重
文件上传失败手动设了错误的 Content-Type交给浏览器自动处理

5.3 封装一个够用的请求模块

最后分享一个我在中小型项目里常用的封装思路,不追求大而全,够用就好:

import axios from 'axios' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE || '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) config.headers.Authorization = `Bearer ${token}` return config }) request.interceptors.response.use( res => { const body = res.data if (body.code !== 0) return Promise.reject(new Error(body.message)) return body.data }, err => { if (err.response?.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(err) } ) export default request

业务里就这样用:request.get('/user', { params: { id } })、request.post('/order', data)。封装的核心原则是“把重复的、易错的逻辑收拢到一处”,而不是为了封装而封装。我见过一些项目把 axios 包了七八层,最后调个接口要翻三个文件才知道参数怎么传,那就过度了。

我个人在实际操作中的体会是,axios 本身不难,难的是把请求格式、鉴权、错误处理这几件事在项目里形成统一约定。新手阶段先把 GET 和 POST 的两种传参方式练熟,把拦截器用起来,遇到问题先看 Network 面板而不是瞎改代码,基本就能应付绝大多数联调场景了。后面如果项目变大,再考虑请求取消、重试、并发控制这些进阶话题,一步一步来,别一口吃成胖子。

返回列表