做前端这些年,AJAX 一直是面试里最容易被低估的一道题。日常项目里大家普遍用 axios 封一层就完事,可一旦后台反馈“参数收不到”、页面把 JSON 显示成乱码、GET 请求永远走旧缓存,能快速定位问题的人,往往不是框架 API 背得最熟的人,而是把原生 AJAX 原理吃透的人。这篇文章不整那些拗口概念,就按实战里最常见的调用场景,把请求发出、参数拼接、编码控制、响应解析,再到 jQuery 和 layui 里的 ajax 写法,一个个拆开配上能直接跑的实例。适合刚接触前后端联调的前端新手,也适合长期用封装库、想回头补一补底层细节的开发者。
1. 先理解 AJAX 解决的到底是什么问题
1.1 传统表单请求的体验痛点
在没有 AJAX 的年代,网页要做一次数据查询,通常就是整一个 form 表单,用户填完点“提交”,浏览器把整个页面参数发给服务器,服务器动态生成一个全新的 HTML 返回,浏览器整个刷新一遍。这个过程有几个非常糟的体验:刷新会白屏,用户在等待时什么都做不了;页面滚动条直接回到顶部,用户刚看了一半的内容位置也没了;表单里已填好的数据被清空,如果服务器校验不通过,用户得重新填一遍。
更麻烦的是,哪怕你只是想更新页面左下角一个列表,浏览器也会把整站脚本、样式、图片重新请求一遍。对后端来说,每次都要重新拼一整张页面,前端只能被动接收整包 HTML,根本没有“局部更新”这种概念。那时候做后台管理系统,页面上点一次查询,整个页面都会闪一下,操作频繁一点,用户眼睛都要花了。AJAX 出现以后,这种交互模式才被彻底改变:页面脚本自己发请求,拿到数据后只更新需要变化的那一小块 DOM。
1.2 异步请求是如何做到不阻塞的
AJAX 全称 Asynchronous JavaScript And XML,核心在 Asynchronous 上。它不是浏览器新发明的技术,而是把 XMLHttpRequest 对象暴露给了 JavaScript,让脚本有能力在页面不跳转的情况下发 HTTP 请求。传统请求是由浏览器导航发起的“整页往返”,AJAX 则是页面里的脚本注册一个异步任务,等网络线程和服务器通信完成后,再把回调丢进 JavaScript 的任务队列。
很多初学者不理解为什么“异步”这么重要。你可以类比成去餐厅吃饭:同步请求像是站在柜台前死等厨师做完菜,期间你在原地干瞪眼,什么也做不了;异步请求像是取个号回座位坐着,可以聊天、看手机,厨房做完喊号你再去端菜。浏览器主线程本来就是单线程的,如果发一个同步请求让主线程死等网络返回,页面上的点击、动画、滚动全部卡住,用户稍微多点两下浏览器甚至会提示“脚本无响应”。AJAX 的优势就在这里:请求发出去之后,send() 方法立刻返回,主线程继续渲染和处理交互,响应到了再执行回调。
1.3 一次请求的完整生命周期
要动手写 AJAX,首先要看懂 XMLHttpRequest 的 readyState。这个属性会经历五个阶段,每次变化都会触发 readystatechange 事件:
| readyState | 阶段名称 | 含义 |
|---|---|---|
| 0 | UNSENT | XHR 对象已经创建,但还没调用 open() |
| 1 | OPENED | open() 已调用,请求地址和方法已确定 |
| 2 | HEADERS_RECEIVED | send() 已调用,响应头已经接收到 |
| 3 | LOADING | 响应体正在下载中 |
| 4 | DONE | 请求完成,成功或失败都算完成 |
还有一个关键点:readyState 变成 4 不代表业务成功,它只代表“这次网络交互结束了”。最终状态要看 xhr.status,也就是 HTTP 状态码:200 到 299 之间一般算成功,404 是地址不存在,500 是服务端内部错误。所以判断响应能不能正常处理,必须同时看 readyState 和 status 两件事。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从零手写一个 AJAX 请求实例
2.1 先创建 XMLHttpRequest
原生 AJAX 的第一步是创建 XHR 对象,代码非常简单:
javascript复制var xhr = new XMLHttpRequest();
如果你在维护老项目,可能会看到这样一段兼容代码:
javascript复制var xhr = null;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
// IE6 及以下才需要,现在基本可忽略
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
ActiveXObject 那一段在新项目里完全可以不写。不过看老代码时要知道它是什么,不然会一脸懵。
2.2 open 和 send 方法
对象创建好之后,需要调用 open() 来初始化请求:
javascript复制xhr.open(method, url, async);
三个参数分别是 HTTP 方法、请求地址、是否异步。method 建议写大写,虽然很多浏览器对大小写不敏感,但个别服务器和网关对请求方法大小写有要求,统一写 GET、POST 最稳妥。async 默认是 true,实际开发里几乎不会传 false,因为前面说过,同步请求会阻塞整个页面。
紧接着调用 send():
javascript复制xhr.send(null);
GET 请求没有请求体,这里传 null 或者不传都可以。如果是 POST 请求,这里要放实际的请求体内容,后面我会详细说。
2.3 完整 GET 请求实例
来看一个完整的例子。假设有一个接口返回用户信息,接口地址是 /api/user,需要传一个 userId:
javascript复制var xhr = new XMLHttpRequest();
xhr.timeout = 8000; // 超过 8 秒没响应就算超时
xhr.open('GET', '/api/user?userId=1001', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
// 传输结束,再看 HTTP 状态码
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
var user = JSON.parse(xhr.responseText);
document.getElementById('nickname').textContent = user.name;
} else {
console.error('HTTP error: ' + xhr.status);
}
}
};
xhr.ontimeout = function () {
console.error('请求超时');
};
xhr.send(null);
这里我把返回报文里的 JSON 字符串通过 JSON.parse 转成了对象。如果不转,user 就是一个长字符串,没法直接通过 user.name 取值。responseText 是字符串,responseXML 则是 XML 文档对象,下面单独讲。
2.4 解析 JSON 和 XML 两种响应
现在绝大多数接口都返回 JSON,但老系统里还有不少返回 XML 的,尤其是 ERP、金融行业的历史接口,服务端可能是用 libxml 或 Java DOM 解析生成的。遇到这种接口,要么后端直接转成 JSON,要么前端自己解析 XML。
XHR 的 responseXML 属性可以自动解析服务端返回的 XML,前提是响应头 Content-Type 是 text/xml 或 application/xml。如果 Content-Type 不对,responseXML 会是 null,建议改用 DOMParser 手动解析:
javascript复制var parser = new DOMParser();
var xmlDoc = parser.parseFromString(xhr.responseText, 'text/xml');
var items = xmlDoc.querySelectorAll('item title');
items.forEach(function (node) {
console.log(node.textContent);
});
JSON.parse 和 DOMParser 都可能因为格式错误抛异常,生产环境里最好包一层 try catch,否则一个脏数据就能让整个脚本崩溃。
3. 给 ajax 请求参数赋值的几种方式与传参细节
3.1 GET 参数拼接时的编码处理
GET 请求的参数是拼在 URL 的 query string 里的。如果参数值里只有普通字母和数字,直接拼就行:
javascript复制xhr.open('GET', '/api/user?page=1&size=10', true);
但实际业务里的关键字、名称往往包含中文和特殊字符。比如搜索“前端 & 后端”这个词,如果直接拼到 URL 里,“&”会被服务器当成参数分隔符,整个 URL 的语义就变了。所以单个参数值必须用 encodeURIComponent 转义:
javascript复制var keyword = '前端 & 后端';
var url = '/api/search?kw=' + encodeURIComponent(keyword);
// 结果类似:/api/search?kw=%E5%89%8D%E7%AB%AF%20%26%20%E5%90%8E%E7%AB%AF
xhr.open('GET', url, true);
注意别用错方法。encodeURI 只处理空格和中文,不会转义 &、=、? 这些 URL 保留字符;而我们需要的恰恰是把参数值里的特殊字符全部转掉,所以给单个参数值编码时要用 encodeURIComponent。
3.2 POST 请求发表单格式数据
POST 请求的数据放在请求体里,最常见的格式是 application/x-www-form-urlencoded。这种格式看起来和 GET 的 query string 一样:
javascript复制var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/user/save', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');
xhr.send('name=' + encodeURIComponent('张三') + '&age=18');
后端如果是 PHP、Java Servlet 这类传统框架,通常会自动解析这种格式到请求参数对象里。这里要提醒一句:请求体里的参数值同样要 encodeURIComponent,很多人只记得 GET 要转义,POST 表单格式直接拼中文,结果后端拿到一堆乱码。
3.3 POST 请求发 JSON 数据
前后端分离项目现在更常用 JSON 格式。用 XHR 发 JSON 时,要把 JS 对象先序列化成字符串:
javascript复制var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/user/save', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({
name: '张三',
age: 18,
tags: ['admin', 'editor']
}));
关键点在于 Content-Type 和请求体内容必须配套。如果前端设置了 application/json,发出去的 body 却是一个 name=abc 这样字符串,后端按 JSON 解析就会报错,有时直接返回 400。反过来,后端方法用 @RequestBody 接收 JSON,你却在请求头里写表单格式,body 也可能解析不出来。每次排查这类问题,先看 Network 面板里的 Content-Type 和 Payload 是不是一对。
3.4 用 FormData 完成表单和文件上传
当表单里包含文件时,拼接字符串的方式就不行了。文件是二进制,必须走 multipart/form-data。XHR 里最方便的方式是使用 FormData:
javascript复制var formData = new FormData();
formData.append('title', '技术文档');
formData.append('file', document.getElementById('fileInput').files[0]);
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload', true);
// 注意:不要手动设置 Content-Type,浏览器会自动生成带 boundary 的 multipart/form-data
xhr.send(formData);
这里有个高频坑:很多人习惯手动去 setRequestHeader('Content-Type', 'multipart/form-data')。一旦手动设置,浏览器无法自动补上 boundary 分隔符,后端解析文件时大概率会失败。正确做法是什么都不设,让浏览器自己生成完整的 Content-Type。
3.5 参数赋值时容易忽略的几个坑
结合我实际排查过的接口问题,“给 ajax 请求参数赋值”这一步能引出不少坑:
- 参数值可能是 0、false 或空字符串,这些值都是合法参数,赋值时不要把布尔值和数字当成“空”过滤掉。我曾经遇到一个同事写 if (param) 判断,结果筛选条件传 false 时后端收不到,排查半天才发现是这个判断把 false 丢掉了。
- 数组参数怎么传需要前后端约定。有的是 ids=1&ids=2,有的是 ids[]=1&ids[]=2,后端不同框架解析方式不一样。没有第三方库序列化时,前端手动拼格式很容易出错,最好直接和后端确认。
- 参数名大小写要一致。后端实体字段通常用驼峰 userId,前端如果传成 userid,映射失败时接口不报错,但后端取到的值是 null,这种问题很难一眼看出来。
4. ajax 请求设置编码格式与乱码处理
4.1 编码问题是一条长链
中文乱码是 AJAX 老生常谈的问题。很多人以为把页面 写上就万事大吉,其实编码是一整条链:前端页面本身的编码、URL 编码方式、HTTP 请求头里声明的字符集、服务器解码方式、数据库存储编码、响应头 Content-Type 里声明的 charset,任何一环不一致,显示出来就是乱码。
现在新系统基本统一用 UTF-8,链条上大多数环节都一致,乱码自然少。但维护老系统时还会遇到 GBK/GB2312 页面、老接口返回 GBK 编码数据的情况,这时候才知道编码原理多重要。
4.2 URL 里的中文为什么必须 encodeURIComponent
URL 标准里本来就不允许出现非 ASCII 字符。浏览器地址栏里直接输入中文,通常是浏览器替你做了编码,但这不意味着 XHR 拼 URL 时也会自动处理。如果不手动编码,可能出现几种情况:请求发出去了,服务端拿到的值不是你预期的那串字符;或者 URL 直接异常,请求根本发不出去。
所以无论 GET 还是 POST 表单格式,凡是用户输入的中文、特殊符号,都在前端先 encodeURIComponent,保证 URL 或请求体里只出现 ASCII 字符。服务器会按约定解码回原字符串。
4.3 通过 Content-Type 设置请求编码格式
请求头里的 charset 更像是“声明”,它告诉服务器“我发出来的内容是按什么字符集编码的”。比如:
javascript复制xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8');
但要注意,真正决定 body 字节编码的是发送前字符串的处理。XHR 调用 send(string) 时,字符串默认按 UTF-8 转成字节。如果你发送的是已经编码好的百分号字符串,那编码方式由 encodeURIComponent 决定,和 charset 声明关系不大。所以“设置编码格式”不是设置一个参数就能解决全部问题,关键还在于发送内容和接收方的解码规则一致。
如果你的项目确实需要发 GBK 编码,简单 send 字符串是做不到的,一种思路是用 Blob 指定字符集:
javascript复制var blob = new Blob([str], { type: 'application/x-www-form-urlencoded;charset=GBK' });
xhr.send(blob);
不过这种需求非常少,日常业务遇到 GBK 乱码,优先还是推动后端改成 UTF-8 输出。
4.4 响应乱码的几种处理方案
响应乱码通常是服务端返回的 charset 和内容实际编码不一致。比如服务器把一批数据用 GBK 输出,但响应头没带 charset 或错误地写了 UTF-8,浏览器按 UTF-8 去解 GBK 字节,中文自然全乱。
老式 XHR 里有一个小众 API,overrideMimeType,可以在 send() 之前强制指定浏览器按某种编码解析:
javascript复制var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/oldList', true);
xhr.overrideMimeType('application/json;charset=GBK');
xhr.send(null);
这个方法必须在 open() 之后、send() 之前调用。如果项目里用的是 fetch,没有 overrideMimeType,可以用 arrayBuffer 加 TextDecoder 自己解码:
javascript复制fetch('/api/oldList')
.then(function (response) {
return response.arrayBuffer();
})
.then(function (buffer) {
var text = new TextDecoder('gbk').decode(buffer);
var data = JSON.parse(text);
console.log(data);
});
TextDecoder 这种方式更通用,既能解析 JSON,也能处理普通文本接口。
5. 项目工程里常见的 ajax 调用方式
5.1 jQuery 的 $.ajax 配置拆解
jQuery 的 ajax 封装之所以流行,是因为它帮开发者处理了跨浏览器差异,还内置了参数序列化、JSON 自动解析、类型简写等方式。一个比较完整的 GET 请求配置长这样:
javascript复制$.ajax({
url: '/api/user/list',
type: 'GET',
data: {
page: 1,
pageSize: 10,
keyword: $('#keyword').val()
},
dataType: 'json',
timeout: 8000,
beforeSend: function (xhr) {
xhr.setRequestHeader('token', localStorage.getItem('token'));
},
success: function (res) {
// 这里 res 已经是被 jQuery 解析好的对象
console.log(res.data);
},
error: function (xhr, textStatus, errorThrown) {
console.error(textStatus, errorThrown);
}
});
几个参数说明一下。data 如果传对象,jQuery 会自动序列化成 URL 编码字符串;type 是 GET 时拼到 URL 上,type 是 POST 时放到请求体里。dataType: 'json' 会通知 jQuery 把响应的文本自动执行 JSON.parse,如果服务端返回的不是合法 JSON,会走进 error 回调并且 textStatus 是 parseerror。beforeSend 适合统一塞 token、时间戳这类公共信息。
5.2 $.get 和 $.post 简写用法
如果只是简单请求,不必写完整 $.ajax,可以用简写方法:
javascript复制$.get('/api/user/list', { page: 1, pageSize: 10 }, function (res) {
console.log(res);
}, 'json');
$.post('/api/user/save', { name: '张三', age: 18 }, function (res) {
console.log(res);
}, 'json');
第 4 个参数是期望的返回类型,写成 'json' 以后,回调里的 res 同样是解析后的对象。需要注意,$.post 默认请求体格式是表单格式,如果后端要求 JSON,还是要回到完整的 $.ajax 去设置 contentType 为 application/json。
5.3 layui ajax get 的正确写法
layui 后台管理模板用得非常广。它没有另起炉灶做一套 AJAX,内部依赖的是自带 jQuery 模块。用 layui 发 get 请求,先要在 layui.use 里加载 jquery 模块,再把 layui.jquery 赋值给一个变量:
javascript复制layui.use(['jquery', 'layer', 'table'], function () {
var $ = layui.jquery;
var layer = layui.layer;
var table = layui.table;
$('#searchBtn').on('click', function () {
$.ajax({
url: '/api/order/list',
type: 'get',
data: {
page: 1,
limit: 20,
keyword: $('#keyword').val().trim()
},
dataType: 'json',
success: function (res) {
if (res.code === 0) {
table.reload('orderTable', {
data: res.data
});
} else {
layer.msg(res.msg || '查询失败');
}
},
error: function (xhr, textStatus) {
layer.msg('网络异常,请稍后重试');
}
});
});
});
如果你在页面里另外引入了完整的 jQuery,要留意会不会和 layui 内置的 layui.jquery 冲突。我习惯在模块里用 var $ = layui.jquery 锁定一份,页面外层的 $ 不要混用,否则可能出现事件绑定正常、ajax 却报 $ is not a function 的怪问题。
5.4 从原生 XHR 视角看 fetch 和 axios
那现在是不是完全不用原生 XMLHttp
