前端异步请求相关问题

Posted by Mars . Modified at

前端发送http请求,跨域、跨页面通信的几种方式梳理

Refer: 阮一峰CORS

一、发送异步Http请求几种方式;

二、跨域问题;

一、发送异步Http请求的几种方式

1. 原生Ajax

  1. 实例化XMLHttpRequest对象;
  2. 绑定xhr对象状态变化监听函数;
  3. 打开xhr对象;
  4. 发送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:总字节数。
    • 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

简单请求和非简单请求,浏览器的发送流程如下图所示:

CORS的简单请求和非简单请求

2.2 JSONP

JSONP是一种古老的广泛使用的跨域请求方式,好处是可以兼容很多老式的浏览器。

JSONP主要是利用了script标签内src属性请求不受跨域限制的特点

JSONP的主要流程:

  1. 在页面引入或写好要执行的函数;
  2. 创建一个<script>标签或动态创建一个script元素,src属性为跨域请求的地址,地址的最后用?callback=funcName,向服务器标记函数方法名;
  3. 挂载这个script元素,发出请求;
  4. 服务器收到这个请求后,解析callback后的函数名funcName,然后返回一个funcName函数执行的代码字符串: funcName(<data>),里面包含参数<data>,就是服务器想要浏览器执行的操作;
  5. 浏览器接收响应,作为代码执行。
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的局限性:

  1. 需要前后端协调配合;
  2. 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教程

对于与当前页不同源的页面引用,只能进行下面两个操作之一:

  1. 修改它的location(重定向);
  2. 给它发送一个信息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打开这类情况。

跨窗口通信有以下三步:

  1. 获取到目标窗口的引用win,然后调用win.postMessage(message, targetOrigin)方法

这里: data是要发送的数据,targetOrigin是目标窗口的源。

也就是说,只有目标窗口在指定的源下(协议、域和端口),才能正常接收到消息。

这保护了目标页面的安全,因为发送方此时与目标页面不同源,因此不能知道目标页面现在实际的源,用户可以随时更改到任何源。这样设定保证了数据不会被发送到非目标源的页面。

  1. 在目标页面上,写入好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的全部内容了。

Keywords: 异步请求 跨域
previousPost nextPost
已经有 1000000 个小伙伴看完了这篇推文。