网络请求与远程资源篇

218 阅读8分钟

网络请求与远程资源篇

文章收录在仓库 study-together 中,持续更新前端内容,欢迎关注~

Ajax(Asynchronous JavaScript+XML,即异步 JavaScript 加 XML),目的:发送服务器请求额外数据而不刷新页面,从而实现更好的用户体验。XHR 对象的 API 被普遍认为比较难用,而 Fetch API 自从诞生以后就迅速成为了 XHR 更现代的替代标准。Fetch API 支持 Promise 和 服务线程( service worker ),已经成为及其强大的 Web 开发工具。

思考:axios 是用的哪个对象发的请求?

axios 浏览器端默认使用 xhr 发请求,node 环境使用 http 发请求,不过也支持使用 fetch,添加 adapter 参数即可

const {data} = axios.get(url, {
  adapter: 'fetch'
})

XMLHttPRequest 对象

使用 XHR

let xhr = new XMLHttpRequest();
// 只能访问同源 URL,也就是域名相同、端口相同、协议相同
xhr.open("get","example.php",true) // 接受三个参数,请求类型,请求url,请求是否是异步
xhr.setRequestHeader("MyHeaders","MyValue") // 自定义请求头字段 必须在 open 方法之后 send 方法之前调用
xhr.send(null) // send 方法的参数是请求体
// 监听请求状态的改变
xhr.onreaystatechange = function(){
  if(xhr.readyState === 4){
    if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304){
      // TODO
    }else{
      // 错误处理
    }
  }
}
// 取消异步请求,调用这个方法后,XHR 对象会停止触发事件,并组织访问这个对象上任何与响应相关的属性。
xhr.abort();
// 获取响应头字段
xhr.getResponseHeader("xxx");
xhr.getAllResponseHeaders();

xhr 的 readyState有如下五个值,分别代表不同的阶段:

  • 0:未初始化(Uninitialized)。尚未调用 open() 方法。
  • 1:已打开(Open)。已调用 open() 方法。
  • 2:已发送(Sen)。已调用 send() 方法。
  • 3:接受中(Receiving)。已经收到部分响应。
  • 4:完成(Compelete)。已经收到所有响应,可以使用了。
处理表单

需要把 Content-Type 头部设置为 application/x-www-formurlencoded;然后序列化 serialize 表单

xhr.setRequestHeader('Content-Type':'application/x-www.formurlencoded');
let form = document.getElementById("user-info");
xhr.send(serialize(form));

第二种方法是使用 XMLHttpRequest Level 2扩展的 FormData 对象

let data = new FormData();
data.append("name","Nicholas");
xhr.send(data);

并且 XMLHttpRequest Level 2还添加了 timeout 设置超时,超过指定时间后,会触发 ontimeout 事件处理程序,readyState 会变成4,因此也会触发 onreadystatechange 事件处理程序

xhr.timeout = 1000;
xhr.ontimeout = function(){
  // 超时
}

跨源资源共享

默认情况下,XHR 只能访问与发起请求的页面在同一个域内的资源。不过,跨源资源共享(CORS)定义了浏览器与服务器如何实现跨源通信。CORS 背后的基本思路是使用自定义的 HTTP 头部允许浏览器和服务器相互了解,以确定请求或响应应该成功还是失败。对于简单请求,发送请求是会添加一个额外的头部:Origin。Origin 包含发送请求的页面的源(协议、域名和端口),如:Origin:http://www.baidu.com。如果服务器决定响应请求,响应头需要添加:Access-Control-Allow-Origin,包含相同的源或者说资源是公开的包含“*”,那么就可以做到跨源资源共享。对于非简单请求,在真正请求发出之前需要先发一种叫预检请求的服务器验证机制,允许自定义头部、除 GET、POST 之外的方法,以及不同请求体内容类型。

什么是简单请求

满足所有下述条件,即为简单请求:

  • 使用以下方法之一

    • GET
    • POST
    • HEAD
  • 允许设置的请求头字段

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • Range(只允许简单的范围标头值,如:bytes=127-255)
  • Content-Type 仅限于下列三者之一

    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • 如果请求是 XMLHttpRequest 对象发出的,在返回的 XMLHttpRequest.upload 对象属性上没有注册任何的事件监听器;即没有调用 xor.upload.addEventListener()

  • 请求中没有使用 ReadableStream 对象

预检请求

预检请求使用 OPTIONS 方法,可以避免跨域请求对服务器的用户数据产生预期之外的影响。请求头会包含以下字段:

  • Origin
  • Accept-Control-Request-Method
  • Accept-Control-Request-Headers

响应中需要包含以下字段:

  • Accept-Control-Allow-Headers
  • Accept-Control-Allow-Methods
  • Accept-Control-Allow-Origin
  • Accept-Control-Max-Age

Accept-Control-Max-Age 定义该预检请求可供缓存的时间,单位为秒,在这时间之内浏览器不会为同一请求再次发送预检请求。不过,浏览器自身会维护一个最大值,Accept-Control-Max-Age超过最大值,则视为无效。

附带凭证的请求

默认情况下,跨源请求不提供凭证(一般是 cookie)。可以通过在请求头中将 withCredentials 属性设置为 true 表示请求会发送凭证。如果服务器运行发送凭证的请求,那么可以在响应中包含如何 HTTP 头部:

Access-Control-Allow-Credentials:true

如果发送了凭据请求而服务器返回的响应中没有这个头部,则浏览器不会把响应交给请求的发送者。需要注意的是在响应附带身份凭证的请求时,服务器不能将 Access-Control-Allow-Origin、Accept-Control-Allow-Methods、Accept-Control-Allow-Origin 设置为" * "。

Fetch API

Fetch API 能够执行 XMLHttpRequest 对象的所有任务,但更容易使用,接口更现代化,能够在 Web 工作线程(worker)等现代 Web 工具中使用。 XMLHttpRequest 可以选择异步,而 Fetch API 则必须是异步。

用法

Fetch 的第一个参数是请求的 url,第二个参数是一个 init 对象, init 对象定义了请求的全部配置项如:body、headers、method 等。fetch 调用之后返回一个 Promise,如果服务器没有响应而导致浏览器超时,Promise 会被 reject。反之 Promise 会 resolve,并且得到一个 response 对象,response 对象呈现了一次请求的响应数据,并暴露了一些 api 以便我们更好的处理响应数据,如:response.text、response.json 等。

fetch("/url", {
  method: "POST",
  body: JSON.stringify({
    foo: "bar",
  }),
  headers: new Headers({
    "Content-Type": "application/json",
  }),
}).then(
  (response) => {
    // 可以根据 response 的状态对响应做处理
    if (response.status === 200) {
      response.json().then((data) => {
        console.log(data);
      });
    } else if (response.status === 404) {
      // something to do
    }
  },
  (err) => {
    console.log(err); // 服务器没有响应而导致浏览器超时
  }
);
​

中断请求

Fetch API 支持通过 AbortController/AbortSignal 对请求中断。调用 AbortController.abort() 会中断所有网络传输,特别适合希望停止传输大型负载的情况。中断进行中的 fetch() 请求会导致错误的拒绝。

let abortController = new AbortController();
fetch('example/api', { signal: abortController.signal })
  .catch(()=> console.log('aborted'));
​
// 10 毫秒后中断请求
setTimeout(()=> abortController.abort());
​
// 已中断

Beacon API

为了把尽量多的页面信息传到服务器,很多分析工具需要在页面生命周期中尽量晚的时候向服务器发送遥测或分析数据。因此,理想的情况下是通过浏览器的 unload 事件发送网络请求。但是 unload 事件对浏览器意味着没有理由再发送结果未知的网络请求(因为页面要销毁了),所以在 unload 事件处理程序中创建的任何异步请求都会被浏览器取消。Beacon API 就是为了解决这个问题而诞生的,这个 API 给 navigator 对象增加了一个 sendBeacon 方法,接受一个 URL 和 一个数据有效载荷参数,并会发送一个 POST 请求。可选的数据有效载荷参数有 ArrayBufferView、Blob、DOMString、FormData 实例。

navigator.sendBeacon('https://example.com/analytics-reporting-url','{foo:"bar"}');

这个方法的特性:

  • sendBeacon() 并不是只能在页面生命周期末尾使用,而是任何时候都可以使用。
  • 调用 sendBeacon() 后,浏览器会把请求添加到一个内部的请求队列。浏览器会主动地发送队列中的请求。
  • 浏览器保证在原始页面已经关闭的情况下也会发送请求。
  • 状态码、超时和其他网络原因造成的失败完全是不透明的,不能通过编程方式处理。
  • Beacon 请求会携带调用 sendBeacon() 时所有相关的 cookie。

Web Socket

Web Socket 的目标是通过一个长时连接实现与服务器全双工、双向的通信。创建 Web Socket 之后,一个 HTTP 请求会发送到服务器以初始化连接。服务器响应后,连接使用 HTTP 的 Upgrade 头部从 HTTP 协议切换到 Web Sokcet 协议。这意味着 Web Socket 不能通过标准 HTTP 服务器实现,而必须使用支持该协议的专有服务器。Web Socket 使用的 URL 方法有变化,不能使用 http:// 或者 https://,要使用 ws:// 或者 wss://,前者是不安全的连接,后者是安全连接。

Web Socket 使用的是自定义协议而非 HTTP 协议,好处是客户端与服务端可以发送非常少的数据,不必担心 HTTP 那样字节级的开销,这一点对移动端非常友好。缺点是定义协议的时间比定义 Javascript API 要长。

基本用法

要创建一个新的 Web Socket,就要实例化一个 WebSocket 对象并传入提供连接的 URL;而且同源策略不适用于 Web Socket,因此可以打开任意站点的连接,至于是否与来自特定源的页面通信,则完全取决于服务器。(握手阶段就可以确定请求来自哪里)。接收数据是通过 Web Socket 实例的 message 事件完成,可以通过event.data 属性访问到有效载荷。发送数据通过 send 方法发送。

let socket = new WebSocket("ws://www.example.com/server.php");
​
let stringData = "hello world";
let arrayBufferData = Unit8Array.from(['f','o','o']);
let blobData = new Blob(['f','o','o']);
​
socket.send(stringData);
socket.send(arrayBufferdata);
socket.send(blobData);
​
socket.onmessage = function(event){
  let data = event.data;
}