主题
XMLHttpRequest 对象总结与跨域(Day14 2/2)
本页对应 Day14_Ajax,共 6 段视频,约 1 小时 27 分。
学习目标
- 能说明纯表单上传文件时必须使用
post并设置enctype="multipart/form-data",并通过请求头验证设置生效。 - 能使用 XHR 获取并区分响应状态码、状态描述、响应头和响应体,说明
responseText与response的差异。 - 能分别使用
JSON.parse(xhr.responseText)和设置xhr.responseType = 'json'解析 JSON 响应,并验证得到对象。 - 能设置
xhr.timeout并监听timeout事件,演示请求超时与正常响应的不同结果。 - 能监听
loadstart、progress、load、error、loadend和readystatechange,并利用loaded/total计算和展示下载进度。 - 能通过
open第三个参数对比同步与异步请求,说明回调执行顺序及同步请求阻塞主线程的现象。
知识点
本节总结代码(保留)
text
1. 设置请求报文
请求体:字符串、formData
请求体字符串三种格式: 无格式、querystring、json
2. 解析响应报文
响应状态码 响应状态描述
响应体
响应体是json格式的字符串
设置响应超时
3. 监听进度事件
readystatechange
loadstart
progress
load
error
loadend设置请求报文:先想清楚“发什么、按什么格式发”
为什么需要:Ajax 不是只把请求发出去就完了,后端要知道请求体的格式,才能正确解析。转写稿里用非 Ajax 表单上传文件时,第一次只写了 method="post",结果文件信息没上去,原因就是表单默认 enctype 是 application/x-www-form-urlencoded,不是文件上传格式。
怎么想:先分请求体类型。字符串请求体常见三种:无格式纯文本、querystring(如 a=1&b=2)、json(如 {"a":1});有文件或表单数据时用 formData。格式要和请求头 Content-Type 对上。请求头里的 Content-Type 是给服务器看的,告诉服务器“我请求体是什么格式”。
怎么做:表单上传文件要同时满足 method="post" 和 enctype="multipart/form-data"。转写稿中属性名一度写成 type、enc type,最后纠正为 enctype。用 XHR 时,字符串请求体按约定设置对应 Content-Type;formData 可直接作为请求体发送。
易错点:文件上传漏写 enctype;把 enctype 写成 type;以为表单默认能自动带文件。转写稿反复在请求头里查 Content-Type,就是确认格式有没有真正设置成功。
解析响应报文:响应行、响应头、响应体分开看
为什么需要:响应回来不只有“内容”,还有状态码、状态描述、响应头和响应体。只盯响应体,会把 404、500 也误判成“请求失败”,其实它们仍是服务器返回的完整响应。
怎么想:先看响应行:xhr.status 是状态码,xhr.statusText 是状态描述。再看响应头:getResponseHeader(字段名) 可取单个头,字段名大小写不敏感;getAllResponseHeaders() 返回的是按行拆开的字符串。最后看响应体:responseText 是文本;若响应体是 JSON 字符串,可用 JSON.parse(xhr.responseText),也可提前设 xhr.responseType = "json",再用 xhr.response 拿对象。
怎么做:转写稿在 onload 里输出 status、statusText;请求不存在的地址得到 404,浏览器标红,但请求响应过程完成,404 内容仍作为响应体。设置 responseType = "json" 后,不能再使用 responseText,否则会报错。超时用 xhr.timeout = 5000(毫秒),到点触发 ontimeout;转写稿用后端故意延时和随机延时调试,看到有时成功、有时超时。
易错点:把 404/500 当成 error 事件;responseType="json" 后还读 responseText;忘记超时是毫秒;把请求头的 Content-Type 和响应头的 Content-Type 混为一谈——前者描述请求体给服务器看,后者描述响应体给浏览器看。
监听进度事件:用状态和阶段区分请求生命周期
为什么需要:一个请求从初始化到结束有多个阶段。只监听 load 只能知道成功结束,失败、下载中、开始请求都看不到;进度事件能把过程拆开。
怎么想:readystatechange 看 readyState:0 初始,1 已调用 open,2 已调用 send,3 开始接收响应体且可能多次触发,4 接收完毕。loadstart 在开始请求时触发,转写稿中此时 readyState 为 1。progress 在接收响应体过程中多次触发,事件对象有 loaded 和 total,可做下载进度,total 常来自响应头 Content-Length。load 表示成功结束;error 只表示网络错误、请求发不出去;loadend 无论成功失败最后都会触发。
怎么做:成功处理放 load;失败处理放 error;收尾放 loadend;下载进度放 progress,用 loaded / total 乘 100 并保留两位小数。转写稿还保留 readystatechange 做参照,输出 1、2、3 多次、4,帮助理解阶段。
易错点:把 404/500 当 error——它们是服务器给了响应,算请求成功;只有请求发不出去才是网络错误。progress 的 total 依赖后端给 Content-Length,不给就难算总进度。readystatechange 中 readyState=3 可能触发很多次,不能每次当完成处理;真正处理完整响应要等 4,或直接用 load。
XHR 对象概述:先把“请求—响应”这件事想清楚
为什么需要:页面要和服务器交换数据,不能只会刷新整页。XHR(XMLHttpRequest)提供了对 HTTP 协议的较完整访问能力,能发 GET、POST、HEAD 等请求,也能同步或异步接收响应。
怎么想:把它看成浏览器里的“请求发起器 + 响应接收器”。名字里有 XML,但并不只收 XML,文本、JSON、HTML 等都可以。
怎么做:先创建对象,再 open() 初始化,再 send() 发送,最后在事件里处理响应。
易错点:不要因为名字叫 XMLHttpRequest 就以为只能处理 XML;真正决定响应如何解析的是响应内容和 responseType 等设置。
创建 XHR 对象:构造函数是入口
为什么需要:没有 XHR 实例,后面的 open()、send() 都无从调用。
怎么想:现代浏览器直接用构造函数创建;老 IE 才需要 ActiveX。
怎么做:
js
const xhr = new XMLHttpRequest();古老 IE(如 IE5、IE6)中:
js
// IE5、IE6
let xhr = new ActiveXObject("Microsoft.XMLHTTP");易错点:现代项目不要主动写 ActiveX 兼容代码;若维护老系统,也要先判断环境。转写稿里反复创建 XHR 对象,是为了说明“对象可以提前创建,响应处理要放在事件回调里”。
属性与状态:重点看 readyState、status、responseText/response
为什么需要:请求是过程性的,必须知道“现在到哪一步”“服务器给了什么状态”“响应体是什么”。
怎么想:readyState 表示请求阶段:0 已创建或 abort 重置;1 已调用 open();2 已调用 send() 且响应头/状态可获得;3 下载中,responseText 可能有部分数据;4 响应数据接收完毕。status 是 HTTP 状态码,statusText 是描述,如 200/OK。
怎么做:在 readystatechange 中观察状态;在 load 中处理成功完成后的响应。转写稿调试时看到:readyState 从 1 到 2,再到 3 可能触发多次,最后到 4;3 阶段响应体可能还不完整,不能急着当最终结果。
易错点:不要只看 readyState === 3 就处理完整响应;404、500 是服务器返回的 HTTP 响应,属于应用层结果,不等于网络层失败。只有请求发不出去等网络层异常才触发 error。
方法与请求头:open、send、setRequestHeader 的顺序
为什么需要:要指定请求方式、地址、请求体,还要告诉服务器请求体格式。
怎么想:open(method, url, [async], [user], [password]) 初始化;send() 发送,GET 通常无请求体,POST 可带请求体;setRequestHeader() 必须在 open() 之后、send() 之前调用。
怎么做:转写稿里用表单上传文件做对照:纯表单上传文件必须 method="post",并设置 enctype="multipart/form-data",否则文件信息不会按预期进入请求体。F12 的 Network 里看请求头 Content-Type 是否变成 multipart/form-data。
易错点:属性名是 enctype,不是 enc type;Content-Type 在请求头里是告诉服务器“请求体是什么格式”,在响应头里是告诉浏览器“响应体是什么格式”,两者不要混。XHR 发 POST 时,请求头也要按需设置。
事件:load、error、loadend、progress 各管什么
为什么需要:不同阶段要做不同处理:开始请求、下载进度、成功、失败、最终收尾。
怎么想:loadstart 在开始请求时触发;progress 在接收响应体过程中多次触发;load 在请求成功完成时触发;error 只在网络层异常时触发;loadend 无论成功还是失败都会触发。
怎么做:转写稿调试中,loadstart 触发时 readyState 为 1;响应体较大时 progress 会多次触发,可用事件对象的 loaded 和 total 做下载进度:
js
xhr.onprogress = function (event) {
const percent = ((event.loaded / event.total) * 100).toFixed(2);
console.log(`已下载 ${percent}%`);
};易错点:total 往往依赖响应头 Content-Length,后端不配合就可能拿不到;loadend 不是“成功事件”,它只表示结束;404 仍可能触发 load,不要用 load 简单等同于“业务成功”。
响应 JSON:responseText 与 responseType
为什么需要:后端常返回 JSON 字符串,前端要变成对象使用。
怎么想:responseText 永远是文本;response 的类型取决于 responseType。
怎么做:方式一,手动解析:
js
const data = JSON.parse(xhr.responseText);方式二,提前设置响应类型:
js
xhr.responseType = 'json';
// 响应完成后使用 xhr.response易错点:转写稿中设置 responseType = 'json' 后再访问 responseText 会报错;设置了响应类型,就用 response。另外 responseType 要在发送请求前设置好。
超时与同步异步:容错和主线程阻塞
为什么需要:后端可能很慢,不能无限等;同步请求会阻塞主线程,需要理解差异。
怎么想:xhr.timeout = 5000 表示 5 秒未响应触发 timeout;open() 第三个参数默认 true 表示异步,设为 false 表示同步。
怎么做:异步请求发出后,后续同步代码先执行,响应回来后才执行 onload;同步请求会卡住主线程,直到请求响应结束。转写稿用两个按钮对比:异步先输出“异步请求发出”,再输出“响应结束”;同步则先等响应结束。
易错点:同步请求体验差,可能卡死页面,实际开发绝大多数用异步;超时时间单位是毫秒,且要在 send() 前设置。
本节说明:转写稿实际在讲什么
给定笔记标题是“3 跨域”,但转写稿通篇围绕 Ajax 的请求与响应细节展开:表单上传文件、响应报文解析、JSON 响应、超时、进度事件、同步与异步;没有出现跨域配置、CORS、代理等跨域内容。为遵守“不得编造”,本文按转写稿扩写,不臆造跨域方案。转写稿中的“阿贾克斯/阿加克斯”统一纠正为 Ajax,“标单”纠正为表单,“pere”纠正为 file,“接森/杰森”纠正为 JSON,“太克来/ENC type”纠正为 enctype。
表单上传文件:POST 与 enctype 缺一不可
为什么需要:普通表单也能发请求,但上传文件不是普通文本提交,浏览器必须按 multipart 格式组织请求体,后端才能收到文件。
怎么想:转写稿中先写了一个非 Ajax 表单,提交后发现失败。原因有两层:一是上传文件应当用 POST;二是 form 默认的 enctype 是 application/x-www-form-urlencoded,不会把文件正确放进请求体。Ajax 发文件时常用 FormData,浏览器会自动处理;纯表单则必须手写 enctype="multipart/form-data"。
怎么做:
html
<form action="/" method="post" enctype="multipart/form-data">
<input type="file" name="avatar">
<button type="submit">上传</button>
</form>易错点:只改 method="post" 不够;属性名是 enctype,不是 type 或 enctype 的错拼。转写稿中一开始写错属性名,F12 里 Content-Type 仍是 application/x-www-form-urlencoded,文件信息为空;改对后才出现文件数据。
解析响应报文:先看响应行、响应头、响应体
为什么需要:Ajax 拿到的不只是“结果文本”,而是一份完整响应报文。调试时要能分清状态码、响应头和响应体。
怎么想:在 xhr.onload 中处理响应,因为请求是异步的,不能在 send() 后立刻读结果。响应行可看 xhr.status 与 xhr.statusText;响应头用 getResponseHeader() 或 getAllResponseHeaders();响应体用 responseText 或 response。
怎么做:
js
xhr.onload = () => {
console.log(xhr.status, xhr.statusText);
console.log(xhr.getResponseHeader('Content-Type'));
console.log(xhr.getAllResponseHeaders());
};易错点:404 也会触发完整响应过程,浏览器标红不代表请求没完成;getAllResponseHeaders() 返回的是字符串,不是对象。请求头里的 Content-Type 描述请求体,给服务器看;响应头里的 Content-Type 描述响应体,给浏览器看。
响应 JSON:JSON.parse 或 responseType
为什么需要:后端常返回 JSON 字符串。前端若直接输出 responseText,得到的是字符串,不是可操作对象。
怎么想:两条路。第一条:拿字符串后 JSON.parse(xhr.responseText)。第二条:提前设置 xhr.responseType = 'json',再用 xhr.response,浏览器自动解析。
怎么做:
js
xhr.responseType = 'json';
xhr.onload = () => {
console.log(xhr.response); // 对象
};易错点:设置 responseType 后,转写稿中再读 responseText 会报错;responseType 要在发送请求前设置。普通文本响应时,responseText 与 response 结果可能一样;JSON 响应才体现差异。
响应超时:给不可控的后端加容错
为什么需要:后端可能因数据库、网络等原因迟迟不响应。前端不能无限等,应设置超时。
怎么想:在 send() 前设置 xhr.timeout,并监听 ontimeout。转写稿中用后端故意延迟 10 秒的接口演示:不设超时会一直等;设 5 秒超时则触发超时;设 4 秒仍可能成功,取决于后端返回时间。
怎么做:
js
xhr.timeout = 5000;
xhr.ontimeout = () => console.log('响应超时');
xhr.onload = () => console.log('响应成功');易错点:超时时间单位是毫秒;超时要在发送前设置;超时与“请求失败”不是一回事,超时是等待超过阈值。
进度事件:readystatechange、load、error、loadend、progress
为什么需要:要知道请求从开始到结束处于什么阶段,尤其大响应体下载时可做进度提示。
怎么想:readystatechange 中 readyState 有 0—4:1 表示 open() 后初始化完成,2 表示 send() 后请求已发送,3 表示开始接收响应体且可能触发多次,4 表示响应体接收完毕。loadstart 开始请求时触发;progress 接收响应体过程中多次触发;load 成功结束触发;error 仅在网络错误、请求发不出去时触发;loadend 无论成功失败最后都触发。
怎么做:
js
xhr.onprogress = e => {
console.log(e.loaded, e.total);
};易错点:404、500 是服务器给了响应,算应用层“请求成功”,会触发 load,不触发 error;只有地址不存在、网络断开等请求发不出去才触发 error。e.total 常来自响应头 Content-Length,需要后端配合。
同步与异步:open 第三个参数
为什么需要:Ajax 默认异步,不阻塞主线程;但转写稿用同步请求做对比,帮助理解阻塞现象。
怎么想:xhr.open(method, url, async) 第三个参数默认 true,表示异步;设为 false 就是同步。异步请求发出后,后续同步代码继续执行,回调等响应回来再进队列;同步请求会占住主线程,直到响应结束和回调执行完,后面代码才能继续。
怎么做:
js
xhr.open('GET', '/getData', false); // 同步,不推荐易错点:同步请求会阻塞页面,实际开发绝大多数场景应使用异步;转写稿中同步请求时“响应结束”先于“同步请求发出”输出,正是因为主线程被同步过程占住。
课堂画面






代码示例
代码一:解析响应报文 + 处理 JSON 响应体
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>解析响应报文 & 处理 JSON 响应体</title>
</head>
<body>
<h1>XHR 解析响应报文</h1>
<button id="btn">点击请求</button>
<div id="box"></div>
<script>
// ① 创建 XHR 对象
const xhr = new XMLHttpRequest();
// ② 提前设置响应体类型(必须在 send 之前)
// 设置之后 xhr.response 会自动把 JSON 字符串解析成对象
xhr.responseType = 'json';
// ③ 响应的处理必须写在事件回调里(异步,send 之后拿不到结果)
xhr.onload = function () {
// 响应行:状态码 + 状态描述
console.log('响应状态码:', xhr.status); // 200 / 404 ...
console.log('响应状态描述:', xhr.statusText); // OK / Not Found
// 响应头:字段名不区分大小写
console.log('content-type:', xhr.getResponseHeader('content-type'));
console.log('date:', xhr.getResponseHeader('date'));
// 获取全部响应头,返回的是字符串(一行一个)
console.log('全部响应头:', xhr.getAllResponseHeaders());
// 响应体
console.log('响应体:', xhr.response); // 已解析成对象
// 另一种写法:不设置 responseType 时,responseText 是纯字符串,自己 parse
// const data = JSON.parse(xhr.responseText);
document.querySelector('#box').innerHTML =
'状态码:' + xhr.status + ',响应体:' + JSON.stringify(xhr.response);
};
// ④ 点击按钮:先 open 初始化,再 send 发送
const btn = document.querySelector('#btn');
btn.onclick = function () {
xhr.open('get', '/getUsers'); // 请求地址由后端路由提供
xhr.send(); // GET 无请求体,不传参数
};
</script>
</body>
</html>要点解释:
open()只是初始化请求(准备请求方式和 URL),真正的发送是send();setRequestHeader()必须夹在两者之间。- 响应只能在
load(或readystatechange中 readyState === 4)里处理,因为请求是异步的,send()后面那一行代码执行时响应还没回来。 - 一旦设置了
xhr.responseType = 'json',就不能再读xhr.responseText,否则浏览器会报错——这正是转写稿里“他不让我用了”的原因。要么用responseText+JSON.parse(),要么设置responseType+response,二选一。 getAllResponseHeaders()返回的是字符串而不是对象,想看结构得自己按行拆。- 易错点:404/500 也是“请求成功完成”(服务器给了响应),
load事件照样触发,只是状态码不是 200,要用xhr.status判断。
代码二:响应超时 + 进度事件
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>响应超时与进度事件</title>
<style>
#box { margin-top: 20px; width: 600px; padding: 20px; min-height: 200px; border: 1px solid #999; }
</style>
</head>
<body>
<h1>XHR 响应超时 & 进度事件</h1>
<button id="btn">点击请求</button>
<div id="box"></div>
<script>
const xhr = new XMLHttpRequest();
const btn = document.querySelector('#btn');
const box = document.querySelector('#box');
// 开始发送请求时触发,此时 readyState 是 1
xhr.onloadstart = function () {
console.log('loadstart,readyState =', xhr.readyState);
box.innerHTML = '开始请求...';
};
// 下载响应体的过程中周期性触发(可能触发很多次)
xhr.onprogress = function (e) {
if (e.lengthComputable) {
const percent = (e.loaded / e.total * 100).toFixed(2);
box.innerHTML = '已下载 ' + percent + '%';
}
};
// 响应成功且响应体接收完毕,此时 readyState 是 4
xhr.onload = function () {
box.innerHTML = '下载完毕';
};
// 只有网络层异常(请求发不出去)才触发;404/500 不触发
xhr.onerror = function () {
box.innerHTML = '请求失败(网络错误)';
};
// 无论成功还是失败,最后都会触发
xhr.onloadend = function () {
console.log('loadend:请求彻底结束');
};
// 超过 xhr.timeout 设置的毫秒数仍未收到响应时触发
xhr.ontimeout = function () {
box.innerHTML = '响应超时';
};
btn.onclick = function () {
xhr.open('get', '/getMoreData');
xhr.timeout = 5000; // 单位是毫秒,5 秒;必须在 send 之前设置
xhr.send();
};
</script>
</body>
</html>要点解释:
- 事件触发顺序大致是:
loadstart→progress(多次)→load或error→loadend;监听顺序不影响触发顺序,触发只由各自条件决定。 onprogress的e.loaded是已下载字节数、e.total是总长度,total来自响应头content-length,后端没给就拿不到(lengthComputable为 false),所以进度条需要前后端配合。xhr.timeout必须在send()之前设置;设置后如果后端响应慢于该时间,会走ontimeout而不是load。- 易错点:
onerror只在“请求根本发不出去”时触发(比如写了一个不存在的 IP 地址),404、500 都算服务器给了响应,走的是load。 - 老写法用
onreadystatechange判断xhr.readyState === 4才处理响应,因为该事件在 2、3 阶段会反复触发;现在用load即可,无需再判断。
代码三:同步请求与异步请求对比
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>同步请求和异步请求</title>
</head>
<body>
<h1>同步请求和异步请求</h1>
<button id="btn1">异步请求</button>
<button id="btn2">同步请求</button>
<script>
const xhr = new XMLHttpRequest();
xhr.onload = function () {
console.log('响应结束', xhr.responseText);
};
const btn1 = document.querySelector('#btn1');
const btn2 = document.querySelector('#btn2');
// 异步请求(默认):第三个参数不写就是 true
btn1.onclick = function () {
xhr.open('get', '/getData'); // 等价于 open('get', '/getData', true)
xhr.send();
console.log('异步请求发出'); // 先执行,onload 里的代码后执行
};
// 同步请求:第三个参数设为 false,主线程会被阻塞到响应接收完毕
btn2.onclick = function () {
xhr.open('get', '/getData', false);
xhr.send();
console.log('同步请求发出'); // 后执行,onload 里的代码先执行
};
</script>
</body>
</html>要点解释:
open()第三个参数表示“是否异步”,默认true;设为false就变成同步请求。- 异步:
send()立刻返回,主线程继续往下走,所以先打印“异步请求发出”,等响应回来、主线程空闲后才执行onload回调。 - 同步:
send()会一直占着主线程直到响应接收完毕,onload回调先执行完,才轮到下一行“同步请求发出”。 - 易错点:同步请求期间页面会“卡死”,其他代码、渲染都停下来,请求越大卡得越久;实际开发 99.99% 用异步,同步只在极特殊场景使用。
- 易错点:本例复用了同一个 XHR 对象,两个按钮共用一份
onload,所以无论点哪个按钮走的都是同一个回调——真实项目里每次请求最好新建一个 XHR 对象。
常见错误
1. 表单上传文件,后端能收到字段却收不到文件 现象:Network 里请求发出去了,后端 req.body 有 username,但 files 为空;或者 Content-Type 仍是 application/x-www-form-urlencoded。 原因:<form> 的 enctype 默认就是 urlencoded,浏览器不会因为 input type=file 自动改;且表单默认 method 是 GET。 修法:<form method="post" enctype="multipart/form-data">,三个都要写全。改完必须去 Network → 请求标头确认 Content-Type 变成 multipart/form-data; boundary=...,没看到 boundary 就是没生效(属性名拼错是最常见原因,是 enctype 不是 enctype/entype)。
2. setRequestHeader 报 InvalidStateError 现象:控制台 Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED. 原因:写在 open() 之前,或写在 send() 之后。 修法:严格卡在 open() 与 send() 之间。习惯上把 timeout、responseType、setRequestHeader 集中写成一个“配置块”,紧贴 send 上方。
3. 用 error 事件处理 404/500,结果什么都没发生 现象:请求返回 404,页面毫无反应,error 回调不执行。 原因:error 只在网络层失败(DNS 解析不了、连不上、断网、跨域被拦)时触发;404/500 是服务器给出的完整响应,属于“请求成功完成”。 修法:业务失败判断放进 load(或 readyState===4)里用 xhr.status 分流;error 只留给网络异常。同时补上 timeout 和 abort 两个终态事件,四个终态才算覆盖全。
4. send() 之后立刻读 responseText,拿到空串 现象:xhr.send(); console.log(xhr.responseText) 输出 ""。 原因:异步请求 send 只是“把请求交出去”,此时 readyState 还是 1,响应根本没回来。 修法:所有读取响应的代码必须放在 load 回调(或 readyState===4 分支)里。顺带一提,别在 readyState===3 时把 responseText 当完整数据用,那是半截数据。
5. 设置了 responseType = 'json',再访问 responseText 直接抛错 现象:Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text'. 原因:responseType 与 responseText 是二选一的设计,指定了非 text 类型后 responseText 被禁用。 修法:统一改用 xhr.response。要么走“responseText + JSON.parse”,要么走“responseType + response”,同一段代码里不要混用两套。
6. 进度条算出 Infinity% 或 NaN 现象:e.loaded / e.total * 100 显示 Infinity/NaN,进度条直接满格或空白。 原因:后端响应头没带 Content-Length,e.total 为 0;跨域时该响应头默认也不暴露给 JS。 修法:if (!e.total) { 走不确定态 UI };后端补 Content-Length,跨域时后端加 Access-Control-Expose-Headers: Content-Length。另外百分比记得 toFixed(2) 前先确认 total 有效。
7. 用 open(method, url, false) 发同步请求,页面卡死 现象:请求期间按钮点不动、动画停住、控制台没有任何输出,等好几秒才“唰”地全部打印出来。 原因:同步请求期间主线程被完全占用,后续所有代码和渲染都排队。 修法:第三参数省略或写 true。同步只在 Worker 或极少数必须顺序执行的场景才考虑,页面主线程一律异步。
练习
练习 1(找错) 下面这段表单上传代码有 3 处问题,改好后用 Network 面板验证请求头与请求体:
html
<form action="/upload" method="get" entype="multipart/form-data">
<input type="file">
<button>上传</button>
</form>验收标准:请求方法为 POST;Content-Type 含 boundary;后端能拿到文件字段(注意 input 缺 name 也收不到)。
练习 2(两种方式解析 JSON) 向 /api/users 发请求,分别用两种写法把响应体变成对象并 console.table 打印: ① responseText + JSON.parse;② 预先设 responseType + response。 额外要求:在 ② 的代码里故意再访问一次 responseText,把报错信息抄下来,说明为什么会报错。
练习 3(进度条) 请求一个较大的响应(可用后端 sleep 或大字符串模拟),实现:进度区实时显示“已下载 xx.xx%”,load 时替换为“下载完毕”,error 时显示“请求失败”。 加分项:如果 e.total 为 0,改为显示“已下载 xxx KB(总大小未知)”。
练习 4(事件判定表) 不写代码,先填表再上机验证。对以下四种情况,写出会触发哪些事件(load / error / timeout / abort / loadend)、readyState 走过哪些值: ① 请求不存在的路径返回 404;② 请求 http://192.0.2.1/x(不可达地址);③ timeout=1000 而后端 3 秒才响应;④ 在 send 后 200ms 调用 abort()。 验收标准:至少答对“404 不触发 error”和“loadend 必然触发”两条。
练习 5(同步 vs 异步输出顺序) 同一页面两个按钮,共用一个 xhr 对象:
js
btn1.onclick = () => { xhr.open('GET', '/api/data'); xhr.send(); console.log('A'); }
btn2.onclick = () => { xhr.open('GET', '/api/data', false); xhr.send(); console.log('B'); }
xhr.onload = () => console.log('C');先预测点 btn1、点 btn2 时控制台的输出顺序,再上机验证并解释差异。思考:如果两次点击间隔很短,共用同一个 xhr 会出什么问题?
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 表单上传文件注意事项 | 07:07 |
| 2 | 解析响应报文 | 14:04 |
| 3 | 响应json数据 | 07:07 |
| 4 | 响应超时 | 10:02 |
| 5 | 进度事件 | 30:20 |
| 6 | 同步和异步 | 18:28 |
小结
- 后端不关心请求来自表单还是 XHR,它只看报文。所以排错的第一动作永远是打开 Network 看请求头/响应头,而不是猜代码——表单上传文件的所有坑,本质上都是“请求头没写对”。
- 前端对一次请求的“控制权”只有三个窗口:open 之后 send 之前的配置窗口(请求头、timeout、responseType)、事件回调里的读取窗口、以及用 status 与事件类型做的分流窗口。配置写错位置,后面全白搭。
- 事件要分两类看:状态类(readystatechange、progress)会多次触发,只能用来做“过程展示”;终态类里 load / error / timeout / abort 四者互斥其一,loadend 无条件兜底。写清理逻辑放 loadend,写业务逻辑放 load。
error与“请求失败”不是同义词。只要服务器给了响应(哪怕 404、500),请求就算成功完成,业务失败要靠 status 判断。这个区分不搞清楚,错误处理永远写不对。responseType与responseText是互斥接口,选一条路走到底。日常 90% 场景只需记住:JSON 用responseType='json'+response,比手写 parse 少一次异常处理负担(但仍要考虑空响应体)。- 进度条不是纯前端能力,它依赖后端给的 Content-Length,跨域下还要额外暴露响应头。凡是“前端显示不出来”的进度类问题,先怀疑响应头。
- 同步请求是历史包袱,它把整个主线程押在一次网络往返上。现代代码里没有理由在主线程使用它,异步 + 事件回调已经能覆盖全部场景。
- 表单上传文件的三件套要背下来:
method="post"、enctype="multipart/form-data"、input有name。少任何一个,后端都收不到文件,而且报错信息通常很含糊,只能靠请求头反查。