主题
跨域(Day15 1/2)
本页对应 Day15_Ajax,共 7 段视频,约 1 小时 49 分。
学习目标
- 能够独立编写 XMLHttpRequest 的 GET 请求,按创建对象、监听事件、open 初始化、send 发送的流程完成请求,并使用 responseType 正确解析 JSON 响应。
- 能够依据协议、域名、端口三项判定同源关系,根据页面 URL 与 Ajax 请求 URL 判断是否跨域,并解释浏览器拦截响应的原因。
- 能够对比 CORS、JSONP、代理服务器三种跨域方案,说明各自适用场景及限制,例如 JSONP 仅支持 GET 请求。
- 能够配合服务端设置 Access-Control-Allow-Origin 等响应头完成 CORS 跨域,并通过浏览器 Network 面板验证请求结果与响应头。
- 能够实现 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. 原生 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 |
| 4 | CORS实现跨域 | 22:52 |
| 5 | jsonp实现原理 | 11:10 |
| 6 | jsonp使用演示01 | 13:13 |
| 7 | jsonp使用演示02 | 18:05 |
小结
- 排查 Ajax 问题的顺序是固定的:先看 Network(请求发没发出去、响应回没回来),再看 Console(浏览器拦没拦)。两步就能判断是"发不出去"还是"收到了但不给你"。
- 跨域不是前端能"修"的。浏览器拦的是响应,真正的开关在后端响应头;前端能做的只有换方案(代理)或换协议(JSONP)。
- 唯一不能靠"一般没事"糊过去的时序要求有两个:监听前置到
send之前,setRequestHeader夹在open与send之间。 - 请求头和请求体是一对。格式对不上是最难查的一类 bug——报错出现在后端,错误却在前端。
responseType一旦设成json,就只认response,别再碰responseText;取数据前先判status。- JSONP 是历史产物:只在"对方后端改不动且只需要 GET"时用,能用 CORS 就别用它。
- 同步 XHR 在主线程上不可接受;异步 + Promise 化是后续所有封装的起点。
- 报错信息可以直接丢给 AI 问,但前提是你能读懂它说的"同源""响应头""凭证"这些词——底层逻辑不通,工具帮不了你。