前端异步请求相关问题
Posted by Mars . Modified at
前端发送http请求,跨域、跨页面通信的几种方式梳理
Refer: 阮一峰CORS
一、发送异步Http请求的几种方式
1. 原生Ajax
- 实例化
XMLHttpRequest
对象; - 绑定xhr对象状态变化监听函数;
- 打开xhr对象;
- 发送http请求。
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if(xhr.readyState === 4){
if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304){
// do something
} else {
// fault. do something.
}
}
};
xhr.open('get', 'url', true); // true异步,false同步。
xhr.send(null); // 向服务器发送信息
1.1 原生Ajax的一些细节
- 如果传入相对URL,则是相对当前代码所在页面;
- 调用open()不会发送请求,只是为send()做好准备,send()执行后才发出请求;
- XHR对象有一个readyState属性,整个请求过程可能有五个值:
- 0:未初始化。尚未调用open();
- 1:已打开。已经调用open(),尚未调用send();
- 2:已发送。已经调用send(),尚未收到响应;
- 3:接收中。已经接收到部分响应;
- 4:完成。已经收到所有响应,可以使用。
- 收到响应前,可以调用xhr.abort()方法取消异步请求;
- 除了默认的http头部之外,如果想添加其他请求头,可以用xhr.setRequestHeader()方法,在发送请求send之前添加;
- 如果有查询字符串,需要用encodeURIComponent()函数编码后,附加在URL的后面,再发送GET请求;
- 可以为xhr对象设置timeout属性,用来表示它的最大响应时长。如果超时仍未响应,则中断请求:
- 超时会触发ontimeout事件;
- 超时后,readyState值仍为4。
- 请求过程中会触发一系列事件,从前到后分别是:
- loadstart: 接收响应的第一个字节时触发;
- progress: 接收响应期间,反复触发;
- progress事件对象中包含:lengthComputable、position、totalSize三个属性;
- lengthComputable:布尔值。代表接收的数据长度是否是可计算的;
- position:接收到的字节数;
- totalSize:总字节数。
- progress事件对象中包含:lengthComputable、position、totalSize三个属性;
- error、abort或load: load在接收响应完成时触发,error在出错条件下触发,abort在调用abort()终止请求时触发;
- loadend:代表通信完成,在上面三个事件之后触发。
- 通过CORS跨域发送ajax请求:
- 默认不会携带凭据信息,包括cookie信息、Http认证和客户端SSL证书;
- 如果想带有凭据信息,需要将xhr.withCredentials属性设为ture;
- 服务器响应需要带有头部:Access-Control-Allow-Credentials: true, 如果没有,则浏览器不会把响应交付给JS请求对象。
- 同步Ajax请求会阻塞页面,影响用户体验。一般除非是在页面生命周期末尾unload事件上发送请求,不使用同步Ajax请求(unload上执行的异步请求会被取消,因为页面即将销毁,浏览器认为没有必要再进行异步请求);
2. Fetch API
Fetch API是JavaScript原生的,基于Promise的标准请求API。(无需加载额外资源)
3. axios
基于Ajax和Promise封装的请求库。
二、跨域问题
1. 什么是同源策略?什么时候出现跨域问题?
在HTML标签(比如img,script等)和CSS中(url函数)使用url进行资源请求,浏览器默认不设任何限制,可以对任何资源url进行请求。
一般情况下,下列三种情况只能在同源条件下实现,非同源默认会报错(跨域错误):
- 读取Cookie、LocalStorage 和 IndexDB
- 获取DOM 元素
- Js发送AJAX请求
这是浏览器的一种基本的保护策略,叫做同源策略。
同源策略
协议、域名和端口号都相同的请求,叫做同源请求。JS代码内只能发出同源请求。
即便两个不同的域名指向同一个 ip 地址,也非同源。
2. 跨域AJAX请求的几种常见解决方式
2.1 跨域资源共享(CORS:Cross Origin Resource Sharing)
CORS是W3C标准,是跨域请求的根本解决方式。
实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
浏览器将请求分为两种:简单请求和非简单请求。
简单请求
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
简单请求和非简单请求,浏览器的发送流程如下图所示:
2.2 JSONP
JSONP是一种古老的广泛使用的跨域请求方式,好处是可以兼容很多老式的浏览器。
JSONP主要是利用了script标签内src属性请求不受跨域限制的特点。
JSONP的主要流程:
- 在页面引入或写好要执行的函数;
- 创建一个<script>标签或动态创建一个script元素,src属性为跨域请求的地址,地址的最后用?callback=funcName,向服务器标记函数方法名;
- 挂载这个script元素,发出请求;
- 服务器收到这个请求后,解析callback后的函数名funcName,然后返回一个funcName函数执行的代码字符串:
funcName(<data>)
,里面包含参数<data>
,就是服务器想要浏览器执行的操作; - 浏览器接收响应,作为代码执行。
let id = 0; // 让不同的jsonp请求回调函数名不同。
const jsonp = function(url, params) {
return new Promise((res, rej) => {
id += 1;
let callbackName = 'callback' + id;
let src = url + '?';
for (let k of Object.keys(params)) {
src += `${k}=${params[k]}&`;
}
src += `callback=${callbackName}`
let script = document.createElement('script');
script.src = src;
window[callbackName] = (data) => {
res(data);
document.documentElement.removeChild(script);
};
document.documentElement.append(script);
});
}
JSONP的局限性:
- 需要前后端协调配合;
- JSONP只能发
GET
请求。
2.3 WebSocket
WebSocket是一种协议,它不受同源策略限制。
只要服务器支持,使用WebSocket协议(ws:// 或 wss://)进行请求即可。
2.4 代理服务器
2.4.1 本地代理服务器
服务器间通信不受同源策略限制。
因此,可以在本地用node开启一个设置了CORS的服务器,用这个服务器转发请求到目标服务器。
2.4.2 Nginx代理
通过Nginx
开启一个正向代理服务器。
二、 跨窗口通信
同源策略规定:
如果我们有对另外一个窗口(例如,一个使用 window.open 创建的弹窗,或者一个窗口中的 iframe)的引用,并且该窗口是同源的,那么我们就具有对该窗口的全部访问权限。
否则,如果该窗口不是同源的,那么我们就无法访问该窗口中的内容:变量,文档,任何东西。唯一的例外是 location:我们可以修改它(进而重定向用户)。但是我们无法读取 location(因此,我们无法看到用户当前所处的位置,也就不会泄漏任何信息)。
非同源的窗口,还可以通过postMessage向其发送一条消息。这是对于非同源页面引用唯二的两个操作。
— 现代JS教程
对于与当前页不同源的页面引用,只能进行下面两个操作之一:
- 修改它的
location
(重定向); - 给它发送一个信息
postMessage(<message>)
。
要想让同一个二级域下的所有子域都被视作同源,需要在每个页面上添加以下代码:
// 为当前页面设置域(默认为当前页面URL的域名)
document.domain = 'site.com';
1. 同源跨窗口通信
1.1 Broadcast Channel API (广播频道)
Broadcast Channel API 可以实现同源下浏览器不同窗口,Tab页,frame或者 iframe 下的 浏览器上下文 (通常是同一个网站下不同的页面)之间的简单通讯。
// 1.连接到test_channel广播频道,如果还没有这个频道,这代表创建一个名叫test_channel的广播频道
const bc = new BroadcastChannel('test_channel');
// 2.向广播信道发送消息
bc.postMessage('This is a test message.');
// 3.其他所有链接到这个广播频道的页面,可以接收到一个message事件
bc.onmessage = (e)=>{
console.log(e)
}
// 4.断开频道连接
bc.close()
1.2 利用localStorage
同源的页面,可以访问同一个localStorage。
通过对localStorage的修改进行监听(storage事件),可以实现跨页面通信。
2. 非同源跨窗口通信
2.1 可以获取到目标窗口的引用时: postMessage
这种情况包括嵌入iframe,使用window.open打开这类情况。
跨窗口通信有以下三步:
- 获取到目标窗口的引用
win
,然后调用win.postMessage(message, targetOrigin)方法
这里: data是要发送的数据,targetOrigin是目标窗口的源。
也就是说,只有目标窗口在指定的源下(协议、域和端口),才能正常接收到消息。
这保护了目标页面的安全,因为发送方此时与目标页面不同源,因此不能知道目标页面现在实际的源,用户可以随时更改到任何源。这样设定保证了数据不会被发送到非目标源的页面。
- 在目标页面上,写入好window.onmessage事件,用于监听收取来自外部页面的message。
window.onmessage = (e) => {
// do someting with the message.
// 此时的e具有三个内部属性: ①data:数据本身;②origin:发送方的源; ③source:发送方窗口的引用(可以随时使用e.source.postMessage反向发送消息。);
}
2.2 a.xxx.com 与 b.xxx.com 通信:修改document.domain
两个页面位于同一个上级域名的不同子域名下,原本为非同源(因为域不同),通过分别设置document.domain = 'xxx.com'
,可让二者为同源。
这样通过a.xxx.com
的<iframe>
就可以获取到b.xxx.com
的全部内容了。