做前端开发,不管你是写后台管理系统还是做移动端 H5、小程序,第一个绕不开的话题就是AJAX。我刚带新人的时候,最怕听到的一句话就是"接口我调通了"——问一下他用了什么方法、传的什么格式、报文长什么样、状态码怎么排查,基本一脸懵。这篇东西不是官方文档的复读,而是我按自己带人时讲的那套顺序整理出来的:先讲清楚 AJAX 和 axios 到底在干嘛,再把 URL、请求方法、HTTP 报文、接口文档这些零散概念串起来,最后用一个人人都会遇到的"用户登录"案例,把前面所有知识点全部过一遍。适合刚学完 HTML/CSS/JavaScript 基础、准备接触前后端交互的读者,也适合那些会把 axios 拿来就用、但出了问题不知道怎么排查的同学。
1. AJAX 到底在解决什么问题
1.1 没有 AJAX 的日子是怎么过的
我常跟新人讲:不要一上来就背"Asynchronous JavaScript And XML"这个全称,没用。你只要记住一句话——AJAX 是浏览器里用 JavaScript 发网络请求、拿到数据后不刷新整个页面就能更新网页内容的一种方式。
在没有 AJAX 的年代,用户想看到新的数据,只能整页刷新:填完表单点一下提交,浏览器白屏转圈,然后服务器返回一个全新的 HTML 页面出来。这种方式慢、体验差,网上购物车点一下加购就整页跳一下,谁也受不了。AJAX 出现之后,浏览器可以在后台悄悄地发一个请求,拿到数据之后用 JS 操作 DOM,局部更新那一小块区域。
我习惯用一个生活化的类比:以前你点餐,每加一个菜,服务员都要把整桌菜重新上一遍;AJAX 是你跟服务员说加个菜,后厨做好直接把新菜端上来,其他菜纹丝不动。本质就是:不用重新加载整个页面,就能从服务器拿数据、给服务器送数据。
1.2 同步与异步:等不等,是核心差异
很多新手卡在"异步"这两个字上。同步请求是:浏览器发出请求之后,代码停下来等,服务器返回结果之前,页面卡死,什么都不能操作。这就像你去银行柜台办业务,窗口没叫到你,你只能站在原地干等,后面的人也排着不能动。
而 AJAX 里的"异步"是:浏览器发出请求之后,代码继续往下执行,爱干嘛干嘛;等服务器把响应数据传回来了,浏览器再去执行你预先写好的回调函数。还是银行的类比:你取完号坐在椅子上刷手机,大屏叫到你的号,你才起身去窗口。
我见过太多初学者拿着一本"AJAX 入门"的书,上面写XMLHttpRequest怎么用、readyState怎么判断,看起来像天书。所以我强烈建议:入门阶段不要用原生 XHR,直接用 axios 把流程跑通,把精力放在"请求—响应—处理数据"这条主线思维上。原生 XHR 等你理解了整个链路之后,再回头补一点都不迟,痛点是啥也能体会得更清楚。
1.3 为什么是 axios,而不是 fetch 或 jQuery
这个问题差不多每次讲课都会被问到。axios 选择它有三个很实际的原因:
第一,API 简洁直观。发一个 GET 请求就是axios.get(url),发 POST 就是axios.post(url, data),配合async/await之后,代码写起来跟同步逻辑一样顺。对比原生 XHR 那一堆事件监听、状态判断、responseText从字符串手动转 JSON,axios 省掉的不是一点点。
第二,自动处理 JSON。axios 发现你传的对象会帮你转成 JSON 字符串,收到响应后如果你的响应体是 JSON 字符串,它也会帮你转成对象。新手最容易在这里踩坑,用原生 XHR 忘记JSON.parse,拿到手一直是字符串,不知道哪里出了问题。
第三,interceptor 拦截器、取消请求、错误处理这些进阶能力都有,项目用到后面走不掉。封装 axios 实例、在请求头统一加 token,这些操作在大型项目里几乎是标配。
有人问 fetch 不也能用吗?fetch 是浏览器原生 API,也能发请求,但它的错误处理不如 axios 方便:fetch 在 HTTP 状态码 404、500 时不会 reject Promise,只在网络层失败才报错。axios 是把所有非 2xx 状态码统一丢进 catch,心智负担小很多。我后来做项目,虽然 fetch 也够用,但团队统一约定用 axios,理由就是"代码风格统一、错误处理省心"。
2. URL 和查询参数,请先理清楚地址
2.1 URL 结构拆解:它就是一个网购收货地址
URL 全称是 Uniform Resource Locator(统一资源定位符),简单说就是"资源在互联网上的地址"。一个完整 URL 长这样:
https://www.example.com:8080/api/users?page=1&size=10#top拆开看分别是:协议(https)、域名(www.example.com)、端口(8080)、路径(/api/users)、查询字符串(?page=1&size=10)、哈希(#top)。
我讲 URL 的时候喜欢用快递单比喻:协议是"你要用什么方式寄"(顺丰还是圆通),域名是"寄到哪个城市"(比如北京),端口是"这个城市里哪个营业厅"(默认 80/443 就是默认营业厅不用写),路径是"xx区 xx街道 xx小区"(服务器上的具体资源位置),查询字符串是"备注:放快递柜就行"。
新手最容易忽略的是端口。开发环境里很多后端接口跑在 8080、3000 这种端口,前端代码里如果漏了端口,请求会一直失败。我让新人排查接口问题,第一件事就是让他把完整 URL 复制到浏览器地址栏打开看看,能通说明地址无误,剩下的问题在请求方法和参数上;打不通,先检查协议、域名、端口三个部分。
2.2 查询参数:不是路径的一部分,是备注信息
查询字符串是 URL 里"?"号后面的一串键值对。比如:
https://api.example.com/search?keyword=iphone&page=2?keyword=iphone&page=2就是查询参数,多个参数之间用&连接。它不是路径的一部分,而是给服务器的"额外说明"——搜索关键词是什么、第几页。本质上,它是 GET 请求传参的主要手段。
用 axios 的时候,一般不要在 url 里手动拼接字符串,而是用params配置项:
// 不要这样写 axios.get('/search?keyword=iphone&page=2') // 推荐这样写 axios.get('/search', { params: { keyword: 'iphone', page: 2 } })这样写的好处是:代码可读性更高,不容易漏&符号,axios 内部还帮你处理了特殊字符的编码问题。但前提是你要知道查询参数的本质是 URL 的一部分,服务器收到的还是拼好的字符串。
2.3 URL 编码:中文参数为什么一传就乱码
我敢打赌,每个人迟早都会遇到一次"我在浏览器地址栏输入了中文,结果空格变成了 %20、中文变成一串 %E4%BD%A0"。这不是什么神秘魔法,而是URL 不能直接包含某些字符,必须按照规范编码。
URL 里允许的字符是有限的,像空格、中文、&、=、#、?这些字符要么有特殊含义,要么不在允许范围内,所以要用百分号编码(percent-encoding)转义。比如"你好"两个字在 UTF-8 编码下变成%E4%BD%A0%E5%A5%BD。
用 axios 传中文的时候不用慌,axios 内部会自动对 params 里的中文做encodeURIComponent编码。真正需要手动处理的场景是:你拿到的 URL 已经是别人拼好的、里面带着未编码的中文;或者你需要把一段"查询条件"作为参数值传给后端,比如keyword的值本身是"a&b",不编码的话后端收到的就是a和b两个键,逻辑完全错误。
手动编码用encodeURIComponent():
const keyword = encodeURIComponent('iPhone 15 Pro'); const url = `https://api.example.com/search?keyword=${keyword}`; console.log(url); // https://api.example.com/search?keyword=iPhone%2015%20Pro注意encodeURI()和encodeURIComponent()不一样:encodeURI()不会编码&、=、?这些 URL 结构字符,只用于编码整个 URL;而encodeURIComponent()要把一个值彻底转义,连&、=都不放过。凡是要把用户输入塞进 URL 当参数值,一律用后者,这是我在实践中总结出的铁律。
3. 请求方法与数据提交,别一提 POST 就只会传 JSON
3.1 五个常用的请求方法,各自是什么性格
HTTP 协议定义了很多请求方法,前端开发天天打交道的就五个:GET、POST、PUT、PATCH、DELETE。
GET:查询,获取数据。参数一般放在 URL 的查询字符串里,不放在请求体。GET 请求是"幂等"的,同一地址请求 10 次结果都一样,浏览器还可能缓存它,所以不要用它来做"删除、新增"这种有副作用的事情。
POST:新增数据,或者执行"不是简单查询"的操作。参数放在请求体(request body)里,浏览器不会缓存 POST 请求。凡是那种不确定该用什么方法的操作,一律用 POST,这是最保守也最安全的做法。
PUT:整体替换资源。比如你要把用户的name、phone、age全部更新一遍,用 PUT 把整条数据全量提交。
PATCH:局部更新资源。只改用户的名字,就只传{ name: '张三' },PATCH 就干了这件事。
DELETE:删除指定资源。语义就是删东西,参数可以放在 URL 路径里,也可以放在请求体里,看后端接口怎么设计的。
很多新人纠结"更新到底用 PUT 还是 PATCH",我的建议是:看后端同事的接口怎么定义,文档写什么用什么,但你自己要清楚这两个的区别。实际项目里不少团队连 PATCH 都不怎么用,统一 POST 一把梭也有的是,规范归规范,团队约定优先。
3.2 数据提交的三种姿势:表单、JSON、文件
新手用 axios 最容易踩坑的地方就在这。axios.post(url, data)里的data,你传一个对象进去,axios 默认会把它序列化成JSON字符串,请求头里自动带上Content-Type: application/json。但现在很多老项目、或后端用 Java Servlet / PHP 写的老接口,只认表单格式的提交。两种格式不一样,你传 JSON 过去,后端解析不出参数,返回 400 或者"参数不能为空",非常让人崩溃。
表单格式全称是application/x-www-form-urlencoded,长这样:
username=zhangsan&password=123456字段之间用&连接,值要做 URL 编码。而 JSON 格式长这样:
{"username":"zhangsan","password":"123456"}axios 提交表单格式有两种办法。第一种是用URLSearchParams构造对象:
const params = new URLSearchParams(); params.append('username', username); params.append('password', password); axios.post('/login', params)URLSearchParams是浏览器内置 API,axios 检测到传进来的是URLSearchParams实例,就会把 Content-Type 设为表单格式,序列化成正宗的key=value&key2=value2字符串。
第二种办法是用qs库或form-serialize这类工具(后面案例里会详细说)。如果你用的是 Element UI 里的qs.stringify,它也干一模一样的事。
再一种提交是文件上传,用FormData:
const fd = new FormData(); fd.append('avatar', fileInput.files[0]); axios.post('/upload', fd)传FormData时,axios 会自动帮你把 Content-Type 设成multipart/form-data并带上边界标记,你不需要手动设置 Content-Type,这是很多新手忍不住手写的坑,手动设反而会出问题。
3.3 Content-Type:这个请求头决定后端怎么读你的身体
Content-Type 前面已经出现很多次了,它的作用就一句话:告诉服务器,你现在发过来的请求体(body)是什么格式。服务器收到请求后,会先看这个请求头,再决定用什么解析方式去读 body。
后端工程师看到Content-Type: application/json,就会用 JSON 解析器;看到application/x-www-form-urlencoded,就会用表单解析器;看到multipart/form-data,就会按"文件+普通字段"的混合方式解析。所以你要么不传(让 axios 帮你自动决定),要么传对,千万不要自己写死比如'application/x-www-form-urlencoded',却传一个 JSON 对象进去,后端用表单解析器一读,啥也没读到。
我做一个简单的对照表,方便你记:
| 提交格式 | Content-Type 请求头 | 请求体样子 | 典型场景 |
|---|---|---|---|
| 查询参数 | 无(GET 时在 URL 里) | 无 body | 搜索、筛选、分页 |
| 表单格式 | application/x-www-form-urlencoded | name=zhangsan&age=18 | 传统表单提交、老接口 |
| JSON | application/json | {"name":"zhangsan"} | 现代 RESTful 接口 |
| 文件 | multipart/form-data | 二进制边界数据 | 图片上传、附件上传 |
遇到接口提交失败,第一步永远先看 Network 面板里的请求头 Content-Type 和你传入的数据格式是否匹配,这一步能解决一半问题。
4. HTTP 报文,不值得害怕
4.1 请求报文:你去餐厅点菜,也要递一张菜单
HTTP 报文这个词听起来吓人,说白了就是"HTTP 请求/响应在网络上传输的具体格式文本"。你只要在浏览器 F12 的 Network 面板里点开任何一个请求,看到的Headers选项卡,展示的就是报文内容。
请求报文分成四部分:请求行(起始行)、请求头、空行、请求体。
一个典型的 POST 请求报文大概长这样:
POST /api/login HTTP/1.1 Host: api.example.com Content-Type: application/json User-Agent: Mozilla/5.0 ... Content-Length: 45 {"username":"admin","password":"123456"}第一行POST /api/login HTTP/1.1是请求行,包含三样东西:请求方法、请求路径、HTTP 版本。接下来几行是请求头,键值对的形式,描述本次请求的元信息。空行是必须的,用来分隔"头部"和"主体"。最后是请求体,也就是你提交的实际数据。
新人做项目真的不用去记每个请求头是什么意思,但有两个要看熟:Content-Type(前面讲了)和Authorization(鉴权 token 放的位置,Bearer开头的字符串,登录之后接口能不能通过就靠它)。
4.2 响应报文:状态码先瞄一眼,再瞄响应体
服务器处理完请求之后返回的报文叫响应报文,也分四部分:状态行、响应头、空行、响应体。
HTTP/1.1 200 OK Content-Type: application/json Date: Mon, 05 Aug 2024 08:00:00 GMT {"code": 0, "message": "登录成功", "data": {...}}状态行里的200 OK是每个人都该认识的。HTTP 状态码按大类记忆特别简单:
2xx:成功。最常看到 200,表示 OK;201 表示 Created,常用于"新增成功"。3xx:重定向。比如 302 Found,浏览器会自动跳转到它给的新地址。4xx:客户端错误。404 是地址不存在,403 是没权限,401 是未登录/登录过期(token 无效)。5xx:服务器错误。500 是后端代码崩了,502/504 是网关超时或上游服务出问题。
我经常让新人做一个练习:故意请求一个不存在的地址,看返回的 404;故意不带 token 访问需要登录的接口,看 401/403。API 调试的直觉就是这么练出来的。
4.3 报文对排查到底有什么用
说实话,我日常工作里很少真的逐行读报文——因为浏览器已经帮你把所有字段结构化了。但你必须学会在 Network 面板里找关键信息,这是排查接口问题的最核心技能。
大概流程是这样:请求发出去后,F12 打开 Network,找到对应请求,先看状态码——如果是 4xx 说明前端传参或地址有问题,5xx 说明问题在后端;然后看请求 Headers 里 Content-Type 对不对、Authorization 带没带;再看 Payload(或 Request Body),确认参数名和值;最后看 Response,后端返回了啥错误信息。
有一次我带一个同事排查"新增用户一直失败",看状态码 400,请求体里分明有参数,后端却说"缺少 password"。我让他看 Payload 里传的参数名,发现他把password写成了passwd,跟接口文档不对应。这类错误光看代码很难发现,但只要把请求报文和接口文档放一起比对,十秒钟的事。
5. 接口文档到底该怎么读
5.1 一份接口文档先看什么
后端给你一份接口文档(现在流行 Swagger/OpenAPI 的网页版),你翻开来不要急着问东问西,先按顺序确认几个核心要素:
- 接口地址:包含 baseURL 的完整请求地址。注意区分测试环境和生产环境。
- 请求方法:GET 还是 POST,文档说啥就是啥。
- 请求参数:每个参数的名字、类型、是否必填、放在 query(URL 查询参数)里还是 body(请求体)里。
- 响应结构:成功时返回啥,
code、message、data什么结构;data是对象还是数组还是分页对象。 - 错误码:业务错误码的含义,比如
code: 10001表示用户名不存在,code: 10002表示密码错误。
用表格模拟一个登录接口文档:
| 项 | 内容 |
|---|---|
| 接口名称 | 用户登录 |
| 接口地址 | POST/api/login |
| 请求参数 | username必填,string,body;password必填,string,body |
| 成功响应 | { "code": 0, "message": "ok", "data": { "token": "xxx" } } |
| 失败响应 | { "code": 10001, "message": "用户名或密码错误" } |
实际开发中我见过不少新人非要凭空臆测参数名,比如文档写userName,他传username,然后一直跟后端扯皮。接口文档里的参数名,一个字符都不能自己改,也不要"我觉得文档写错了"自作主张换一个。不确定就先问,不要用猜的。
5.2 接口联调时容易忽略的细节
除了参数对不对,还要注意几个"文档里写了但你容易忽略"的点:
- baseURL:接口地址到底是
https://api.example.com/api/login还是你当前开发机器的http://localhost:8080/login?很多新人只复制了路径,忘了拼接域名。 - 接口鉴权:登录接口不用 token,但登录成功之后的接口基本都要。headers 里带
Authorization,格式是Bearer <token>还是直接裸 token,文档里会写。 - 参数命名风格:后端常用驼峰(
userName)或下划线(user_name),这个以文档为准。 - 时间格式化:接口要求
2024-08-05还是2024/08/05,传错格式后端照样可能解析失败,返回 400。 - 数组参数:传数组的时候,是
ids=[1,2]还是ids=1&ids=2,不同后端风格可能不同。
我自己的原则是:接口联调之前,先把文档里所有参数手写成一个清单,一个个核对需求,宁可花半小时看文档,也不要做完再返工。
6. 用一个用户登录案例把所有知识点串起来
6.1 需求:最简单的登录功能,隐藏了多少细节
我终于讲到这个最能说明问题的例子了。登录功能是每个前端新手的第一个"完整闭环"练习,因为它把这一篇文章里所有东西都用上了:你要认识接口地址(URL),要选请求方法(POST),要提交正确格式的数据(表单或 JSON),要读响应报文(status + body),要看接口文档(参数、错误码),最后根据结果跳转或提示。
需求很简单:页面上有一个用户名输入框、一个密码输入框、一个登录按钮。点击按钮后,向POST /api/login发送请求,成功就跳转到首页,失败就在页面上提示"用户名或密码错误"。
我特意不用 Vue/React,就用纯 HTML + 原生 JS + axios,这样你能看清楚核心逻辑,不受框架干扰。
6.2 form-serialize:为什么不用手动取一个个输入框的值
很多新人写表单取值都是这么写的:
const username = document.querySelector('#username').value; const password = document.querySelector('#password').value;表单就两个字段还行,字段一多,代码就变成复制粘贴大会,而且很容易漏字段、取错值。这里推荐一个轻量插件form-serialize,作用就一句话:根据表单里所有带 name 属性的控件,自动生成一个参数对象或字符串。
先引入插件(常见方式是 npm 安装或者直接引 CDN):
<script src="https://unpkg.com/form-serialize@0.7.2/dist/form-serialize.min.js"></script>调用的方式:
const data = serialize(document.querySelector('#loginForm'), { hash: true, empty: true }); // data => { username: 'admin', password: '123456' }这里的第二个参数配置项里,hash: true表示结果返回一个对象而不是字符串;empty: true表示空值的字段也保留在结果里。如果你不传hash: true,默认返回的是username=admin&password=123456这种字符串,直接用来当表单格式的请求体内容都可以,这正好呼应了前面讲的application/x-www-form-urlencoded。
有个大坑必须提醒一下:表单控件必须要有 name 属性,id 属性是不算数的。serialize只认 name。新手容易把 input 只写一个id="username",然后 serialize 结果里没有这个字段,半天排查不出来。
6.3 完整代码:从取参到提交,每一步都别想当然
完整写一个登录页:
<form id="loginForm"> <div> <label>用户名</label> <input type="text" name="username" placeholder="请输入用户名" /> </div> <div> <label>密码</label> <input type="password" name="password" placeholder="请输入密码" /> </div> <button type="button" id="btnLogin">登录</button> </form> <p id="tips" style="color: red"></p>注意这里的按钮我写的是type="button",不是type="submit"。为什么?因为我不想触发浏览器默认的表单提交行为(页面刷新),我自己用 JS 控制提交逻辑。如果你用了type="submit",要在 JS 里e.preventDefault()阻止默认行为,否则页面一闪就刷新,状态全没了,这也是新手很容易卡住的点。
JS 部分:
document.querySelector('#btnLogin').addEventListener('click', async function () { const form = document.querySelector('#loginForm'); // 把表单数据转为对象 const data = serialize(form, { hash: true, empty: true }); // 前端简单的非空校验 if (!data.username || !data.password) { document.querySelector('#tips').textContent = '请输入用户名和密码'; return; } // 按钮置灰,防止重复提交 this.disabled = true; try { const response = await axios.post('/api/login', data); // 这里假设后端返回的格式是 { code: 0, message: 'ok', data: { token } } if (response.data.code === 0) { // 保存 token,后续接口要用 localStorage.setItem('token', response.data.data.token); // 跳转到首页 location.href = '/index.html'; } else { // 后端业务逻辑失败,比如 code 10001 用户不存在等 document.querySelector('#tips').textContent = response.data.message || '登录失败'; } } catch (error) { // 网络错误或 HTTP 状态码非 2xx,进入这里 document.querySelector('#tips').textContent = '请求异常,请稍后重试'; console.error(error); } finally { this.disabled = false; // 恢复按钮 } });这段代码里值得品的地方不少:
第一步serialize(form, { hash: true, empty: true })如果表单里有 checkbox 或 radio,返回的 value 默认是字符串"on",而不是true/false。要让 checkbox 返回布尔值,得加一个配置项{ hash: true, empty: true, proper: true }。这个proper是 form-serialize 的老用户才知道的隐藏开关。我项目里做过不少表格筛选条件,就是因为 checkbox 一个"on"传过去,后端判读永远为真,排查了好一阵。
第二步前端校验很有必要。虽然后端一定会校验,但前端先拦一道,用户体验完全不一样:字段没填就给出提示,而不是等那一次网络请求白跑。
第三步this.disabled = true是防重复提交的经典手法。登录接口如果因为网络慢,用户手快点十次,会发十个请求,后端可能创建十个会话。按钮置灰是最简单粗暴好用的方案。有人问:用户重新点还是会触发吗?click 事件在 disabled 的按钮上根本不会触发,所以安全。
第四步 await + try/catch 是我最推荐的 axios 写法。response是完整的响应对象,response.data才是后端返回给你的业务数据。很多新人打印response发现里面有个 data 套 data,不知道取哪一层。记住:**response.data是 HTTP 层的响应体**,如果后端再包一层{ code, data },业务数据就要response.data.data。
6.4 这个案例把这一整篇的什么知识点用上了
拿这个登录接口来复盘,你会发现前面所有内容都落到了实处:
- URL 知识:
/api/login是接口路径,axios 会把 baseURL 自动拼接上去。你可在 Network 里看到完整请求 URL。 - 请求方法:登录用 POST,因为它在服务器上"创建了一次会话"。
- 数据提交:这里传给 axios 的是对象,默认 Content-Type 是
application/json,后端用 JSON 解析。如果你们的后端只认表单格式,把data改成new URLSearchParams(data)即可,一行修改。 - HTTP 报文:失败时看响应状态码、响应体里 code 和 message,你就能定位问题是网络、参数还是业务逻辑。
- 接口文档:
code: 0表示成功,非零表示失败,这些定义都来自后端给的文档。 - form-serialize:它省去了逐个取值、拼参数的体力活,让你专注处理真正的业务逻辑。
7. 新手最容易踩的坑(问题速查)
7.1 跨域报错:明明接口在 Postman 能通,浏览器就是不行
这是前端联调遇见的第一个"拦路虎"。你请求一个后端接口,浏览器报错:
Access to XMLHttpRequest at 'http://api.example.com/api/login' from origin 'http://localhost:5500' has been blocked by CORS policy这个报错里看到的英文就是CORS(Cross-Origin Resource Sharing)。原因一句话:浏览器出于安全策略,默认禁止一个"源"(协议+域名+端口)的网页向另一个源的服务器发 ajax 请求。你的页面跑在localhost:5500,接口在http://api.example.com,这俩源不一样,浏览器就拦了。
解决方法分两层。第一层是后端开启 CORS 响应头,比如:
Access-Control-Allow-Origin: *这是正确的最终解决方式。第二层是开发阶段用本地代理(比如 Vite 的 proxy、webpack-dev-server 的 proxy),让浏览器误以为请求是同源的。这里要注意:这不是浏览器出问题,也不是你前端代码的问题,所以不要在 axios 里瞎调各种配置,我在新手阶段干过这种蠢事:加了一堆自定义 header 和 mode,最后解决不了问题。
还有个经典误区:Postman 能通不代表浏览器能通。Postman 是个独立的客户端软件,不受浏览器同源策略限制。所以出现跨域报错时,别拿 Postman 说事,直接找后端开 CORS 或配置代理。
7.2 提交的参数不对:serialize 拿不到值
登录案例里我把name强调了很多遍,这里再列几个 serialize 的常见问题:
- 忘了加
name属性:控件没 name,serialize 直接忽略。 - 按钮的
name也被收集进去了:比如按钮上有name="login",serialize 会把按钮也当成一个字段收集。确保按钮没有 name 或者不需要的值给过滤掉。 disabled的表单控件不被收集:HTML 规范就是如此,被禁用的表单控件不会提交。你要么让它在提交前重新启用,要么手动把它值塞进去。- 复选框返回值是
"on":用proper: true解决。
7.3 请求发出去但是一直 pending
Network 面板里看到请求状态一直是 pending,等了半天也没结果。常见原因有两个:一是请求地址被跨域拦截,浏览器根本不发出去(或发了但响应被拦),另一种是后端接口处理太慢或服务没启动。前者看 Console 有 CORS 报错;后者看请求地址能不能在浏览器直接访问,能访问但是一直转圈,基本可以确定是后端的事了。
另一个小场景是请求被浏览器缓存:GET 请求如果服务器没有设置Cache-Control,浏览器可能直接返回缓存的旧数据,看起来就像"我改了代码没生效"。排查方法:Network 面板勾选 "Disable cache",或者加一个时间戳参数破除缓存:
axios.get('/api/list', { params: { _t: Date.now() } });7.4 状态码 401/403:登录失效的锅
开发后台管理系统一定会遇到这种情况:登录跳转明明已经做了,token 也存了,过一会儿再访问接口,开始疯狂报 401 Unauthorized 或 403 Forbidden。401 的意思是"你没有身份或身份无效",403 是"知道你是谁,但你没权限访问这个资源"。两者经常被混着用,有些后端不管三七二十一全返回 401,实际含义都差不多。
新手在这个坑里最容易犯的错是:只把报错打出来,却不知道要根据 401 自动跳转登录页。正确做法是在 axios 响应拦截器里做统一处理:
axios.interceptors.response.use( response => response, error => { if (error.response && error.response.status === 401) { // token 失效,清除本地信息并跳转到登录页 localStorage.removeItem('token'); location.href = '/login.html'; } return Promise.reject(error); } );这样所有接口都不用各自写"401就跳登录",统一收口在一处。实际项目里 token 相关的坑还远不止这些:token 过期策略、刷新 token、多个请求并发时 401 拦截多次触发——这些进阶内容以后可以单独写一篇。
7.5 异步代码顺序混乱:明明请求还没回来,就先用了数据
新手写完第一段 axios 代码时,十有八九会写成这样:
let list = []; axios.get('/api/list').then(res => { list = res.data; }); console.log(list); // 输出依然是 [],为什么?这正好呼应了文章一开始讲的异步概念。axios.get()发出请求后,代码立刻继续往下走了,console.log(list)执行的时候,响应根本还没回来,list当然还是空数组。这个问题的解法就是你用async/await:
async function getList() { const res = await axios.get('/api/list'); const list = res.data; console.log(list); // 现在有数据了 }await 会"暂停"当前函数,等 Promise 完成后再继续执行。记住一个原则:发请求之后要处理数据,就用 await 或 .then,不要指望外面的变量在你没等的情况下自动填好。
至于 axios 二次封装、拦截器统一管理 token、请求取消、并发请求、文件上传进度条,这些都是这篇入门文章之外的内容。学完这篇,你至少能做到:看得懂接口文档、写得出发请求的代码、出了问题能在 Network 面板和报文里定位原因。这条主线捋顺了,后面那些"封装"内容只是水到渠成的事情。