ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

AJAX 基础实例:从请求参数编码到原生 XHR 与常见坑

AJAX 基础实例:从请求参数编码到原生 XHR 与常见坑 很多前端新人一开始接触 AJAX容易把它理解成“发个请求然后等数据”但真正用到实际项目里才发现光是把请求发出去远远不够。参数怎么传才不乱码、后端到底认哪种编码格式、为什么同一个请求有时候走缓存有时候不走、请求超时了要不要提示用户……这些细碎问题才是 AJAX 真正难掌握的地方。这篇内容围绕“AJAX 基础实例”展开我会用一个完整的用户名查重场景把原生 XMLHttpRequest、编码格式设置、参数赋值、常见坑全部串起来讲清楚最后补一个关于 VS2022 里使用 AjaxControlToolkit 的说明。适合刚入门前端、或者写过 AJAX 但一直“会用不懂原理”的开发者当作一次性补基础的手册来读。1. AJAX 到底解决什么问题先把设计思路理顺1.1 传统表单请求的痛点在哪里在 AJAX 普及之前网页要想向服务器要数据最常见的方式就是提交表单或者直接点击链接跳转。每一次交互都意味着浏览器要销毁当前页面、发送请求、等待服务器返回一个全新页面再重新渲染。这个过程的体验问题很明显白屏等待、状态丢失、带宽浪费而且用户只能被动等待什么也干不了。举一个最简单的例子注册页面里检查用户名是否已被占用。传统做法是用户填完整个表格、点提交、整个页面刷新然后服务器返回一个“用户名已被占用”的错误提示。用户还得后退、重新填一遍。这种流程放在今天的交互标准下几乎不可接受。AJAX 的核心思路就是打破这个“全页面刷新”的循环。它让浏览器在后台默默发一个请求拿到数据后只更新页面的一小块区域用户感知不到请求的发生也不会中断当前操作。你输入一个用户名光标离开输入框页面静悄悄发了一个请求几毫秒后旁边冒出提示“这个用户名已被注册”整个过程不刷新页面。这就是 AJAX 存在的根本理由。1.2 一次 AJAX 请求背后的完整生命周期要真正理解 AJAX不能把目光只放在“发送”这个动作上而要看全整条链路。一次标准的 AJAX 请求大致有四个阶段创建请求对象也就是 XMLHttpRequest 实例通过 open 方法配置请求方法、URL 和是否异步通过 send 方法把请求发出去在请求状态变化时触发回调拿到响应后做后续处理。这四个阶段里前两步几乎没有难点真正容易出问题的是最后一步——你怎么知道请求完成了响应是成功还是失败数据回来格式对不对这里就牵扯出 XMLHttpRequest 最核心的机制readyState 状态机。它有 0 到 4 五个状态值分别表示请求的不同阶段。其中 4 表示整个请求流程已经结束这是业务代码里最常判断的状态。你只需要监听 readystatechange 事件判断 readyState 是否为 4再去查看 HTTP 状态码是 200 还是 500就可以决定接下来做什么。这个“事件驱动 状态判断”的模型是 AJAX 区别于普通函数调用的关键所在。你写的不是“请求完成后执行这行代码”而是“当请求状态变化时检查是否完成再决定是否执行那行代码”。想清楚这个逻辑转换后面所有代码都顺了。2. 核心细节解析写好一个 AJAX 请求的关键环节2.1 创建请求和 readyState 状态机创建 XMLHttpRequest 在现代浏览器里非常简单一行代码var xhr new XMLHttpRequest();没有兼容性烦恼。不过这里有两个值得注意的地方。第一这个对象可以复用吗可以但需要在复用前调用 abort 或者重新走完 open 到 send 的流程实际开发中更推荐每次请求都新建一个对象避免僵持状态影响下一次请求。第二xhr 对象的生命周期不短事件监听要早绑定最好在 open 之前就设置好 onreadystatechange防止漏掉某些状态。关于 readyState很多老手写代码的时候其实只关心 4xhr.onreadystatechange function() { if (xhr.readyState 4) { // 请求完成考虑处理 status } };但如果你想追求更细腻的交互体验比如大响应体的加载进度条就会用到 readyState 等于 3 的情况代表响应体正在下载中。监听 progress 事件来反馈百分比数据的来源就是它。还有一个容易混淆的点readyState 是 4 只代表请求结束了不代表成功。判断业务成功与否还要看 HTTP 状态码。2xx 范围是成功304 是走缓存404 表示资源不存在500 是服务端异常所以真实代码里要同时判断 xhr.status。2.2 GET 还是 POST根据场景做选择开篇内容的实操里多数初学者会在这两个方法之间纠结。其实选择标准很明确请求的目的如果是“查数据”比如查用户名、查列表、查详情优先用 GET。GET 请求可以被浏览器缓存相同参数重复请求时会快很多而且方便分享 URL。请求的目的如果是“改数据”比如新增用户、修改密码、上传文件那就必须 POST。因为 POST 的请求参数放在请求体里不会暴露在 URL 上而且提交体积上限比 GET 大很多。一个常见的误区是“POST 比 GET 安全”。HTTP 本身是明文协议POST 参数在开发者工具里一样能看到。所谓安全只是相对的日常开发中不要指望用 HTTP 方法来做权限保护该上 HTTPS 就上 HTTPS该做服务端校验就做好。继续回到我们的用户名查重例子因为它是一个查询操作用 GET 就足够。URL 上带上要检查的用户名参数即可。2.3 给 AJAX 请求参数赋值的方法这部分是热词里“给ajax请求参数赋值”对应的核心内容。很多人在这里翻车翻得最多的就是“忘记编码”。所谓参数赋值其实就是把数据放到请求里随请求一起送达服务端。针对 GET 和 POST方式完全不同。先说 GET。GET 请求的查询参数拼在 URL 后面以问号开始用 连接多组键值对var url /api/checkUsername?userName encodeURIComponent(username); xhr.open(GET, url, true); xhr.send();注意这里用了 encodeURIComponent这是一个很多人会忽略的细节。用户输入的用户名可能包含中文、空格、特殊符号直接拼进 URL 会导致解析混乱或者请求失败。encodeURIComponent 会把特殊字符转换成百分号编码形态比如“张三”会被编码成“%E5%BC%A0%E4%B8%89”。服务端拿到后会自动解码你不需要多做什么但发送前必须保证数据是编码过的。再看 POST。POST 的参数放在请求体里有三种常见的赋值方式。第一种表单格式用同样的键值对字符串但需要手动设置 Content-Typevar xhr new XMLHttpRequest(); xhr.open(POST, /api/saveUser, true); xhr.setRequestHeader(Content-Type, application/x-www-form-urlencoded;charsetUTF-8); xhr.send(userName encodeURIComponent(username) age age);服务端解析时按表单格式处理这种方式非常适合简单的键值对数据。第二种FormData 格式适合带文件上传的情况var formData new FormData(); formData.append(userName, username); formData.append(avatar, fileInput.files[0]); var xhr new XMLHttpRequest(); xhr.open(POST, /api/upload, true); // 注意这里不要手动设置 Content-Type xhr.send(formData);一个关键点使用 FormData 时不要手动设置 Content-Type。浏览器会自动添加一个带 boundary 的 multipart/form-data 头你一旦手动设置反而可能因为 boundary 缺失导致服务端解析失败。第三种JSON 格式目前前后端分离项目中用得最多var xhr new XMLHttpRequest(); xhr.open(POST, /api/saveUser, true); xhr.setRequestHeader(Content-Type, application/json;charsetUTF-8); xhr.send(JSON.stringify({ userName: username, age: age }));JSON 格式对复杂嵌套结构支持更好服务端直接反序列化成对象即可是最推荐的方式。还需要强调一点无论哪种格式如果涉及字符串输入在拼接到请求里之前都要做编码处理。尤其是标签页输入框、URL 参数这一类外部输入这一步不做轻则数据变乱码重则成为安全隐患的来源。2.4 请求编码格式设置的实操原则“ajax请求设置编码格式”这个热词说白了就是要保证两点请求发出时的编码、服务端解析时的编码要一致。乱码问题九成都是编码不一致造成的。在 URL 上传递 GET 参数时浏览器会按页面编码处理页面是 UTF-8 就按 UTF-8 编码。现代项目统一 UTF-8 就没问题。但如果你接触遗留项目页面还在用 GBK那 GET 参数里的中文就非常容易出事。解决思路有两个要么前端发送前主动做一次编码转换要么后端在接收时统一用指定的字符集解码。更推荐的还是全链路统一 UTF-8方便省心。POST 请求的情况稍微复杂。application/x-www-form-urlencoded 格式本身支持 charset 参数后端解析时如果不指定字符集默认可能按 ISO-8859-1 或者平台默认编码处理中文就会变成乱码。实用的做法是在服务端框架里配置强制使用 UTF-8 解析请求体同时前端发送时也明确标出 UTF-8xhr.setRequestHeader(Content-Type, application/x-www-form-urlencoded;charsetUTF-8);对于 JSON 格式同样如此Content-Type 里显式带上 charset 反而更稳妥虽然很多框架不强制要求。核心原则一句话前后端统一编码不要各自为政。3. 实操从零实现一个完整的 AJAX 用户名查重功能3.1 设计思路与页面结构为了把上面那些理论落到实践中我用一个最常见的功能做演示用户注册页当用户输入用户名并移开光标时自动请求后端提示该用户名是否被占用。整个页面不刷新结果实时显示。前端页面结构很简单一个输入框一个放提示信息的容器div classregister-form label foruserName用户名/label input typetext iduserName nameuserName autocompleteoff / span iduserNameTip/span /div输入框监听 blur 事件也就是失焦时触发检查。为了演示方便也可以在 input 事件里做实时检测但真实项目为了避免频繁请求一般会结合防抖逻辑。我的例子里先用失焦触发逻辑更清晰。后端接口这里不写具体语言实现只约定接口行为。假设接口地址是 /api/checkUsernameGET 方式接收一个参数 userName返回 JSON 格式数据{ exists: true }exists 为 true 表示用户名已存在。3.2 原生 XHR 版本的完整实现把整个流程整合起来代码如下var input document.getElementById(userName); var tip document.getElementById(userNameTip); input.addEventListener(blur, function() { var userName input.value.trim(); if (!userName) { tip.textContent ; return; } var xhr new XMLHttpRequest(); var url /api/checkUsername?userName encodeURIComponent(userName); xhr.open(GET, url, true); xhr.setRequestHeader(Accept, application/json); xhr.onreadystatechange function() { if (xhr.readyState ! 4) { return; } if (xhr.status 200 xhr.status 300) { var result JSON.parse(xhr.responseText); if (result.exists) { tip.textContent 该用户名已被占用; tip.style.color #d9534f; } else { tip.textContent 该用户名可用; tip.style.color #5cb85c; } } else { tip.textContent 校验失败请稍后重试; tip.style.color #d9534f; } }; xhr.onerror function() { tip.textContent 网络异常请检查连接; tip.style.color #d9534f; }; xhr.send(); });这段代码里有一个之前反复强调过的点先判断 readyState 是否为 4再判断 status。为什么顺序不能反因为请求还没有完成时status 可能是 0直接判断 status 可能把还在进行中的请求误判为异常。另一个细节是 Accept 头发段。虽然它不影响后端返回的数据本身但它告诉服务端“我这边希望拿到 JSON 格式”。后端可以据此返回更合适的内容类型。这是一种良好的请求习惯。运行这个页面前我建议你先打开浏览器开发者工具的网络面板观察一下请求的完整过程。你会看到一个请求从 initiator 触发、到 request 发送、再到 response 返回的完整时间线。这对建立 AJAX 的直觉非常有帮助。3.3 fetch 版本对比写得更少理解更深原生 XHR 的代码确实有点啰嗦因此在现代浏览器环境里fetch 已经成为了更主流的替代方案。同样的功能用 fetch 实现会简洁不少input.addEventListener(blur, function() { var userName input.value.trim(); if (!userName) { tip.textContent ; return; } fetch(/api/checkUsername?userName encodeURIComponent(userName), { headers: { Accept: application/json } }) .then(function(response) { if (!response.ok) { throw new Error(HTTP response.status); } return response.json(); }) .then(function(result) { if (result.exists) { tip.textContent 该用户名已被占用; } else { tip.textContent 该用户名可用; } }) .catch(function(error) { tip.textContent 校验失败请稍后重试; }); });fetch 基于 Promise链式的写法让代码更直观。它把请求发送和响应解析拆成了两个环节response.ok 检查的是 HTTP 状态码是否在 2xx 范围这和 XHR 里判断 status 是一回事。但 fetch 也有坑当服务端返回 404 或者 500 时fetch 不会自动走到 catch 分支它一样会把响应对象返回给你。所以 response.ok 的判断绝不能省略。这一点和 XHR 的思维完全一致都是“请求完成不等于业务成功”。使用 fetch 时默认请求不带 cookie跨域场景的凭证模式也需要单独设置。如果你要沿用浏览器端会话状态记得加 credentials 配置。这些属于 fetch 的高级应用基础入门阶段知道存在即可。3.4 GET 请求缓存问题的现场处理在这个用户名查重例子里有一个很容易出现但不容易注意的现象连续用相同的用户名触发请求第二次请求可能根本没到服务器直接用了浏览器缓存里的结果。这在某些场景下是好事——比如用户头像资源、静态文件缓存能减少服务器压力。但对于用户名查重这种时效性很强的接口缓存可能让你查到旧数据用户刚刚注册成功你马上又检查结果从缓存里读到了“用户名可用”这就尴尬了。如何规避最简单粗暴的解决方案是给 URL 加一个时间戳参数var url /api/checkUsername?userName encodeURIComponent(userName) _ Date.now();这个参数服务端通常会忽略但浏览器认为是不同 URL就会绕过缓存发起新请求。更规范的思路是在后端响应头里设置 Cache-Control: no-cache这提示浏览器每次都要先向服务端确认响应是否有效。不过很多接口在设计时并不考虑这个问题所以前端加时间戳仍然是最省事的兜底方案。4. 常见问题与排查技巧实录4.1 中文乱码问题的排查思路Node.js 后端、Java 后端、Go 后端我都见过中文乱码的案例。排查乱码的思路不是只有“前端编码设置不对”这一条路而是要按照数据流逐段排查。第一步先用浏览器开发者工具看请求详情。如果是 GET 请求观察 URL 里中文变成什么样。如果 URL 里显示的是 %E5%BC%A0说明前端编码正常问题出在后端解析。如果 URL 里直接显示中文那就要看服务端能否正确接收非编码字符推荐改回 encodeURIComponent 编码。第二步如果是 POST 请求在开发者工具里查看请求标头的 Content-Type确认 charsetUTF-8 是否生效。再切到 Payload 栏看请求体里的数据是否已经是编码后的格式。第三步检查服务端框架的字符集配置。数据库、JDBC 连接串、Web 容器响应头里是否统一使用了 UTF-8任何一个环节缺失都可能从源头引入乱码。大部分乱码问题走到第三步就能定位。真正麻烦的是数据库已经存了乱码那就要先清洗存量数据再统一链路编码工作量会大很多。所以事前统一编码远比事后补救划算。4.2 HTTP 状态为 0 的几种可能性很多前端新手遇到“xhr.status 0”就懵了以为接口坏了。实际上 status 为 0 通常意味着请求根本没有得到有效响应。常见原因包括请求被浏览器拦截比如跨域且服务端未配置 CORS请求发出后浏览器侧主动阻断请求未发送成功比如 URL 写错、域名解析失败用户中断了请求比如调用了 abort 方法文件协议打开页面某些浏览器环境下接口请求被限制。排查这类问题时先看浏览器的网络面板请求有没有发出去、有没有返回响应内容、控制台有没有 CORS 错误信息。多数情况下status 0 并不是后端接口的问题而是前端环境和配置的问题。4.3 AjaxControlToolkit 与 VS2022 的兼容问题在 ASP.NET Web Forms 项目中AjaxControlToolkit 曾经是非常流行的页面增强组件库提供了日历控件、弹出面板、自动完成等一系列现成能力。热词里有同学在问“ajax control toolkit 20支持vs 2022吗”这里集中说明一下。AjaxControlToolkit 本身是 NuGet 包它的承载框架是 .NET Framework 的 Web Forms 工程。VS2022 完全支持编辑和编译这类项目只要项目本身能正常通过 NuGet 安装对应版本的 AjaxControlToolkit就可以正常使用。包版本如果选最新的稳定版目标框架一般设定为 .NET Framework 4.5 及以上VS2022 没有障碍。实际操作中有几个常见注意点页面里要用 ToolkitScriptManager 代替普通的 ScriptManager并把 AjaxControlToolkit 的命名空间注册到 Web.config 的 controls 节点某些较旧版本的控件在 VS2022 可视化编辑器中可能没有设计时预览这类问题属于编辑器支持和运行时无关不影响线上功能如果项目是从旧版本 .NET 升级上来的注意确认目标框架和包版本匹配否则可能出现编译冲突。不过说句实在话如果你的新项目还在用 Web Forms 和 AjaxControlToolkit那么长期维护的难度会逐步加大。Web Forms 是经典技术但已经不太适合新一代前后端分离的开发模式。新项目建议优先考虑 ASP.NET Core 提供 Web API前端用原生 AJAX、fetch 或者封装好的 HTTP 库进行交互。旧项目继续用 AjaxControlToolkit 没问题但新项目里不建议再引入它。4.4 网络异常和超时的处理细节AJAX 请求发出去后如果网络断掉、服务端挂掉或者请求长时间没有得到响应用户体验会非常差。原生 XHR 提供了超时机制xhr.timeout 5000; // 单位毫秒超过 5 秒视为超时 xhr.ontimeout function() { tip.textContent 请求超时请稍后重试; };timeout 属性设置后浏览器会在指定时间内没有收到响应时自动触发 ontimeout 回调。这里有一个细节超时时间只对等待响应的阶段生效不包括请求体上传的时间。如果要覆盖上传大文件的情况需要用 XMLHttpRequest 上传对象的 timeout 属性单独设置。使用 fetch 时原生 API 不自带超时配置需要借助 AbortController 来实现var controller new AbortController(); var timer setTimeout(function() { controller.abort(); }, 5000); fetch(url, { signal: controller.signal }) .then(function(response) { clearTimeout(timer); return response.json(); }) .catch(function(error) { if (error.name AbortError) { console.log(请求超时); } // 其他错误处理 });这个思路不仅适用于超时也可以用于“用户取消请求”的场景。用户切走页面或者点击了取消按钮直接调用 abort把请求中止掉避免无谓的服务器资源浪费。结尾的几句体会做了这么多年前后端我最大的感受是AJAX 的基础知识一点都不难难在每个细节都可能在不经意间给你挖坑。编码格式不一致、缓存策略没想清楚、状态码判断漏了分支、超时没处理每一处小问题都会变成线上事故。今天这个用户名查重的例子虽然简单但它把 AJAX 的核心链路都串起来了参数怎么封装、编码怎么设置、响应怎么判断、异常怎么兜底。你把这个例子彻底吃透原生 XHR 和 fetch 的区别也弄明白了后面再去用 axios 这类封装库就会觉得它们是水到渠成的东西而不是从天而降的黑魔法。最后分享一个我自己的习惯每次写完 AJAX 代码一定手动打开 DevTools 的 Network 面板把请求从头到尾看一遍只看一眼就能判断出问题出在前端、中间层还是后端这个习惯帮我省了很多排查时间。
返回列表