Skip to content

跨域(Day15 1/2) ​

本页对应 Day15_Ajax,共 7 段视频,约 1 小时 49 分。

学习目标 ​

  1. 能够独立编写 XMLHttpRequest 的 GET 请求,按创建对象、监听事件、open 初始化、send 发送的流程完成请求,并使用 responseType 正确解析 JSON 响应。
  2. 能够依据协议、域名、端口三项判定同源关系,根据页面 URL 与 Ajax 请求 URL 判断是否跨域,并解释浏览器拦截响应的原因。
  3. 能够对比 CORS、JSONP、代理服务器三种跨域方案,说明各自适用场景及限制,例如 JSONP 仅支持 GET 请求。
  4. 能够配合服务端设置 Access-Control-Allow-Origin 等响应头完成 CORS 跨域,并通过浏览器 Network 面板验证请求结果与响应头。
  5. 能够实现 JSONP 跨域请求:动态创建 script、设置 src 并传递回调名、提前定义回调函数、挂载后移除 script,成功获取并处理返回数据。

知识点 ​

Ajax 的基本使用流程 ​

为什么需要:页面局部更新时,不应整页刷新;Ajax 让 JS 在页面运行中向后端发请求、拿响应。怎么想:按“创建对象→监听响应→初始化请求→发送请求”四步走。怎么做:

js
const xhr = new XMLHttpRequest();
xhr.onload = function () {
  console.log(xhr.responseText);
};
xhr.open('GET', '/getData', true);
xhr.send();

易错点:open 只做初始化,不会发请求;真正发送靠 send。事件监听最好在 send 之前完成;setRequestHeader 必须放在 open 之后、send 之前,否则设置不上。转写稿演示了把监听放到 open 后也可以,但不要依赖“send 后再监听”的偶然成功。

请求数据与请求头 ​

为什么需要:GET、POST 等请求都可能携带数据,但携带位置不同。怎么想:URL 查询字符串适合少量数据;请求体适合较多数据,字符串请求体常见有 text/plain、application/x-www-form-urlencoded、application/json,也可用 FormData。怎么做:GET 把数据拼到 URL;POST 用 send 传请求体,并用 setRequestHeader 设置匹配的 Content-Type。易错点:GET、HEAD 不能加请求体;请求体格式必须和 Content-Type 匹配,否则后端可能解析失败。FormData 可上传文件,字符串请求体做不到。

处理响应、超时与进度 ​

为什么需要:不仅要拿响应体,还要知道状态码、响应头、失败和进度。怎么想:响应行看 status/statusText,响应头用 getResponseHeader/getAllResponseHeaders,响应体用 responseText 或 response。设置 xhr.responseType='json' 后,xhr.response 可直接得到对象。怎么做:

js
xhr.timeout = 5000;
xhr.ontimeout = () => console.log('超时');
xhr.onerror = () => console.log('请求失败');
xhr.onprogress = e => console.log(e.loaded, e.total);
xhr.onloadend = () => console.log('响应彻底结束');

易错点:404、500 仍说明能收到响应,通常走 load;error 多指网络错误、请求发不出去。progress 可能触发多次;loadend 最后触发。转写稿还提到 loadstart 表示开始请求。

同步与异步 ​

为什么需要:请求响应要花时间,不能阻塞后续代码。怎么想:Ajax 的“异步”不是单指 open 或回调,而是从开始请求到响应彻底结束的整个过程是异步的。怎么做:open 第三个参数用 true。易错点:若用同步,后续代码会被卡住,直到请求响应结束,体验和性能都差。

同源策略 ​

为什么需要:浏览器用同源策略限制页面与不同源资源交互,本质是保护服务器数据。怎么想:比较“Ajax 代码所在页面 URL”和“Ajax 请求 URL”的协议、域名、端口,三者必须一致。怎么做:转写稿用 localhost 打开页面、Ajax 请求 127.0.0.1 演示:请求能发到后端,后端也能收到并响应,但浏览器报 CORS 错误并拒绝响应,load 不触发,error 可触发。易错点:同源策略是浏览器行为,不是服务器要求;请求已发出,只是响应被浏览器拦截。localhost 与 127.0.0.1 虽常等价,对同源策略仍算不同域名。

CORS 解决跨域 ​

为什么需要:合法跨域要服务器放行。怎么想:浏览器拒绝响应,服务器加“通行证”响应头,浏览器看到才放行。怎么做:服务器设置 Access-Control-Allow-Origin;可写单个域名,也可根据请求头 Origin 动态回写允许的域名;* 表示允许所有源。转写稿调试:加响应头后成功;多域名不能简单用逗号拼,浏览器会报该值无效;可用数组判断 Origin 后回写。携带 Cookie 还需 xhr.withCredentials=true 与 Access-Control-Allow-Credentials: true,且 Allow-Origin 不能是 *。易错点:前端不能单独解决 CORS;file:// 双击打开时 Origin 为 null,通常不被允许;跨域默认拿不到全部响应头,需 Access-Control-Expose-Headers。

JSONP 原理与步骤 ​

为什么需要:历史跨域方案,利用 script 标签天生可跨域,只支持 GET。怎么想:script 请求后端返回一段可执行 JS,JS 调用前端提前定义的函数,并把数据作为实参传入。怎么做:

js
function parseData(data) {
  console.log(data);
}
const script = document.createElement('script');
script.src = 'http://127.0.0.1:8080/getData02?cb=parseData';
document.body.appendChild(script);
document.body.removeChild(script);

易错点:函数名要前后端约定,常通过 cb 参数传;后端响应体必须是合法可执行 JS,如 parseData([...]),不能直接返回裸 JSON;先写死 script 会一刷新就请求,动态创建才能点按钮再请求;忘记 document.body 会报 body is not defined;添加后立即移除,请求仍已发出,但页面不会堆积 script。转写稿还演示了后端把对象转成 JSON 字符串拼进调用代码,前端执行后拿到数组再 map 渲染。

先看整体:AJAX 的四个动作 ​

AJAX 不是一种“新语言”,而是浏览器里用 XMLHttpRequest 发请求、拿响应的一套流程。为什么需要它?因为传统页面要靠整页刷新拿数据;AJAX 可以在页面不刷新的情况下,只向服务器要一段数据,再由 JS 更新页面。怎么想?把它拆成四步:创建对象、监听响应、初始化请求、正式发送。怎么做?最小代码如下:

js
const xhr = new XMLHttpRequest();

xhr.onload = () => {
  console.log(xhr.responseText);
};

xhr.open('GET', '/getData');
xhr.send();

易错点:open 只是做请求初始化,不会发请求;真正发送是 send。事件监听最好写在 send 之前,写在 open 前后区别不大,但不要等到 send 之后才加,否则响应太快时可能漏掉。

基本使用流程:创建、监听、初始化、发送 ​

为什么先创建对象?因为所有请求配置和响应结果都挂在这个 xhr 实例上。怎么想?顺序上可以记成“先有对象,再有回调,再配置,再发送”。怎么做?创建 new XMLHttpRequest();监听 onload;调用 open(method, url, async);调用 send(body)。转写稿里演示时,一打开页面就请求 /getData,控制台能看到两个请求:一个是页面本身,一个是 AJAX 请求。

易错点:open 的第三个参数控制同步/异步,默认是异步。不要把它和 send 混了;send 的参数取决于是否有请求体,GET 通常不传。

发送请求:URL、请求体字符串、FormData、请求头 ​

为什么请求数据要分位置?因为不同请求方式、不同数据量、不同数据类型,适合的携带方式不同。怎么想?先看数据放哪儿:URL 查询字符串、请求体字符串、FormData。怎么做?

通过 URL 携带数据,就是把数据拼成查询字符串,例如 /getData?name=a&age=18。所有请求方式都可以在 URL 上带查询参数,但数据量大时不适合。

通过请求体携带字符串,常见三种格式:text/plain、application/x-www-form-urlencoded(k=v&k2=v2)、application/json。必须用 setRequestHeader 设置匹配的 Content-Type:

js
xhr.open('POST', '/api');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ name: 'a' }));

通过请求体携带 FormData,可以 new FormData() 后 append,也可以 new FormData(formElement) 直接收表单数据;它还能上传文件,因为会把文件按二进制处理:

js
const fd = new FormData();
fd.append('name', 'a');
xhr.send(fd);

易错点:setRequestHeader 必须在 open 之后、send 之前;GET、HEAD 不能加请求体;FormData 不要手动乱设 Content-Type,浏览器会自动带 boundary。

处理响应:状态、响应头、响应体、JSON、超时 ​

为什么要在 onload 里取响应?因为此时响应已成功回来。怎么想?响应报文分响应行、响应头、响应体。怎么做?状态码用 xhr.status,状态描述用 xhr.statusText;响应头用 getResponseHeader / getAllResponseHeaders;响应体常用 xhr.responseText,或先设 xhr.responseType = 'json',再用 xhr.response 直接拿对象:

js
xhr.responseType = 'json';
xhr.onload = () => {
  console.log(xhr.response);
};

超时则设置 xhr.timeout = 5000,并监听 ontimeout。易错点:responseText 是字符串;要 JSON 对象要么自己 JSON.parse,要么设 responseType。超时是网络等意外,不是业务错误。

进度事件:loadstart、progress、load、error、loadend ​

为什么需要进度事件?因为请求不是瞬间完成的,尤其数据量大时要知道“开始了、下载中、成功了、失败了、彻底结束”。怎么想?按生命周期记。怎么做?loadstart 表示开始请求;progress 在接收响应过程中可能触发多次,事件对象里有 loaded 和 total;load 表示响应成功;error 表示请求发不出去的网络错误;loadend 最后触发。

js
xhr.onprogress = e => console.log(e.loaded, e.total);
xhr.onloadend = () => console.log('响应彻底结束');

易错点:404、500 也能收到响应,仍会触发 load,不是 error;error 多用于网络错误、跨域被浏览器拦截等“拿不到响应”的情况。

同步和异步:异步的是整个过程 ​

为什么要强调异步?因为请求响应需要时间,同步会阻塞后续代码。怎么想?异步不是“只有回调函数是异步”,而是从开始请求到响应彻底结束的整个过程是异步的。怎么做?保持 open 默认异步,不要在业务里写同步请求。易错点:把 open 第三参数写成 false 会卡住页面;理解事件顺序时,loadend 通常在最后触发。

转写稿后段的跨域提醒 ​

转写稿还演示了同源策略、CORS 和 JSONP:同源要求协议、域名、端口一致;请求能到后端,但浏览器可能拒绝响应;CORS 靠后端设置 Access-Control-Allow-Origin;JSONP 利用 script 标签跨域能力,只支持 GET,需要前端提前定义函数、后端返回调用该函数的 JS 代码,并在用完后移除动态 script。这些属于本节流程之外的跨域专题,先记住边界:AJAX 基本流程管“怎么发、怎么收”,跨域管“浏览器允不允许收”。

先理解:跨域到底在拦什么 ​

为什么需要:Ajax 请求受同源策略限制。所谓同源,是浏览器要求“Ajax 代码所在页面的 URL”和“Ajax 请求的 URL”在协议、域名、端口三部分保持一致。只要有一项不同,就可能跨域。
怎么想:先别把“跨域”理解成请求发不出去。转写稿里的调试很关键:页面用 localhost 打开,Ajax 请求 127.0.0.1,浏览器报 CORS 错误,onload 不触发,onerror 触发;但后端仍然收到了请求并作出了响应。也就是说,请求能到服务器,服务器也愿意响应,是浏览器在拿到响应后按同源策略拒绝把响应交给前端。
怎么做:写 Ajax 时先检查两个 URL:

js
// 页面地址:http://localhost:8080/config01
// 请求地址:http://127.0.0.1:8080/getData01
const xhr = new XMLHttpRequest();
xhr.onload = function () {
  console.log(xhr.response);
};
xhr.open('GET', 'http://127.0.0.1:8080/getData01');
xhr.send();

易错点:localhost 和 127.0.0.1 虽然常指向本机,但在同源策略里不算同一个域名;协议、端口也一样要逐项比对。不要把“后端能收到请求”误认为“跨域已经成功”。

CORS:让服务器给浏览器发“通行证” ​

为什么需要:同源策略由浏览器执行,前端不能单方面解除。CORS(跨域资源共享)的思路是:服务器在响应头里明确告诉浏览器“这个来源允许访问”。
怎么想:浏览器拒绝的是响应;那就要让响应带上许可信息。服务器设置 Access-Control-Allow-Origin,浏览器看到合法响应头后才放行。
怎么做:后端设置响应头,例如允许单个来源:

js
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080');

也可以允许所有来源:

js
res.setHeader('Access-Control-Allow-Origin', '*');

前端仍然正常发 Ajax:

js
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://127.0.0.1:8080/getData01');
xhr.onload = function () {
  console.log(xhr.response);
};
xhr.send();

调试时可在浏览器“网络”面板查看响应头,确认是否出现 Access-Control-Allow-Origin。
易错点:Access-Control-Allow-Origin 不能写成多个域名用逗号隔开;转写稿中浏览器明确提示该值无效。若要允许多个来源,需要后端根据请求头 Origin 判断后再返回对应值。跨域请求默认不携带 Cookie;若要携带 Cookie,前后端都要额外配置,不能只加一个响应头。CORS 主要是后端工作,前端不要试图靠改 Ajax 代码“解决”。

JSONP:利用 script 标签的跨域能力 ​

为什么需要:CORS 需要后端配合;JSONP 是另一种历史方案,只支持 GET。它利用 script、img、link 等标签天生可加载跨域资源的特性,其中 script 加载到的内容会被当作 JS 执行。
怎么想:让后端返回一段“调用前端函数的 JS 代码”,前端提前定义好函数,脚本一加载就执行,从而把数据带进前端。
怎么做:前端先定义函数,再动态创建 script:

js
function parseData(data) {
  console.log(data);
}

const script = document.createElement('script');
script.src = 'http://127.0.0.1:8080/getData02?cb=parseData';
document.body.appendChild(script);
document.body.removeChild(script);

后端返回的内容应类似:

js
parseData([{ name: '张三', age: 18 }]);

转写稿中调试过:后端返回 alert(250) 时,前端会直接执行弹窗;返回 parseData() 时,如果函数未提前定义,会报 parseData is not defined。函数名可通过查询参数传给后端,如 ?cb=parseData,由后端拼进响应体。
易错点:JSONP 不是 Ajax 请求方式,也不是 XHR;它只支持 GET。函数必须提前定义,且后端返回的必须是合法可执行的 JS。传对象数据时要转成 JSON 字符串再拼进函数调用,不能直接把变量名写进去。script 添加到 body 后请求就会发出,随后可立即移除,避免页面元素越积越多。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

1. 原生 Ajax 基本使用流程(含响应处理、超时与进度事件) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Ajax 基本使用流程</title>
  <style>
    #box { width: 600px; min-height: 200px; border: 2px dashed #999; padding: 20px; margin-top: 20px; }
  </style>
</head>
<body>
  <button id="btn">发送请求</button>
  <div id="box"></div>

  <script>
    const btn = document.querySelector('#btn');
    const box = document.querySelector('#box');

    // 1. 创建 XHR 对象
    const xhr = new XMLHttpRequest();

    // 提前设置响应体格式,onload 里的 response 就会自动变成对象/数组
    xhr.responseType = 'json';
    // 超时时间:5 秒
    xhr.timeout = 5000;

    // 2. 监听事件(在 send 之前监听即可,open 前后都行)
    xhr.onloadstart = function () {
      console.log('开始请求,readyState =', xhr.readyState); // 1
    };

    // 接收响应过程中触发,内容多时可能触发多次
    xhr.onprogress = function (e) {
      console.log('已下载字节数:', e.loaded, '总字节数:', e.total);
    };

    // 响应成功(只要能收到响应,404/500 也算触发)
    xhr.onload = function () {
      console.log(xhr.status, xhr.statusText);
      box.innerHTML = xhr.response.map(item => item.name + ':' + item.age).join('<br>');
    };

    // 请求失败:一般指网络错误,请求根本发不出去
    xhr.onerror = function () {
      console.log('请求失败');
    };

    // 超时
    xhr.ontimeout = function () {
      console.log('响应超时');
    };

    // 响应彻底结束,最后触发
    xhr.onloadend = function () {
      console.log('响应彻底结束');
    };

    btn.onclick = function () {
      // 3. 初始化请求(只做基本设置,不会真正发请求)
      xhr.open('get', 'http://127.0.0.1:8080/getData');
      // 4. 正式发送请求,GET 不需要请求体,不传参
      xhr.send();
    };
  </script>
</body>
</html>

关键行与易错点

  • new XMLHttpRequest() 是第一步,后面所有事件都挂在这个对象上。
  • open() 只是初始化,不发送请求;send() 才真正发送。事件在 open 前后监听都没区别,但必须在 send 之前监听,否则网速太快可能响应已结束、事件没加上。
  • responseType = 'json' 设置后,xhr.response 直接是对象/数组;不设置则只能用 xhr.responseText 拿字符串。
  • onload 是“响应成功”,只要收到响应就触发(包括 404/500);onerror 是“请求失败”(网络错误),两者别混。
  • 触发顺序:loadstart → progress(可能多次)→ load → loadend。
  • 设置请求头必须在 open() 之后、send() 之前,否则设置不上。

2. JSONP 实现跨域(动态创建 script 标签) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>JSONP</title>
  <style>
    #box { width: 600px; min-height: 200px; border: 2px dashed #999; padding: 20px; margin-top: 20px; }
  </style>
</head>
<body>
  <button id="btn">发送请求</button>
  <div id="box"></div>

  <script>
    const btn = document.querySelector('#btn');
    const box = document.querySelector('#box');

    // 提前定义好函数,函数名要和传给后端的 cb 值保持一致
    function parseData(data) {
      box.innerHTML = data
        .map(item => item.name + ':' + item.age + ',' + item.address)
        .join('<br>');
    }

    btn.onclick = function () {
      // 1. 动态创建 script 标签
      const script = document.createElement('script');

      // 2. 设置 src,通过 cb 把函数名告诉后端
      script.src = 'http://127.0.0.1:8080/getData02?cb=parseData';

      // 3. 添加到 body 中,请求才会真正发出
      document.body.appendChild(script);

      // 4. 立即移除,避免页面结构越来越臃肿(请求已经发出,不受影响)
      document.body.removeChild(script);
    };
  </script>
</body>
</html>

关键行与易错点

  • 原理:script 标签天生具备跨域能力,不受同源策略限制;后端把“调用函数的代码”作为响应体返回,浏览器拿到后直接当 JS 执行。
  • 前端必须提前定义好函数,后端返回的 parseData([...]) 执行时才能找到它,否则报 is not defined。
  • 函数名通过 ?cb=xxx 传给后端,后端取出后拼进响应体,这样双方不用写死函数名。
  • appendChild 之后请求才发出,但可以立即 removeChild:代码同步顺序执行,添加时请求已发出,移除不影响。
  • 后端返回的必须是合法可执行的 JS 代码;如果把对象直接拼进字符串(不加引号),前端会把它当变量,报 xxx is not defined,所以要 JSON.stringify。
  • JSONP 只支持 GET,这是它最大的缺点。

3. CORS 解决跨域(Node + Express 后端设置响应头) ​

js
const express = require('express');
const app = express();

// 统一设置跨域响应头(必须在响应之前设置)
app.use((req, res, next) => {
  // 允许指定源跨域访问,注意写的是「打开页面」的那个源
  res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:8080');
  // 允许跨域携带 cookie(跨域默认不携带)
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  // 跨域时前端默认只能拿到少数几个响应头,需要额外暴露
  res.setHeader('Access-Control-Expose-Headers', 'X-My-Header');
  res.setHeader('X-My-Header', 'hello');
  next();
});

// 普通接口
app.get('/getData', (req, res) => {
  console.log('接收到请求');
  res.send([
    { name: '张三', age: 18, address: '北京' },
    { name: '李四', age: 20, address: '上海' }
  ]);
});

// JSONP 接口:从 URL 里取出函数名,拼成「调用函数」的代码返回
app.get('/getData02', (req, res) => {
  const cb = req.query.cb;
  const data = [{ name: '张三', age: 18, address: '北京' }];
  res.send(`${cb}(${JSON.stringify(data)})`);
});

app.listen(8080, () => {
  console.log('http://127.0.0.1:8080');
});

关键行与易错点

  • 同源策略是浏览器的行为:请求能发到服务器、服务器也会正常响应,是浏览器拒绝把响应交给页面,所以只能由后端加响应头来解决。
  • Access-Control-Allow-Origin 写的是打开页面那个 URL 的源(协议 + 域名 + 端口),不是请求地址的源,这一点最容易搞混。
  • localhost 和 127.0.0.1 虽然指向同一台机器,但不同源,一样会被拦截。
  • 该响应头不能写多个域名(逗号分隔也无效,浏览器会报值无效);需要多个时,后端读取请求头里的 Origin,判断它在不在白名单数组里,再动态设置成对应的那一个。
  • 写 * 表示允许所有源,但此时不能与 Access-Control-Allow-Credentials: true 同时使用,必须写具体域名。
  • 跨域默认不携带 cookie;想携带要前后端配合:前端 xhr.withCredentials = true,后端 Allow-Credentials 为 true 且 Allow-Origin 不能是 *,cookie 还要设置 path、max-age。

常见错误 ​

1. 请求头设了却不生效,后端拿不到数据

  • 现象:POST 提交 JSON,后端 req.body 为空;Network 里看不到你设置的那个请求头。
  • 原因:setRequestHeader 写在了 open 之前或 send 之后;或请求头的值与 send 里实际的数据格式不匹配。
  • 修法:顺序固定为 open → setRequestHeader → send;请求头必须和请求体格式一一对应(JSON 字符串配 application/json,k=v&k=v 配 application/x-www-form-urlencoded);传 FormData 时不要手动设 Content-Type,boundary 得由浏览器自己生成。

2. send 里直接塞对象

  • 现象:后端收到 "[object Object]",或干脆是空。
  • 原因:send 只接受字符串 / FormData / Blob 这类,普通对象会被隐式转成字符串。
  • 修法:先 JSON.stringify,或用 FormData、URLSearchParams 组装。

3. 响应读出来是空的

  • 现象:设了 responseType = 'json' 之后读 responseText 拿到空串,甚至抛 InvalidStateError。
  • 原因:responseType 为 json 时 responseText 不可用,数据在 response 上。
  • 修法:统一读 xhr.response;进回调先判 xhr.status,再取数据。

4. 偶发拿不到响应,刷新几次又好了

  • 现象:本地调试时回调不执行,重试一次就正常。
  • 原因:事件监听写在了 send 之后,响应太快(缓存命中/本机服务),回调还没挂上请求就结束了。
  • 修法:监听一律前置到 send 之前;open 前后无所谓,send 之后有风险。

5. 报 CORS 错误,但 Network 里明明有 200 和响应体

  • 现象:控制台红字 CORS policy,load 不触发、error 触发,Network 面板能看到完整响应。
  • 原因:请求发出去了、后端也响应了,是浏览器按同源策略把响应拦下了;服务端没给 Access-Control-Allow-Origin。
  • 修法:前端改不了——让后端加响应头(开发期也可用代理转发)。注意该头只能写一个源,多源要靠后端读 Origin 请求头动态匹配;写 * 时不能配合凭证。

6. 跨域带不上 Cookie

  • 现象:跨域接口一直返回未登录。
  • 原因:前端没开 withCredentials,或后端 Allow-Origin 写了 * / 没开 Allow-Credentials。
  • 修法:xhr.withCredentials = true;后端 Access-Control-Allow-Credentials: true,且 Allow-Origin 必须是具体源,不能是 *。

7. JSONP 报 xxx is not defined

  • 现象:控制台提示回调函数未定义。
  • 原因:script 插入页面就开始请求并执行,而回调函数定义在它后面;或 src 上的 cb 参数值与函数名对不上。
  • 修法:先定义全局函数,再创建并插入 script;cb 的值必须与函数名完全一致(这个名字由后端约定)。

8. JSONP 的 script 标签越积越多 / 想用 POST

  • 现象:每点一次按钮页面里多一个 script;改成 POST 后毫无反应。
  • 原因:插入后没清理;JSONP 依赖 script 标签,天生只支持 GET。
  • 修法:插入后立即 remove(请求已发出,移除不影响);需要 POST 或要读响应头/状态码,就换 CORS。

9. 同步请求把页面卡死

  • 现象:open 第三个参数传 false,页面冻结、控制台警告。
  • 原因:主线程被阻塞。
  • 修法:用异步,把后续逻辑放进 load 回调(或 Promise 化)。

练习 ​

1. 封装 request(config) 用原生 XHR 封装一个返回 Promise 的函数,支持 method / url / data / headers / timeout;GET 走 URL 参数、POST 走请求体并自动匹配 Content-Type;超时与网络错误分别 reject 出不同信息。 验收:把地址改成不存在的端口,能拿到"网络错误"分支而不是一直 pending。

2. 进度事件可视化 按钮点击后请求数据并渲染成列表,同时监听 loadstart / progress / load / error / loadend,把触发顺序和 progress 的 loaded、total、百分比实时打到页面上。 验收:progress 至少触发一次并算出百分比;能说清 load 与 loadend 谁先谁后。

3. 亲手复现一次跨域 页面用 localhost 打开,XHR 请求 127.0.0.1。观察 Network 与 Console 后,在不改前端一行代码的前提下让后端加响应头解决。 要求写出三件事:请求有没有到达后端、浏览器在哪一步拦截、改的是哪一方的哪个头。

4. JSONP 拉一次数据 动态创建 script、用 cb 参数约定函数名、拿到数据渲染,并在插入后移除标签。 验收:连点 5 次按钮,Elements 里 script 数量不增长,数据正确渲染。

5. 两种方案对比 同一份接口分别用 CORS 和 JSONP 各请求一次,用表格列出差异:请求方式、能否读状态码/响应头、错误处理能力、需要后端做什么、适用场景。

原视频对照 ​

#视频时长
1总结19:18
2同源策略19:54
3解决跨域的方案04:51
4CORS实现跨域22:52
5jsonp实现原理11:10
6jsonp使用演示0113:13
7jsonp使用演示0218:05

小结 ​

  1. 排查 Ajax 问题的顺序是固定的:先看 Network(请求发没发出去、响应回没回来),再看 Console(浏览器拦没拦)。两步就能判断是"发不出去"还是"收到了但不给你"。
  2. 跨域不是前端能"修"的。浏览器拦的是响应,真正的开关在后端响应头;前端能做的只有换方案(代理)或换协议(JSONP)。
  3. 唯一不能靠"一般没事"糊过去的时序要求有两个:监听前置到 send 之前,setRequestHeader 夹在 open 与 send 之间。
  4. 请求头和请求体是一对。格式对不上是最难查的一类 bug——报错出现在后端,错误却在前端。
  5. responseType 一旦设成 json,就只认 response,别再碰 responseText;取数据前先判 status。
  6. JSONP 是历史产物:只在"对方后端改不动且只需要 GET"时用,能用 CORS 就别用它。
  7. 同步 XHR 在主线程上不可接受;异步 + Promise 化是后续所有封装的起点。
  8. 报错信息可以直接丢给 AI 问,但前提是你能读懂它说的"同源""响应头""凭证"这些词——底层逻辑不通,工具帮不了你。