Skip to content

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); // 同步,不推荐

易错点:同步请求会阻塞页面,实际开发绝大多数场景应使用异步;转写稿中同步请求时“响应结束”先于“同步请求发出”输出,正是因为主线程被同步过程占住。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

代码一:解析响应报文 + 处理 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

小结 ​

  1. 后端不关心请求来自表单还是 XHR,它只看报文。所以排错的第一动作永远是打开 Network 看请求头/响应头,而不是猜代码——表单上传文件的所有坑,本质上都是“请求头没写对”。
  2. 前端对一次请求的“控制权”只有三个窗口:open 之后 send 之前的配置窗口(请求头、timeout、responseType)、事件回调里的读取窗口、以及用 status 与事件类型做的分流窗口。配置写错位置,后面全白搭。
  3. 事件要分两类看:状态类(readystatechange、progress)会多次触发,只能用来做“过程展示”;终态类里 load / error / timeout / abort 四者互斥其一,loadend 无条件兜底。写清理逻辑放 loadend,写业务逻辑放 load。
  4. error 与“请求失败”不是同义词。只要服务器给了响应(哪怕 404、500),请求就算成功完成,业务失败要靠 status 判断。这个区分不搞清楚,错误处理永远写不对。
  5. responseType 与 responseText 是互斥接口,选一条路走到底。日常 90% 场景只需记住:JSON 用 responseType='json' + response,比手写 parse 少一次异常处理负担(但仍要考虑空响应体)。
  6. 进度条不是纯前端能力,它依赖后端给的 Content-Length,跨域下还要额外暴露响应头。凡是“前端显示不出来”的进度类问题,先怀疑响应头。
  7. 同步请求是历史包袱,它把整个主线程押在一次网络往返上。现代代码里没有理由在主线程使用它,异步 + 事件回调已经能覆盖全部场景。
  8. 表单上传文件的三件套要背下来:method="post"、enctype="multipart/form-data"、input 有 name。少任何一个,后端都收不到文件,而且报错信息通常很含糊,只能靠请求头反查。