尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

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

深入解析XMLHttpRequest:从Ajax核心到文件上传实战

深入解析XMLHttpRequest:从Ajax核心到文件上传实战 1. 从“点击刷新”到“无感更新”聊聊XHR这位老伙计如果你在2005年左右开始接触网页开发那你一定经历过那个“点击按钮整个页面白屏刷新”的年代。那时候想在不刷新页面的情况下从服务器拿点数据更新页面的一小块内容简直是天方夜谭。直到XMLHttpRequest我们亲切地称之为XHR的出现这一切才被改变。它不是什么高深莫测的黑科技本质上就是一个内置于浏览器中的JavaScript对象但这个对象给了我们一个能力让JavaScript能在后台悄悄地和服务器“打电话”拿到数据后再悄悄地更新页面。这就是我们后来熟知的AjaxAsynchronous JavaScript and XML技术的核心。今天虽然fetchAPI凭借Promise的优雅后来居上但XHR这位“老将”依然活跃在无数生产环境的代码里理解它不仅是理解一段历史更是理解现代Web异步通信的基石。无论是处理文件上传进度还是兼容一些老旧的第三方库XHR的知识依然不可或缺。这篇文章我们就来彻底拆解XHR从它的基本使用到它的威力加强版——XHR Level 2的新特性并通过实际案例让你能真正上手。2. XMLHttpRequest基础与服务器“对话”的四步曲使用XHR对象与服务器交互就像完成一次标准的电话通讯有固定的步骤。理解每一步在做什么比死记硬背代码更重要。2.1 创建通话对象new XMLHttpRequest()一切始于创建一个XHR实例。这就像拿起电话听筒。const xhr new XMLHttpRequest();这里有个需要注意的历史兼容性问题在IE7之前的古老IE浏览器中XHR是以ActiveX对象的形式实现的。所以非常老的代码里可能会看到这样的写法let xhr; if (window.XMLHttpRequest) { // 现代浏览器 xhr new XMLHttpRequest(); } else { // 老掉牙的IE (IE5, IE6) xhr new ActiveXObject(Microsoft.XMLHTTP); }好在如今我们几乎不需要再考虑这种兼容性直接使用new XMLHttpRequest()即可。2.2 拨号与说明意图open() 方法创建对象后你需要告诉它我要打给谁URL用什么方式HTTP Method通话。xhr.open(GET, https://api.example.com/data, true);open方法通常接收三个关键参数method: 请求方法如‘GET’、‘POST’、‘PUT’、‘DELETE’。url: 请求发送到的服务器地址。async(可选): 是否异步默认为true。强烈建议永远使用true异步。如果设为false同步在收到服务器响应之前JavaScript代码会完全阻塞导致页面“冻结”用户体验极差已被现代前端实践所摒弃。这个步骤只是配置了请求并未真正发出。2.3 监听对方回应onreadystatechange 事件在拨号发送请求之前我们需要先设置好“接听”逻辑即定义当请求状态发生变化时的处理函数。这是通过监听readyState的变化来实现的。readyState有5个状态0 (UNSENT): 代理XHR对象已创建但open()方法还未调用。1 (OPENED):open()方法已调用连接已建立。2 (HEADERS_RECEIVED):send()方法已调用并且响应头已被接收。3 (LOADING): 响应体正在下载中responseText属性包含部分数据。4 (DONE): 整个请求操作已完成可能成功也可能失败如404。我们最关心的是状态4DONE。通常我们会这样写xhr.onreadystatechange function() { if (xhr.readyState 4) { // 请求已完成可以处理响应了 if (xhr.status 200 xhr.status 300) { // 判断HTTP状态码是否成功 console.log(请求成功, xhr.responseText); } else { console.error(请求失败状态码, xhr.status); } } };注意onreadystatechange是一个“状态变化”就触发的事件意味着在请求过程中状态1-2-3-4会触发多次。务必在函数内部通过if (xhr.readyState 4)来确保只在最终完成时进行业务处理避免重复操作。2.4 开始通话并传递信息send() 方法最后一步拨出电话并决定是否要说点什么发送请求体。// 对于GET请求通常不发送数据参数一般拼接在URL上 xhr.send(); // 对于POST请求可以将数据作为send()的参数发送 xhr.send(key1value1key2value2); // 发送表单格式数据 // 或者 xhr.send(JSON.stringify({key1: value1, key2: value2})); // 发送JSON数据调用send()方法后请求才真正被发出。对于异步请求async: trueJavaScript代码会继续往下执行不会等待响应。当服务器响应返回时之前设置的onreadystatechange事件处理器就会被调用。2.5 一个完整的GET请求示例将以上步骤组合起来一个完整的异步GET请求如下const xhr new XMLHttpRequest(); xhr.open(GET, https://api.example.com/users?id123, true); xhr.onreadystatechange function() { if (xhr.readyState 4) { if (xhr.status 200) { // 成功解析响应数据假设是JSON const userData JSON.parse(xhr.responseText); console.log(用户数据, userData); // 更新DOM... document.getElementById(username).textContent userData.name; } else { console.error(获取用户数据失败状态码, xhr.status); } } }; // 可以设置请求头例如告诉服务器我们期望JSON格式的返回 xhr.setRequestHeader(Accept, application/json); xhr.send();3. XHR Level 2老树开新花功能大增强最初的XHR规范存在不少局限比如无法上传文件、无法获取传输进度、不能跨域请求后来通过CORS解决但最初没有。XHR Level 2是原标准的升级版它增加了一系列强大的新特性让XHR在现代Web开发中依然能打。这些特性如今已被所有现代浏览器广泛支持。3.1 进度事件告别“盲等”在Level 2之前我们只知道请求“进行中”和“完成”完全不知道下载或上传了多少数据。这对于大文件传输来说体验很糟糕。Level 2引入了进度事件让我们可以轻松实现进度条。主要事件有progress: 在数据传输过程中周期性触发用于报告上传或下载的进度。load: 当请求成功完成时触发等价于readyState 4且状态码为成功系列。error: 当请求遭遇网络错误时触发如DNS解析失败、网络断开。abort: 当请求被调用abort()方法取消时触发。loadend: 在请求结束时触发无论成功、失败还是取消在load、error或abort事件之后触发。下载进度示例xhr.onprogress function(event) { if (event.lengthComputable) { // 长度是否可计算 const percentComplete (event.loaded / event.total) * 100; console.log(下载进度${percentComplete.toFixed(2)}%); // 更新进度条UI progressBar.value percentComplete; } }; xhr.onload function() { console.log(下载完成); progressBar.style.display none; // 隐藏进度条 };上传进度示例 上传进度监控需要监听**xhr.upload**对象上的事件。// 监听上传进度 xhr.upload.onprogress function(event) { if (event.lengthComputable) { const percentComplete (event.loaded / event.total) * 100; console.log(上传进度${percentComplete.toFixed(2)}%); uploadProgressBar.value percentComplete; } }; xhr.upload.onload function() { console.log(上传完成); }; xhr.open(POST, /upload); xhr.send(formData); // 假设formData是一个包含大文件的FormData对象实操心得progress事件触发非常频繁不要在其中执行复杂的DOM操作或计算否则可能引发性能问题。通常的做法是使用requestAnimationFrame进行节流或者只更新一个简单的文本或进度条数值。3.2 FormData原生表单数据“打包器”在Level 2之前要构造一个复杂的multipart/form-data请求比如上传文件需要手动拼接复杂的格式字符串极其繁琐且容易出错。FormData对象的出现完美解决了这个问题。你可以把它看作一个智能的表单数据容器// 1. 从现有表单元素创建 const myForm document.getElementById(myForm); const formData new FormData(myForm); // 自动收集表单内所有有name属性的元素值 // 2. 或者手动创建并添加数据 const formData new FormData(); formData.append(username, 张三); // 文本字段 formData.append(avatar, fileInputElement.files[0]); // 文件字段 formData.append(extraData, JSON.stringify({ key: value })); // 甚至可以附加JSON字符串 // 3. 直接使用FormData发送请求 const xhr new XMLHttpRequest(); xhr.open(POST, /submit); xhr.send(formData); // 无需设置Content-Type头XHR会自动设置为multipart/form-data使用FormData后XHR会自动设置正确的Content-Type请求头multipart/form-data并带有边界符浏览器会负责处理复杂的编码工作我们只需关心数据和文件本身。3.3 超时控制给请求加上“倒计时”网络环境复杂一个请求可能永远没有回应。Level 2提供了timeout属性可以设置请求的超时时间毫秒。如果请求在指定时间内未完成则会自动触发ontimeout事件并中断请求。xhr.timeout 10000; // 设置超时时间为10秒 xhr.ontimeout function() { console.error(请求超时); // 给用户提示并可能进行重试逻辑 };3.4 响应类型responseType指定期望的返回格式早期XHR的响应数据主要以文本形式responseText或XML文档responseXML存在。Level 2扩展了responseType属性允许我们告诉浏览器我们希望以什么格式接收响应数据浏览器会帮我们做好解析。(默认): 字符串文本对应responseText。text: 字符串文本。json: 浏览器会自动尝试将响应文本解析为JavaScript对象结果保存在response属性中。如果解析失败response为null但不会抛出错误。blob: 二进制大对象如图片、文件流结果在response中。arraybuffer: 类型化数组缓冲区用于处理纯粹的二进制数据。document: XML/HTML文档对象对应responseXML。使用示例xhr.responseType json; // 告诉浏览器“我期待JSON格式的响应” xhr.onload function() { if (xhr.status 200) { // 直接使用xhr.response它已经是一个JavaScript对象了 const data xhr.response; console.log(data.user.name); // 无需再手动 JSON.parse(xhr.responseText) } };设置responseType: json是一个非常好的实践它让代码更清晰并且浏览器原生的JSON解析通常比JSON.parse()更安全、高效。4. 实战案例构建一个带进度条的文件上传组件现在我们将Level 2的新特性组合起来实现一个完整的、带实时进度显示和预览的文件上传功能。4.1 HTML结构简单的上传界面!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleXHR Level2 文件上传示例/title style .upload-area { border: 2px dashed #ccc; padding: 40px; text-align: center; margin: 20px 0; cursor: pointer; } .upload-area.dragover { border-color: #007bff; background-color: #f0f8ff; } #progressContainer { margin-top: 20px; display: none; } #progressBar { width: 100%; height: 20px; } #preview { max-width: 300px; margin-top: 20px; } /style /head body h2拖拽或选择文件上传/h2 div iddropArea classupload-area p点击选择文件或直接将文件拖拽到此区域/p input typefile idfileInput acceptimage/* styledisplay: none; /div div idprogressContainer p上传进度span idprogressText0%/span/p progress idprogressBar value0 max100/progress /div img idpreview src alt图片预览 script srcupload.js/script /body /html4.2 JavaScript逻辑核心上传与进度监控创建upload.js文件实现完整逻辑(function() { const dropArea document.getElementById(dropArea); const fileInput document.getElementById(fileInput); const progressContainer document.getElementById(progressContainer); const progressBar document.getElementById(progressBar); const progressText document.getElementById(progressText); const preview document.getElementById(preview); let selectedFile null; // 1. 点击区域触发文件选择 dropArea.addEventListener(click, () fileInput.click()); // 2. 文件选择变化事件 fileInput.addEventListener(change, (e) { if (e.target.files.length 0) { handleFileSelection(e.target.files[0]); } }); // 3. 拖拽事件处理 dropArea.addEventListener(dragover, (e) { e.preventDefault(); dropArea.classList.add(dragover); }); dropArea.addEventListener(dragleave, () { dropArea.classList.remove(dragover); }); dropArea.addEventListener(drop, (e) { e.preventDefault(); dropArea.classList.remove(dragover); if (e.dataTransfer.files.length 0) { handleFileSelection(e.dataTransfer.files[0]); } }); // 处理选中的文件 function handleFileSelection(file) { // 简单的文件类型校验仅图片 if (!file.type.startsWith(image/)) { alert(请选择图片文件); return; } selectedFile file; console.log(已选择文件, file.name, 大小, (file.size / 1024 / 1024).toFixed(2), MB); // 预览图片 const reader new FileReader(); reader.onload (e) { preview.src e.target.result; preview.style.display block; }; reader.readAsDataURL(file); // 开始上传 uploadFile(file); } // 4. 核心上传函数 function uploadFile(file) { const formData new FormData(); formData.append(file, file); // 字段名‘file’需与后端接口约定一致 formData.append(uploadTime, new Date().toISOString()); // 可以附加其他数据 const xhr new XMLHttpRequest(); // 配置请求 xhr.open(POST, /api/upload); // 替换为你的实际上传接口地址 xhr.timeout 60000; // 设置60秒超时 // 显示进度容器 progressContainer.style.display block; progressBar.value 0; progressText.textContent 0%; // 监听上传进度事件关键监听的是 xhr.upload xhr.upload.onprogress (event) { if (event.lengthComputable) { const percent Math.round((event.loaded / event.total) * 100); progressBar.value percent; progressText.textContent ${percent}%; console.log(上传中: ${percent}%); } }; // 监听请求完成事件 xhr.onload () { progressText.textContent 处理中...; if (xhr.status 200 xhr.status 300) { try { const response JSON.parse(xhr.responseText); console.log(上传成功, response); progressText.textContent 上传成功; alert(文件上传成功服务器返回路径${response.filePath}); } catch (e) { console.error(响应解析失败, e); progressText.textContent 响应解析失败; } } else { console.error(上传失败状态码, xhr.status); progressText.textContent 上传失败 (${xhr.status}); alert(上传失败请重试。); } // 3秒后隐藏进度条 setTimeout(() { progressContainer.style.display none; }, 3000); }; // 错误处理 xhr.onerror () { console.error(网络请求错误); progressText.textContent 网络错误; alert(网络异常请检查连接。); progressContainer.style.display none; }; xhr.ontimeout () { console.error(请求超时); progressText.textContent 请求超时; alert(上传时间过长请重试。); progressContainer.style.display none; }; // 发送请求 xhr.send(formData); console.log(上传请求已发送...); } })();4.3 案例解析与关键点这个案例集中展示了XHR Level 2的多个核心特性FormData的便捷性我们无需关心multipart/form-data的边界符和格式只需将File对象append进去XHR会处理一切。上传进度监控通过监听xhr.upload.onprogress事件我们获得了精确的上传进度数据并实时更新了进度条UI。这是实现良好用户体验的关键。更清晰的事件模型我们使用了onload成功完成替代了onreadystatechange来检查状态4代码意图更明确。同时结合onerror和ontimeout进行全面的错误处理。超时控制设置了timeout为60秒防止因网络问题导致请求无限挂起。前端预览通过FileReader在文件上传前就实现了本地图片预览提升了交互的即时性。注意事项实际部署时你需要将xhr.open(‘POST’, ‘/api/upload’)中的URL替换为真实的后端接口地址。并且后端接口需要能够处理multipart/form-data格式的请求解析出名为file的文件字段。5. 常见问题、排查技巧与兼容性考量即使掌握了基本用法在实际开发中你仍会遇到一些坑。这里记录了一些典型问题和我的处理经验。5.1 为什么我的POST请求数据后端收不到这可能是最常遇到的问题之一。问题通常出在请求头Content-Type和数据格式不匹配上。场景一发送JSON数据。错误做法只调用了xhr.send(JSON.stringify(data))但没有设置Content-Type头。后端可能无法识别这是JSON。正确做法xhr.open(POST, /api/user); xhr.setRequestHeader(Content-Type, application/json; charsetutf-8); // 关键 xhr.send(JSON.stringify({ name: 张三, age: 25 }));场景二发送FormData数据。错误做法手动设置了Content-Type: multipart/form-data。正确做法不要设置Content-Type头当你使用FormData对象作为send()的参数时XHR会自动设置正确的Content-Type并且会包含一个唯一的boundary边界符。如果你手动设置反而会破坏这个自动生成的边界符导致后端解析失败。const formData new FormData(); formData.append(key, value); xhr.open(POST, /api/upload); // xhr.setRequestHeader(Content-Type, multipart/form-data); // 千万不要加这行 xhr.send(formData);5.2 跨域请求CORS问题如果你从http://localhost:8080向https://api.another-domain.com发送XHR请求浏览器会因为同源策略而阻止。控制台会报错“Access to XMLHttpRequest at ‘…’ from origin ‘…’ has been blocked by CORS policy”。解决方案取决于你是否能控制后端服务器能控制后端在后端服务器的响应中添加CORS头。Access-Control-Allow-Origin: http://localhost:8080 // 或 * (允许所有域不推荐用于生产环境携带凭证时) Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, Authorization不能控制后端对于简单的GET请求可以考虑使用JSONP仅限GET。对于现代API通常后端已配置CORS。如果是第三方公开API请查阅其文档是否支持CORS。否则你需要通过自己的后端服务器做一次代理转发。5.3 如何取消一个正在进行的XHR请求在某些场景下如用户跳转页面、搜索框输入防抖需要取消未完成的请求。XHR提供了abort()方法。let currentXhr null; function search(keyword) { // 如果已有请求在进行先取消它 if (currentXhr currentXhr.readyState ! 4) { currentXhr.abort(); console.log(已取消上一个搜索请求); } currentXhr new XMLHttpRequest(); currentXhr.open(GET, /api/search?q${encodeURIComponent(keyword)}); currentXhr.onload function() { /* ... 处理结果 ... */ }; currentXhr.onerror function() { /* ... 处理错误 ... */ }; currentXhr.send(); }调用abort()后会触发XHR对象的abort事件readyState也会变为4。5.4 responseType: ‘json’ 的陷阱设置responseType: ‘json’非常方便但有一个细节如果服务器返回的不是合法的JSON字符串比如返回了HTML错误页面或空字符串xhr.response的值会是null而不会抛出异常。你的代码需要处理这种null情况。xhr.responseType json; xhr.onload function() { if (xhr.status 200) { const data xhr.response; // 可能是对象也可能是null if (data) { // 成功解析为JSON processData(data); } else { // 响应体不是合法JSON console.error(服务器返回了非JSON格式的响应); // 可以尝试用xhr.responseText查看原始文本 console.log(原始响应, xhr.responseText); } } };5.5 与现代fetch API的对比与选择如今fetch()API凭借基于Promise的链式调用、更简洁的语法成为新的标准。那么我们还需要XHR吗fetch的优势语法更简洁基于Promise易于组合和使用async/await。更现代是W3C标准代表了未来方向。默认不携带Cookie行为更可控。依然选择XHR的场景需要上传进度监控fetchAPI目前截至2023年仍无法直接获取上传进度而XHR的upload.onprogress是现成的解决方案。需要请求超时控制fetch本身不支持超时设置需要借助AbortController而XHR的timeout属性简单直接。需要更细粒度的请求状态控制XHR的readyState在调试复杂请求流程时有时更有用。兼容性要求如果你的项目需要支持非常古老的浏览器如IE10及以下fetch不可用XHR是唯一选择。不过可以通过polyfill解决。我的个人建议是对于大多数新的项目优先使用fetch。当遇到需要监控上传进度这个硬性需求时再考虑使用XHR或者寻找基于fetch的进度监控polyfill方案。将两者结合使用取长补短也是常见的做法。例如用fetch处理普通的JSON API请求用XHR专门处理大文件上传。
返回列表