前端开发里,跟后端打交道最频繁的动作就是发请求。早些年大家用原生XMLHttpRequest写一长串回调,后来 jQuery 的$.ajax火了一阵,再往后项目工程化程度越来越高,axios就成了绝大多数人绕不开的一个 HTTP 客户端库。这个标题里带“傻瓜式入门”,我理解得很直白:不绕弯子,把 GET、POST 怎么发、请求头参数怎么加这几件事讲透,让刚上手的人照着就能跑起来。我自己带过不少刚入行的同学,发现大家卡住的地方往往不是“不会写 axios.get”,而是不知道请求头该往哪塞、POST 的 body 和 params 到底啥区别、拦截器配了为什么没生效。这篇就把这些坑一个个填平,从最基础的调用讲到头部参数的几种添加方式,再补上实际项目里踩过的经验。不管你是刚学前端、正在做第一个接口联调项目,还是已经写过几行但总觉得没吃透,都能从里面找到能直接抄的东西。
1. 先搞清楚 axios 到底是个什么东西
1.1 它解决的核心问题:把请求这件事标准化
很多人对 axios 的第一印象是“一个发请求的库”,这话没错但太浅。它真正值钱的地方在于把浏览器和 Node 环境下的 HTTP 请求抽象成了一套统一的 Promise 接口。原生fetch虽然现代浏览器都支持,但它有几个让人头疼的点:默认不带 cookie、4xx/5xx 状态码不会 reject、没有请求超时配置、JSON 解析还得自己.json()一下。axios 把这些都包好了,你拿到response.data就是解析好的对象,状态码非 2xx 会直接进 catch,超时、取消、拦截器这些能力开箱即用。
从工程角度看,axios 的另一个价值是可配置的层级结构。它支持全局默认配置、实例级配置、单次请求配置三层,优先级从低到高。这意味着你可以在项目入口统一设baseURL和超时时间,在某个业务模块的实例上单独加认证头,再在个别请求里覆盖特殊参数。这种分层设计是它比裸写fetch更适合中大型项目的原因。
提示:axios 在浏览器端底层用的是
XMLHttpRequest,在 Node 端用的是http/https模块。所以它不存在“浏览器专属”的问题,同一套代码在服务端渲染场景里也能跑,这是它相比很多浏览器专用请求库的优势。
1.2 GET 和 POST 的本质区别,别只记“一个查一个改”
新手最容易混淆的就是 GET 和 POST 的参数放哪。我用一句话概括:GET 的参数拼在 URL 上,POST 的参数放在请求体里。但这句话背后有更实际的含义。GET 请求的参数会出现在地址栏、浏览器历史、服务器访问日志里,所以它天然不适合传敏感信息,也有长度限制(虽然现代浏览器放宽了很多,但服务端和代理层仍可能截断)。POST 把数据放在 body 里,不受 URL 长度约束,适合传大段 JSON、文件、表单。
在 axios 里,这个区别体现得很具体:GET 用params配置项,axios 会自动帮你做 URL 编码并拼接到问号后面;POST 用data配置项,axios 会根据Content-Type决定怎么序列化。搞混这两个配置项是新手最常见的错误之一,后面我会专门用一节讲清楚。
1.3 为什么入门阶段建议先用 CDN 引入
刚学的时候不建议一上来就搞 npm 安装加打包工具,那会把注意力从“请求怎么写”转移到“环境怎么配”。直接用<script>标签引入 CDN 上的 axios,打开一个 HTML 文件就能在控制台里试,反馈最快。等你把 GET、POST、请求头这些玩熟了,再迁到工程化项目里,那时候配baseURL、拦截器才有意义。这个顺序很重要,先会用再会配,别本末倒置。
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>引入之后全局会挂一个axios对象,直接在<script>里或者浏览器控制台调用即可。下面所有的例子都基于这个前提,你复制过去就能跑。
2. GET 请求:从最简单的调用到参数拼接
2.1 最简形式与返回值结构
一个最朴素的 GET 请求长这样:
axios.get('https://api.example.com/users') .then(response => { console.log(response.data); }) .catch(error => { console.error(error); });这里要重点理解返回的response对象结构,它不是一个裸数据,而是包了一层。常用的几个字段:data是服务端返回的响应体(axios 自动做了 JSON 解析)、status是 HTTP 状态码、headers是响应头、config是这次请求的完整配置。新手经常直接console.log(response)然后疑惑“我要的数据怎么埋在里面”,答案就是取response.data。
用async/await写法会更清爽,这也是现在项目里的主流写法:
async function fetchUsers() { try { const response = await axios.get('https://api.example.com/users'); return response.data; } catch (error) { console.error('请求失败:', error.message); } }2.2 用 params 传查询参数,别手动拼字符串
带查询参数的 GET 请求,正确做法是用params配置项:
axios.get('https://api.example.com/users', { params: { page: 1, pageSize: 20, keyword: '张三' } });axios 会自动把它拼成?page=1&pageSize=20&keyword=%E5%BC%A0%E4%B8%89,中文和特殊字符的 URL 编码它帮你处理了。如果你手动拼字符串,很容易漏掉encodeURIComponent,遇到中文、空格、&符号就出问题。我见过有同学写url + '?name=' + name,结果名字里带个&直接把参数截断了,排查半天。
params还有个好处是值为null或undefined的键会被自动忽略,不会拼出?keyword=undefined这种脏 URL。这个细节在表单筛选场景里特别实用,用户没填的字段直接传undefined就行。
2.3 路径参数和查询参数别搞混
RESTful 风格的接口经常有路径参数,比如/users/123里的123。这种不是查询参数,得用模板字符串拼进 URL:
const userId = 123; axios.get(`https://api.example.com/users/${userId}`);判断标准很简单:看接口文档里参数是写在路径段里还是问号后面。路径段用模板字符串,问号后面用params。两者可以同时存在:
axios.get(`https://api.example.com/users/${userId}/orders`, { params: { status: 'paid', page: 1 } });3. POST 请求:body 怎么传、格式怎么对
3.1 data 配置项与默认序列化行为
POST 请求的数据放在data里:
axios.post('https://api.example.com/users', { name: '李四', age: 25 });这里有个关键点必须讲清楚:axios 默认会把普通对象序列化成 JSON,并自动设置Content-Type: application/json。也就是说上面这段代码发出去的请求头里已经带好了 JSON 类型,body 是{"name":"李四","age":25}。很多人不知道这个默认行为,又手动加了一遍Content-Type,虽然不报错但属于多余操作。
如果你的后端要的是表单格式(application/x-www-form-urlencoded),那就不能直接传对象,得用URLSearchParams或者qs库转换:
const params = new URLSearchParams(); params.append('name', '李四'); params.append('age', 25); axios.post('https://api.example.com/users', params);用URLSearchParams时 axios 会自动把Content-Type设成application/x-www-form-urlencoded,body 变成name=%E6%9D%8E%E5%9B%9B&age=25。这个转换过程新手经常漏掉,结果后端收到一个 JSON 字符串却按表单解析,拿不到字段。
3.2 POST 也能带查询参数
这点很多人不知道:POST 请求同样可以用params配置项往 URL 上挂查询参数,和 body 里的data互不干扰。
axios.post('https://api.example.com/users', { name: '李四' }, // 请求体 { params: { source: 'web' } } // URL 查询参数 );注意axios.post的第二个参数是 body,第三个参数才是配置对象。这个签名顺序是新手高频出错点,把params写到第二个参数位置,结果整个对象被当成 body 发出去了。记住口诀:get 的第二个参数是配置,post 的第二个参数是数据、第三个才是配置。
3.3 三种传参方式对照表
为了把这块彻底理清,我整理了一张对照表,实际开发中照着查就行:
| 场景 | 配置项 | 出现位置 | 示例 |
|---|---|---|---|
| GET 查询参数 | params | URL 问号后 | ?page=1 |
| POST 查询参数 | params(第三个参数) | URL 问号后 | ?source=web |
| POST 请求体 | data(第二个参数) | 请求 body | {"name":"李四"} |
| 路径参数 | 模板字符串 | URL 路径段 | /users/123 |
4. 请求头参数:几种添加方式与优先级
4.1 单次请求里加 headers
最直接的方式是在配置对象里写headers:
axios.get('https://api.example.com/profile', { headers: { 'Authorization': 'Bearer your-token-here', 'X-Request-Id': 'abc-123' } });POST 请求同理,放在第三个参数里:
axios.post('https://api.example.com/users', { name: '李四' }, { headers: { 'Authorization': 'Bearer your-token-here' } } );这种方式适合个别请求需要特殊头的场景,比如某个接口要额外的签名头。缺点是每个请求都写一遍很啰嗦,token 变了还得全局改。
4.2 全局默认头与实例级头
项目里最常见的做法是设全局默认头:
axios.defaults.headers.common['Authorization'] = 'Bearer your-token-here'; axios.defaults.headers.post['Content-Type'] = 'application/json';headers.common对所有请求生效,headers.get、headers.post分别只对对应方法生效。登录成功后把 token 塞进common,退出登录时删掉,这是很经典的做法。
但全局配置有个隐患:一个页面里如果有多个不同后端服务,全局头会污染所有请求。这时候用实例更合适:
const userApi = axios.create({ baseURL: 'https://api.example.com/user', timeout: 5000, headers: { 'Authorization': 'Bearer user-token' } }); const adminApi = axios.create({ baseURL: 'https://api.example.com/admin', timeout: 10000, headers: { 'Authorization': 'Bearer admin-token' } });两个实例各自带头,互不干扰。这是我个人最推荐的组织方式,尤其是中大型项目,按业务域拆实例比全局配置清晰得多。
4.3 拦截器里统一加头,动态 token 的正确姿势
token 存在本地、会过期、会刷新,写死在配置里不现实。标准做法是用请求拦截器:
axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = `Bearer ${token}`; } return config; }, error => { return Promise.reject(error); });每次请求发出前都会读一次最新的 token,登录、刷新 token 后无需改任何调用代码。这里有个细节要注意:拦截器里改的是config.headers,不是重新赋值整个 headers 对象,直接config.headers = {...}在某些版本里会丢掉 axios 内部的默认头。
4.4 头部优先级:谁覆盖谁
三层配置的优先级从高到低是:单次请求 headers > 实例 headers > 全局 defaults。也就是说单次请求里写的同名头会覆盖实例和全局的。理解这个优先级,排查“为什么我设的头没生效”时就有方向了——先看是不是被更高优先级的配置覆盖了。
注意:
Content-Type这个头比较特殊,axios 会根据data的类型自动推断。如果你手动设了Content-Type: application/json但传的是URLSearchParams,就会出现头和 body 不匹配的情况,后端解析失败。这种问题不会报错,只会表现为“后端收不到参数”,排查起来很费时间。
5. 完整实操:一个可运行的请求封装示例
5.1 从零搭一个带认证的请求模块
把前面讲的东西串起来,写一个实际项目里能用的封装。假设我们要做一个带 token 认证、统一错误处理、超时控制的请求模块:
const request = axios.create({ baseURL: 'https://api.example.com', timeout: 8000, headers: { 'Content-Type': 'application/json' } }); request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers['Authorization'] = `Bearer ${token}`; } return config; }, error => Promise.reject(error)); request.interceptors.response.use( response => response.data, error => { if (error.response) { const status = error.response.status; if (status === 401) { console.warn('登录已过期,请重新登录'); } else if (status >= 500) { console.error('服务端异常'); } } else if (error.code === 'ECONNABORTED') { console.error('请求超时'); } return Promise.reject(error); } ); export default request;这个封装做了几件事:统一baseURL省得每次写全路径、8 秒超时防止请求挂死、请求拦截器自动带 token、响应拦截器直接把response.data剥出来(调用方不用再.data)、统一处理 401 和 5xx。这套结构我在多个项目里用过,改动成本低,扩展也方便。
5.2 调用侧怎么写才干净
封装好之后,业务代码就非常薄了:
async function loadUserList(page) { const data = await request.get('/users', { params: { page } }); return data.list; } async function createUser(payload) { return await request.post('/users', payload); }因为响应拦截器已经剥了data,这里拿到的直接就是业务数据。注意createUser里没有传 headers,token 由拦截器统一加,这就是分层配置带来的好处——业务代码只关心业务参数。
5.3 参数计算的一个实际例子:分页偏移量
有时候后端要的是偏移量而不是页码,需要前端算一下。假设每页 20 条,第page页(从 1 开始)的偏移量是(page - 1) * 20:
const pageSize = 20; const page = 3; const offset = (page - 1) * pageSize; // 40 request.get('/users', { params: { offset, limit: pageSize } });这种计算看着简单,但边界要留意:page从 0 开始还是从 1 开始,前后端约定必须一致,否则第一页会漏数据或多数据。我踩过一次,后端页码从 0 开始,前端从 1 开始传,结果第一页永远是空的,查了半天才发现是约定问题。
6. 常见问题与排查速查
6.1 高频报错与对应原因
实际开发中遇到的 axios 问题,八成集中在下面这几类,我整理成速查表:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 后端收不到 POST 参数 | Content-Type 与 body 格式不匹配 | 检查是否传了对象却声明表单类型 |
| 请求头里没有 Authorization | 拦截器未生效或 token 为空 | 打印 config.headers 确认 |
| 跨域报错 CORS | 服务端未放行来源 | 这是服务端配置问题,前端改不了 |
| 请求超时 | timeout 设太短或网络慢 | 调大 timeout 或检查接口耗时 |
| 拿到的是字符串不是对象 | 响应不是 JSON 或未设响应类型 | 检查responseType和响应头 |
| params 里的中文乱码 | 手动拼 URL 未编码 | 改用 params 配置项 |
6.2 几个容易忽略的细节
跨域问题不是 axios 的锅。很多人第一次遇到 CORS 报错就怀疑 axios 配置,其实这是浏览器的同源策略限制,解决只能靠服务端加响应头,或者开发阶段用构建工具的代理转发。axios 层面无论怎么配都绕不过去,这点要认清,别在错误的方向上折腾。
params和data同时存在时别慌。有些接口既要在 URL 上带筛选条件,又要在 body 里传数据,两个配置项一起用完全没问题,它们走的是不同通道。
取消重复请求。搜索框输入时如果每次按键都发请求,很容易出现后发先至的乱序问题。可以用AbortController取消上一次请求:
let controller = null; async function search(keyword) { if (controller) controller.abort(); controller = new AbortController(); try { const data = await request.get('/search', { params: { keyword }, signal: controller.signal }); return data; } catch (e) { if (axios.isCancel(e)) return; // 主动取消的不算错误 throw e; } }这段代码在搜索联想场景里非常实用,能避免旧请求覆盖新结果。
6.3 我踩过的几个坑
第一个坑是在拦截器里返回了response却在业务里又取.data。响应拦截器如果写了response => response.data,业务侧就直接拿数据;如果写的是response => response,业务侧才需要.data。两种风格都行,但一个项目里必须统一,混用必然出 bug。
第二个坑是全局设置了Content-Type导致文件上传失败。上传文件要用multipart/form-data,如果全局默认头写死了application/json,文件请求也会带上错误的类型。正确做法是上传时单独覆盖,或者干脆不设全局Content-Type,让 axios 按数据类型自动推断。
第三个坑是token 过期后没有统一跳转。401 处理如果散落在各个业务代码里,维护起来很痛苦。放在响应拦截器里统一处理,配合一个标志位防止多个请求同时触发多次跳转,这个细节在实际项目里很关键。
7. 从入门到顺手:几条实用建议
学 axios 这件事,我的体会是先跑通再优化。一开始别急着上拦截器、封装、取消请求这些进阶内容,先用最简单的axios.get和axios.post把接口调通,看到数据回来,建立信心。等基础调用熟了,再逐步引入实例、拦截器、统一错误处理,每一步都对应一个真实痛点,这样学起来不空洞。
关于请求头的添加,我给个选择建议:单个请求的特殊头写在配置里,业务模块共用的头用实例,全局认证头用拦截器动态注入。这三层各司其职,别把所有头都堆在全局,也别每个请求都手写一遍。判断标准就是“这个头的作用范围有多大”,范围决定层级。
最后分享一个调试小技巧:排查请求问题时,打开浏览器开发者工具的 Network 面板,点开具体请求看 Request Headers 和 Payload 两个区域,头对不对、body 格式对不对一目了然。比在代码里到处console.log高效得多。很多时候问题不在 axios 配置,而在于你以为发出去的东西和实际发出去的东西不一致,Network 面板就是照妖镜。