【重走JavaScript之高级程序设计】网络请求与远程资源

网络请求与远程资源

Asynchronous Javascript And XML

a(async异步)j(JavaScript)a(and)x(XML:与json同级语言格式)

ajax 作为前端必修课,页面无刷新获取数据。是一种创建动态网页(即前后端交互)的技术。虽然Ajax 这个名称中包含 XML,但实际上Ajax 通信与数据格式无关。

XHR对象的API被普遍认为比较难用,而 FetchApI 自从诞生以后就迅速成为了 XHR更现代的替代标准。Fetch API支持期约(promise)和服务线程(service worker ),已经成为极其强大的 Web 开发工具

1 XMLHttpRequest 对象

所有现代浏览器都通过 XMLHttpRequest 构造数原生支持 XHR 对象;

let xhr = new XMLHttpRequest();


1.1 XMLHttpRequest

1.1.1 XMLHttpRequest 属性
属性 描述
XMLHttpRequest.onreadystatechange 监控事件,当 xhr.readyState 属性发生改变触发
XMLHttpRequest.readyState xhr的状态0-4共五个请求状态码,请求响应过程的当前活动阶段
XMLHttpRequest.response 返回一个 ArrayBuffer、Blob、Document,或 DOMString,具体是哪种类型取决于 XMLHttpRequest.responseType 的值。其中包含整个响应实体(response entity body)。
XMLHttpRequest.responseText 返回一个 DOMString,该 DOMString 包含对请求的响应,如果请求未成功或尚未发送,则返回 null。
XMLHttpRequest.responseURL 返回经过序列化(serialized)的响应 URL,如果该 URL 为空,则返回空字符串。
XMLHttpRequest.responseXML 返回一个 Document,其中包含该请求的响应,如果请求未成功、尚未发送或是不能被解析为 XML 或 HTML,则返回 null。
XMLHttpRequest.responseType 指定响应体格式
XMLHttpRequest.status 响应的http状态
XMLHttpRequest.statusText 返回一个 DOMString,其中包含 HTTP 服务器返回的响应状态。与 XMLHTTPRequest.status 不同的是,它包含完整的响应状态文本(例如,"200 OK")。
XMLHttpRequest.timeout 一个无符号长整型(unsigned long)数字,表示该请求的最大请求时间(毫秒),若超出该时间,请求会自动终止。
XMLHttpRequest.upload XMLHttpRequestUpload,代表上传进度。
XMLHttpRequest.withCredentials 一个布尔值 (en-US),用来指定跨域 Access-Control 请求是否应当带有授权信息,如 cookie 或授权 header 头。

XMLHttpRequest 对象的 readyState 属性,表示请求响应过程的当前活动阶段:

  • 0: 未初始化(Uninitialized)。创建了XMLHttpRequest对象,但未调用open()方法
  • 1: 已打开(Open)。已经调用open()方法,但未调用send()方法
  • 2: 已发送(Sent)。已经调用send()方法,但未接收到响应
  • 3: 接收中(Receiving)。已经接收到部分响应数据
  • 4: 完成(Complete)。已经接收到全部响应数据,并且可以在客户端使用

1.1.2 XMLHttpRequest 事件处理器

除了onreadystatechange,大部分浏览器还实现了以下事件处理器。除了可以设置 on* 属性外,也提供标准的监听器 addEventListener() API 来监听XMLHttpRequest 事件。除了可以设置 on* 属性外,也提供标准的监听器 addEventListener() API 来监听XMLHttpRequest 事件。

事件处理器 描述
XMLHttpRequest.onprogress 当请求传输数据时,触发该事件。可以通过监听此事件来获取请求的进度信息。
XMLHttpRequest.onload 当请求成功完成时,触发该事件。此事件只会在请求成功(如HTTP状态码为200)时触发。
XMLHttpRequest.onerror 当请求发生错误时,触发该事件。这包括网络错误、服务器错误等。
XMLHttpRequest.onabort 当请求被中止时,触发该事件。这通常是因为用户取消了请求或由于其他原因请求被中止。

1.1.3 XMLHttpRequest 方法
属性 描述
XMLHttpRequest.open(method, url, async?, user?, password?) 打开要发送请求的地址,参数:请求方式、请求的url地址、请求是否异步的布尔值(默认true)
XMLHttpRequest.send(requsetBody?) 发送请求(体)
XMLHttpRequest.setRequestHeader(key, value) 设置请求头
XMLHttpRequest.getResponseHeader(key) 获取响应头
XMLHttpRequest.abort() 请求发出,立刻终止

1.1.4 XMLHttpRequest 使用

只能访问同源 URL,也就是域名相同、端口相同、协议相同。如果请求的URL与发送请求的页面在任何方面有所不同,则会抛出安全错误。

// 打开要发送地址,演示用(实际开发中勿用同步请求)
xhr.open("get", "example.txt", false);
// 发送请求
xhr.send(null);

// 收到响应后,XHR对象的属性responseText、responseXML、status和statusText会被填充。

// 收到响应后,第一步要检查status 属性以确保响应成功返回。
// 一般来说,HTTP 状态码为 2x 表示成功。此时,responseText 或responsexM(如果内容类型正确) 属性中会有内容。
// 如果 HTTP状态码是304,则表示资源未修改过,是从浏览器缓存中直接拿取的。当然这也意味着响应有效。为确保收到正确的响应,应该检查这些状态,如下所示:
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
  alert(xhr.responseText);
} else {
  alert("Request was unsuccessful: " + xhr.status);
}
// 初始化 XMLHttpRequest 对象
let xhr = new XMLHttpRequest();
// 以下代码使用 DOMLeve0风格为XHR对象添加了事件处理程序,因为并不是所有浏览器都支持DOM Level 2风格。
// 与其他事件处理程序不同,onreadystatechange 事件处理程序不会收到event对象。
xhr.onreadystatechange = function () {
  // XHR对象有一个 readystate 属性,每次readyState 从一个值变成另一个值,都会触发readystatechange事件。
  // 一般来说,我们唯一关心的 readystate 值是 4.表示数据已就绪。
  if (xhr.readyState == 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
      alert(xhr.responseText);
    } else {
      alert("Request was unsuccessful: " + xhr.status);
    }
  }
};

// 为保证跨浏览器兼容,onreadystatechange 事件处理程应该在调用 open()之前赋值。
xhr.open("get", "example.txt", true);
xhr.send(null);
// 在收到响应之前如果想取消异步请求,可以调用 abort()方法:
// 调用这个方法后,XHR 对象会停止触发事件,并阻止访问这个对象上任何与响应相关的属性。中断请求后,应该取消对 XHR对象的引用。
// 由于内存问题,不推荐重用 XHR对象。
xhr.abort();

XMLHttpRequest.open(method, url, async?, user?, password?)

  • method: 要使用的 HTTP 方法,比如 GET、POST、PUT、DELETE、等。对于非 HTTP(S) URL 被忽略。
  • url: 一个 DOMString 表示要向其发送请求的 URL。
  • async?: 一个可选的布尔参数,表示是否异步执行操作,默认为 true。如果值为 false,send() 方法直到收到答复前不会返回。如果 true,已完成事务的通知可供事件监听器使用。如果 multipart 属性为 true 则这个必须为 true,否则将引发异常。
  • user?: 可选的用户名用于认证用途;默认为 null。
  • password?: 可选的密码用于认证用途,默认为 null。

xhr.open() 的第三个参数为true代表异步请求,此方法会在发送会立即返回。如果第三个参数为false代表同步请求,则此方法直到响应到达后才会返回。
XMLHttpRequest.send(requsetBody?) 方法接受一个可选的参数,其作为请求主体;如果请求方法是 GET 或者 HEAD,则应将请求主体设置为 null。requsetBody可以为以下

  • 可以为 Document, 在这种情况下,它在发送之前被序列化。
  • 可以是 Blob, BufferSource, FormData, URLSearchParams, 或者 USVString 对象。
  • null,默认为null

1.2 HTTP 头部

每个 HTTP 请求和响应都会携带一些头部字段,这些字段可能对开发者有用。XHR 对象会通过-些方法暴露与请求和响应相关的头部字段。
默认情况下,XHR请求会发送以下头部字段:

HTTP头部字段 描述
Accept 浏览器可以处理的内容类型。
Accept-Charset 浏览器可以显示的字符集。
Accept-Encoding 浏览器可以处理的压缩编码类型。
Accept-Language 浏览器使用的语言。
Connection 浏览器与服务器的连接类型
Cookie 页面中设置的 Cookie。
Host 发送请求的页面所在的域。
Referer 发送请求的页面的 URI。注意,这个字段在 HTTP 规范中就拼错了,所以考虑到兼容性也必须将错就错。(正确的拼写应该是 Referrer。)
user-Agent 浏览器的用户代理字符串。
let xhr = new XMLHttpRequest();

xhr.onreadystatechange = function () {
  if (xhr.readyState == 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
      alert(xhr.responseText);
    } else {
      alert("Request was unsuccessful: " + xhr.status);
    }
  }
};

xhr.open("get", "example.php", true);
// 如果需要发送额外的请求头部,可以使用 setRequestHeader()方法。这个方法接收两个参数:头部字段的名称和值。
// 必须在 open ()之后、send()之前调用 setRequestHeader ()
// 自定义头部一定要区别于浏览器正常发送的头部
// 否则可能影响服务器正常响应。有些浏览器允许重写默认头部,有些浏览器则不允许。
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);
// 可以使用 getResponseHeader()方法从 XHR 对象获取响应头部,只要传人要获取头部的名称即可。
let myHeader = xhr.getResponseHeader("MyHeader");
// 如果想取得所有响应头部,可以使用 getallResponseHeaders()方法,这个方法会返回包含所有响应头部的字符串。
let allHeaders = xhr.getAllResponseHeaders();

// 通过解析以下头部字段的输出,就可以知道服务器发送的所有头部,而不需要单独去检查了。
// Date: Sun,14 Nov 2004 18:04:03 GMT
// Server: Apache/1.3.29 (Unix)
// Vary: Accept
// X-Powered-By: HP/4.3.8
// Connection: close
// Content-Type: text/html; charset=iso-8859-1

1.3 GET请求

最常用的请求方法是 GET 请求,用于向服务器查询某些信息。必要时,需要在 GET 请求的 URL后面添加查询字符串参数。对 XHR 而言,查询字符串必须正确编码后添加到 URL 后面,然后再传给 open() 方法。

查询字符串中的每个名和值都必须使用 encodeURIcomponent() 编码,所有名/值对必须以和号(&)分隔,如下面的例子所示:

xhr.open("get","example.php?name1=valuel&name2=value2", true);

可以使用以下函数将查询字符串参数添加到现有的 URL 末尾:

/**
 * @description: 将查询字符串参数添加到现有的 URL 末尾。
 * @param {string} url 查询字符串的 URL
 * @param {string} name 查询参数
 * @param {string} value 参数值
 * @return {*}
 */
function addURLParam(url, name, value) {
  // 检查 URL中是否已经包含问号(以确定是否已经存在其他参数)。如果没有则加上一个问号;否则就加上一个和号。
  url += url.indexof("?") == -1 ? "?" : "&";
  // 分别对参数名和参数值进行编码,并添加到 URL 末尾
  url += encodeURIComponent(name) + "_" + encodeURIComponent(value);
  // 返回更新后的 URL。
  return url;
}
// 这里使用adduRLParam()函数可以保证通过 XHR 发送请求的 URL 格式正确
let url = "example.php";

// 添加参数
url = addURLParam(url, "name", "Nicholas");
url = addURLParam(url, "book", "Professional JavaScript");

// 初始化请求
xhr.open("get", url, false);

1.4 POST请求

第二个最常用的请求是 POST 请求,用于向服务器发送应该保存的数据。每个POST 请求都应该在请求体中携带提交的数据,而GET请求则不然。POST 请求的请求体可以包含非常多的数据,而且数据可以是任意格式。要初始化POST 请求, open() 方法的第一个参数要传"post",比如:

xhr.open("post","example.php", true);

function submitData() {
  let xhr = new XMLHttpRequest();

  xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
        alert(xhr.responseText);
      } else {
        alert("Request was unsuccessful: " + xhr.status);
      }
    }
  };

  xhr.open("post", "postexample.php", true);
  xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  // 来自D为"user-info"的表单中的数据被序列化之后发送给了服务器。
  let form = document.getelementById("user-info");
  xhr.send(serialize(form));
}

注意 POST 请求相比 GET 请求要占用更多资源。从性能方面说,发送相同数量的数据GET请求比 POST请求要快两倍。


1.5 XMLHttpRequest Level2

并非所有浏览器都实现了 XMLHttpRequest Level2的所有部分,但所有浏览器都实现了其中部分功能。

1.5.1 FormData 类型

现代 Web 应用程序中经常需要对表单数据进行序列化,因此 XMLHtpRequest Level 2 新增了FormData 类型。
FormData 类型便于表单序列化,也便于创建与表单类似格式的数据然后通过 XHR发送。

下面的代码创建了一个 FormData 对象,并填充了一些数据:

let data = new FormData();
// append()方法接收两个参数:键和值,相当于表单字段名称和该字段的值。
data.append("name", "Nicholas"):

此外,通过直接给 FormData 构造函数传入一个表单元素,也可以将表单中的数据作为键/值对填充进去:
let data = new FormData(document.forms[0]);

let xhr = new XMLHttpRequest();

xhr.onreadystatechange = function () {
  if (xhr.readyState == 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
      alert(xhr.responseText);
    } else {
      alert("Request was unsuccessful: " + xhr.status);
    }
  }
};

xhr.open("post", "postexample.php", true);
// 来自D为"user-info"的表单中的数据被序列化之后发送给了服务器。
let form = document.getelementById("user-info");
xhr.send(new FormData(form));

使用 FormData 的另一个方便之处是不再需要给 XHR对象显式设置任何请求头部了。XHR对象能够识别作为 FormData 实例传人的数据类型并自动配置相应的头部。


1.5.2 超时

XHR对象增加了一个 timeout 属性,用于表示发送请求后等待多少毫秒,如果响应不成功就中断请求。

let xhr = new XMLHttpRequest();

xhr.onreadystatechange = function () {
  if (xhr.readyState == 4) {
    try {
      if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
        alert(xhr.responseText);
      } else {
        alert("Request was unsuccessful: " + xhr.status);
      }
    } catch (ex) {
      //假设由ontimeout 处理
    }
  }
};

xhr.open("get", "timeout.php", true);
// 给 timeout 设置1000 毫秒意味着,如果请求没有在)秒钟内返回则会中断。
// 此时则会触发 ontimeout 事件处理程序,readystate 仍然会变成4,因此也会调用 onreadystatechange 事件处理程序。
xhr.timeout = 1000;
// 调用 ontimeout 事件处理
xhr.ontimeout = function () {
  alert("Request did not return in a second.");
};
xhr.send(null);
1.5.3 overrideMimeType()方法

Firefox 首先引人了 overrideMimeType() 方法用于写 XHR 响应的 MIME 类型。这个特性后来也被添加到了 XMLHtpRequest Level 2。因为响应返回的 MIME 类决定了 XHR 对象如何处理响应,所以如果有办法覆盖服务器返回的类型,那么是有帮助的。

假设服务器实际发送了 XML数据,但响应头设置的 MIME 类型是 text/plain 。结果就会导致然数据是 XML,但 responseXML 属性是 null。此时调用 overrideMimeType() 可以保证将响应当成 XML 而不是纯文本来处理:

// 这个例子强制让 XHR 把响应当成 XML 而不是纯文本来处理。为了正确覆盖响应的 MIME 类型
// 必须在调用 sena()之前调用 overrideMimeType()。
let xhr = new XMLHttpRequest();
xhr.open("get", "text.php", true);
xhr.overrideMimeType("text /xm1");
xhr.send(null);


2 进度事件 Progress Events


ProgressEvent 接口是测量如 HTTP 请求(一个XMLHttpRequest,或者一个 <img><audio><video><style><link> 等底层资源的加载)等底层流程进度的事件。

每次请求都会首先触发 loadstart 事件,之后是一个或多个 progress 事件,接着是 error, abortload 中的一个,最后以 loadend 事件结束。

进度事件 描述
loadstart 在接收到响应的第一个字节时触发。
progress 在接收响应期间反复触发。
error 在请求出错时触发。
abort 在调用 abort()终止连接时触发。
loadend 通信完成时,且在 error、abort 或 load 之后触发。
load 在成功接收完响应时触发。

2.1 load 事件

增加了一个 load 事件用于替代 readystatechange 事件。 load 事件在响应接收完成后立即触发,这样就不用检查 readystate 属性了。onload 事件处理程序会收到一个 event 对象,其 target 属性设置为 XHR 实例,在这个实例上可以访问所有 XHR对象属性和方法。

只要是从服务器收到响应,无论状态码是什么,都会发 load 事件。这意味着还需要检查 status 属性才能确定数据是否有效。Firefox、Opera、Chrome 和 Safari 都支持 load 事件。

let xhr = new XMLHttpRequest();
xhr.onload = function () {
  if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
    alert(xhr.responseText);
  } else {
    alert("Request was unsuccessful: " + xhr.status);
  }
};

xhr.open("get", "altevents.php", true);
xhr.send(null);

2.2 progress 事件

progress 事件,在浏览器接收数据期间,这个事件会反复触发。每次触发时, onprogress 事件处理程序都会收到 event 对象,其 target 属性是 XHR对象。且包含3个额外属性:

  • lengthcomputable: 是一个布尔值,表示进度信息是否可用
  • position: 是接收到的字节数
  • totalsize: 响应的 Content-Length 头部定义的总字节数
let xhr = new XMLHttpRequest();

xhr.onload = function () {
  if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
    alert(xhr.responseText);
  } else {
    alert("Request was unsuccessful: " + xhr.status);
  }
};

// 必须在调用open()之前添加 onprogress 事件处理程序。
xhr.onprogress = function (event) {
  let divstatus = document.getElementById("status");
  // 每次触发 progress 事件都会更新 HTML 元素中的信息。
  if (event.lengthComputable) {
    // 假设响应有 content-Length 头部,就可以利用这些信息计算出已经收到响应的百分比。
    divstatus.innerHTML = "Received" + event.position + "of" + event.totalSize + "bytes";
  }
};

xhr.open("get", "altevents.php", true);
xhr.send(null);


3. 跨资源共享(CORS)


跨源资源享(CORS,Cross-Onigin Resource Sharing)定义了浏览器与服务器如何实现跨源通信。CORS 背后的基本思路就是使用自定义的 HTTP 头部允许浏览器和服务器相互了解,以确实请求或响应应该成功还是失败。

对于简单的请求,比如 GET 或 POST 请求,没有自定义头部,而且请求体是 text/plain 类型这样的请求在发送时会有一个外的头部叫 origin。origin 头部包含发送请求的页面的源(协议域名和端口),以便服务器确定是否为其提供响应。下面是 origin 头部的一个示例:

Origin: http://www.nczonline.net

如果服务器决定响应请求,那么应该发送 Access-control-Allow-origin 头部,包含相同的源或者如果资源是公开的,那么就包含 * 。比如:

Access-Control-Allow-Origin: http://www.nczonline.net

如果没有这个头部,或者有但源不匹配,则表明不会响应浏览器请求。否则,服务器就会处理这个请求。注意,无论请求还是响应都不会包含 cookie 信息。

// 现代浏览器通过 XMLHttpRequest 对象原生支持 CORS。在尝试访问不同源的资源时,这个行为会被自动触发。
let xhr = new XMLHttpRequest();

xhr.onload = function () {
  if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
    alert(xhr.responseText);
  } else {
    alert("Request was unsuccessful: " + xhr.status);
  }
};

// 要向不同域的源发送请求,可以使用标准 XHR对象并给 open ()方法传人一个绝对 URL比如:
xhr.open("get", "http://www.somewhere-else.com/page/", true);
xhr.send(null);

跨域 XHR 对象允许访问 statusstatusText 属性,也允许同步请求。出于安全考虑,跨域 XHR对象也施加了一些额外限制。

  • 不能使用 setRequestHeader() 设置自定义头部。
  • 不能发送和接收 cookie。
  • getAllResponseHeaders() 方法始终返回空字符串。

因为无论同域还是跨域请求都使用同一个接口,所以最好在访问本地资源时使用相对 URL,在访问远程资源时使用绝对 URL。这样可以更明确地区分使用场景,同时避免出现访问本地资源时出现头部或cookie 信息访问受限的问题。

3.1 预检请求

通常是由浏览器自动发起的请求,开发者无需手动去操作。

一个 CORS 预检请求是用于检查服务器是否支持 CORS 即跨域资源共享。

它一般是用了以下几个 HTTP 请求首部的 OPTIONS 请求:Access-Control-Request-Method 和 Access-Control-Request-Headers,以及一个 Origin 首部。

  • origin: 与简单请求相同。
  • Access-Contro1-Request-Method: 请求希望使用的方法。
  • Access-Control-Request-Headers: (可选)要使用的逗号分隔的自定义头部列表。

一个客户端可能会在实际发送一个 DELETE 请求之前,先向服务器发起一个预检请求,用于询问是否可以向服务器发起一个 DELETE 请求:

OPTIONS /resource/foo
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: origin, x-requested-with
Origin: https://foo.bar.org

如果服务器允许,那么服务器就会响应这个预检请求。并且其响应首部 Access-Control-Allow-Methods 会将 DELETE 包含在其中:

  • Access-Contro1-Allow-origin: 与简单请求相同。
  • Access-Contro1-Allow-Methods: 允许的方法(逗号分隔的列表)。
  • Access-Contro1-Allow-Headers: 服务器允许的头部(逗号分隔的列表 )。
  • Access-Contro1-Max-Age: 缓存预检请求的秒数。
HTTP/1.1 200 OK
Content-Length: 0
Connection: keep-alive
Access-Control-Allow-Origin: https://foo.bar.org
Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE
Access-Control-Max-Age: 86400

预检请求返回后,结果会按响应指定的时间缓存一段时间。换句话说,只有第一次发送这种类型的请求时才会多发送一次额外的 HTTP 请求。


3.2 凭据请求

XMLHttpRequest.withCredentials 属性是一个布尔值,它指示了是否该使用类似 cookieAuthorization标头 或者 TLS客户端证书 等凭据进行跨站点访问控制(Access-Control)请求。设置 withCredentials 对同源请求是无效的。

默认情况下,跨源请求不提供凭据(cookie、HTTP 认证和客户端 SSL 证书)。可以通过将 withcredentials 属性设置为 true 来表明请求会发送凭据。如果服务器允许带凭据的请求,那么可以在响应中包含如下 HTTP 头部:

Access-Control-Allow-Credentials: true

如果发送了凭据请求而服务器返回的响应中没有这个头部,则浏览器不会把响应交给 JavaScript( responseText 是空字符串, status 是0, onerror() 被调用)。注意,服务器也可以在预检请求的响应中发送这个 HTTP 头部,以表明这个源允许发送凭据请求。

const xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com/", true);
xhr.withCredentials = true;
xhr.send(null);


4 替代性跨源技术


CORS 出现之前,实现跨源 Ajax 通信是有点麻烦的。开发者需要依赖能够执行跨源请求的 DOM特性,在不使用 XHR对象情况下发送某种类型的请求。虽然CORS 目前已经得到广泛支持,但这些技术仍然没有过时,因为它们不需要修改服务器。

4.1 图片探测

图片探测是利用<img>标签实现跨域通信的最早的一种技术。任何页面都可以跨域加载图片而不必担心限制,因此这也是在线广告跟踪的主要方式。可以动态创建图片,然后通过它们的 onloadonerror 事件处理程序得知何时收到响应。

这种动态创建图片的技术经常用于图片探测(imagepings)。图片探测是与服务器之间简单、跨域单向的通信。数据通过查询字符串发送,响应可以随意设置,不过一般是位图图片或值为 204 的状态码浏览器通过图片探测拿不到任何数据,但可以通过监听 onloadonerror 事件知道什么时候能接收到响应。

图片探测频繁用于跟踪用户在页面上的点击操作或动态显示广告。当然,图片探测的缺点是只能发送 GET 请求和无法获取服务器响应的内容。这也是只能利用图片探测实现浏览器与服务器单向通信的原因。

下面看一个例子:

// 创建了一个新的 Image 实例
let img = new Image();
// 然后为它的 onload 和 onerror 事件处理程序添加了同-个函数。
// 这样可以确保请求完成时无论什么响应都会收到通知。
img.onload = img.onerror = function () {
  alert("Done!");
};
// 设置完src 属性之后请求就开始了,这个例子向服务器发送了一个 name 值。
img.src = "http://www.example.com/test?name=Nicholas";

4.2 JSONP

也是一种实现跨域的手段。

原理:script标签发送不受同源策略影响。前后端不分离已弃用,面试会问。



5 Fetch API


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

Fetch API 本身是使用 JavaScript 请求资源的优秀工具,同时这个 API也能够应用在服务线程(service worker) 中,提供拦截、重定向和修改通过 fetch()生成的请求接口。

5.1 基本用法

fetch() 方法是暴露在全局作用域中的,包括主页面执行线程、模块和工作线程。调用这个方法浏览器就会向给定 URL 发送请求。

5.1.1 分派请求

fetch() 只有一个必需的参数 input 。多数情况下,这个参数是要获取资源的 URL 。这个方法返回一个期约。

// URL 的格式(相对路径、绝对路径等)的解释与 XHR 对象一样。
let f = fetch("/bar");
console.log(r); // Promise <pending>

请求完成、资源可用时,期约会解决为一个 Response 对象。这个对象是 API的封装,可以通过它取得相应资源。获取资源要使用这个对象的属性和方法,掌握响应的情况并将负载转换为有用的形式,如下所示:

fetch("bar.text").then(response => {
  console.log(response);
});

// Response { type : "basic" , url:...}

5.1.2 读取响应

读取响应内容的最简单方式是取得纯文本格式的内容,这要用到 text() 方法。这个方法返回一个期约,会解决为取得资源的完整内容:

fetch("bar.text").then(response => {
  response.text().then(data => {
    console.log(data);
  });
});

// bar.txt的内容
fetch("bar.text")
  .then(response => response.text())
  .then(data => console.log(data));

// bar.txt的内容

5.1.3 处理状态码和请求失败

Fetch API支持通过 Response 的 status(状态码) 和 statusText(状态文本) 属性检查响应状态。成功获取响应的请求通常会产生值为 200的状态码,如下所示:

fetch("/bar").then(response => {
  console.log(response.status); // 200
  console.log(response.statusText); // OK
});

请求不存在的资源通常会产生值为 404 的状态码:

fetch("/does-not-exist").then(response => {
  console.log(response.status); // 404
  console.log(response.statusText); // Not Found
});

请求的 URL 如果抛出服务器错误会产生值为 500 的状态码:

fetch("/throw-server-error").then(response => {
  console.log(response.status); // 500)
  console.log(response.statusText); // Internal Server Error
});

可以显式地设置 fetch() 在遇到重定向时的行为(本章后面会介绍),不过默认行为是跟随重定向并返回状态码不是300~399 的响应。跟随重定向时,响应对象的 redirected 属性会被设置为 true,而状态码仍然是 200:

fetch("/permanent-redirect").then(response => {
  // 默认行为是跟随重定向直到最终 URL
  // 这个例子会出现至少两轮网络请求
  // <origin url>/permanent-redirect   ->   <redirect url>
  console.log(response.status); // 200
  console.log(response.statusText); // OK
  console.log(response.redirected); // true.
});

在前面这几个例子中,虽然请求可能失败(如状态码为500),但都只执行了期约的解决处理函数事实上,只要服务器返回了响应, fetch() 期约都会解决。这个行为是合理的:系统级网络协议已经成功完成消息的一次往返传输。至于真正的“成功”请求,则需要在处理响应时再定义。

通常状态码为200时就会被认为成功了,其他情况可以被认为未成功。为区分这两种情况,可以在状态码非 200~299 时检查 Response 对象的 ok 属性:

fetch("/bar").then(response => {
  console.log(response.status); // 200
  console.log(response.ok); // true
});

fetch("/does-not-exist").then(response => {
  console.log(response.status); // 404
  console.log(response.ok); // true
});

因为服务器没有响应而导致浏览器超时,这样真正的 fetch()失败会导致期约被拒绝:

违反 CORS、无网络连接、HTTPS 错配及其他浏览器/网络策略问题都会导致期约被拒绝。

fetch("/hangs-forever").then(
  response => {
    console.log(response);
  },
  err => {
    console.log(err);
  }
);

//(浏览器超时后)
// TypeError: "NetworkError when attempting to fetch resource.

可以通过 url 属性检查通过 fetch() 发送请求时使用的完整URL

// foo.com/bar/baz发送的请求
console.log(window.location.href); // https://foo.com/bar/baz
fetch("qux").then(response => console.log(response.url)); // https://foo.com/bar/qux
fetch("/qux").then(response => console.log(response.url)); // https://foo.com/qux
fetch("//qux.com").then(response => console.log(response.url)); // https://qux.com
fetch("https://qux.com").then(response => console.log(response.url)); // https://qux.com

5.1.4 自定义选项

只使用 URL时, fetch() 会发送 GET 请求,只包含最低限度的请求头。要进一步配置如何发送请求,需要传入可选的第二个参数 init 对象。init 对象要按照下表中的键/值进行填充。

method 请求使用的方法,
如 GET、 POST。
headers 请求的头信息,形式为 Headers 的对象或包含 ByteString 值的对象字面量。
body 指定使用请求体时请求体的内容:
必须是 Blob、BufferSource、FormData、URLSearchparams、ReadableStream 或 String 的实例。
注意 GET 或 HEAD 方法的请求不能包含 body 信息。
mode 请求的模式,如 cors、no-cors 或者 same-origin。
credentials 请求的 credentials,如 omit、same-origin 或者 include。为了在当前域名内自动发送 cookie,必须提供这个选项,从 Chrome 50 开始,这个属性也可以接受 FederatedCredential (en-US) 实例或是一个 PasswordCredential (en-US) 实例。
cache 用于控制浏览器与 HTTP 缓存的交互。要跟踪缓存的重定向,请求的redirect 属性值必须是"follow",而且必须符合同源策略限制。必须是下列值之一。
请求的 cache 模式:default、 no-store、 reload 、 no-cache、 force-cache 或者 only-if-cached。
redirect 可用的 redirect 模式:follow (自动重定向), error (如果产生重定向将自动终止并且抛出一个错误),或者 manual (手动处理重定向)。在 Chrome 中默认使用 follow)
referrer 一个 USVString 可以是 no-referrer、client 或一个 URL。默认是 client。
referrerPolicy 指定了 HTTP 头部 referer 字段的值。可能为以下值之一:no-referrer、 no-referrer-when-downgrade、origin、origin-when-cross-origin、 unsafe-url。
integrity 包括请求的 subresource integrity 值(例如: sha256-BpfBw7ivV8q2jLiT13fxDYAe2tJllusRSZ273h2nFSE=)。
// Example POST method implementation:
async function postData(url = "", data = {}) {
  // Default options are marked with *
  const response = await fetch(url, {
    method: "POST", // *GET, POST, PUT, DELETE, etc.
    mode: "cors", // no-cors, *cors, same-origin
    cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
    credentials: "same-origin", // include, *same-origin, omit
    headers: {
      "Content-Type": "application/json"
      // 'Content-Type': 'application/x-www-form-urlencoded',
    },
    redirect: "follow", // manual, *follow, error
    referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    body: JSON.stringify(data) // body data type must match "Content-Type" header
  });
  return response.json(); // parses JSON response into native JavaScript objects
}

postData("https://example.com/answer", { answer: 42 }).then(data => {
  console.log(data); // JSON data parsed by `data.json()` call
});

5.2 常见Fetch请求模式

与 XMLHttpRequest 一样, fetch() 既可以发送数据也可以接收数据。使用 init 对象参数,可以配置 fetch() 在请求体中发送各种序列化的数据。

5.2.1 发送JSON 数据

可以像下面这样发送简单 JSON 字符串:

let payload = JSON.stringify({ foo: "bar" });

let jsonHeaders = new Headers({ "Content-Type": "application/json" });

fetch("/send-me-json", {
  method: "POST",
  body: payload,
  headers: jsonHeaders
});

5.2.2 在请求体中发送参数

因为请求体支持任意字符串值,所以可以通过它发送请求参数

let payload = "foo=bar&baz=gux";

let paramHeaders = new Headers({ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" });

fetch("/send-me-json", {
  method: "POST",
  body: payload,
  headers: paramHeaders
});

5.2.3 发送文件

因为请求体支持 FormData 实现,所以 fetch() 也可以序列化并发送文件字段中的文件

let imageFormData = new FormData();

let imageInput = document.querySelector("input[type='file']");

imageFormData.append("image", imageInput.files[0]);

fetch("/img-upload", {
  method: "POST",
  body: imageFormData
});

这个 fetch() 实现可以支持多个文件.

let imageFormData = new FormData();
let imageInput = document.queryselector("input [type='file'][multiple]");

for (let i = 0; i < imageInput.files.length; ++i) {
  imageFormData.append("image", imageInput.files[i]);
}

fetch("/img-upload", {
  method: "POST",
  body: imageFormData
});

5.2.4 加载 Blob 文件

FetchAPI 也能提供 Blob 类型的响应,而 Blob 又可以兼容多种浏览器API。一种常见的做法是明确将图片文件加载到内存,然后将其添加到HTML图片元素。为此,可以使用响应对象上暴露的 blob() 方法。这个方法返回一个期约,解决为一个 Blob 的实例。然后,可以将这个实例传给 URL.createobjectur1() 以生成可以添加给图片元素 src 属性的值:

const imageElement = document.querySelector("img");

fetch("my-image.png")
  .then(response => response.blob())
  .then(blob => {
    imageElement.src = URL.createObjectURL(blob);
  });

5.2.5 发送跨源请求

从不同的源请求资源,响应要包含 CORS 头部才能保证浏览器收到响应。没有这些头部,跨源请求会失败并抛出错误。

fetch("//cross-origin.com");
// TypeError: Failed to fetch
// No 'Access-Control-Allow-Origin' header is present on the requested resource

如果代码不需要访问响应,也可以发送 no-cors 请求。此时响应的 type 属性值为 opaque,因此无法读取响应内容。这种方式适合发送探测请求或者将响应缓存起来供以后使用

fetch("//cross-origin.com", { method: "no-cors" }).then(response => console.log(response.type));

// opaque

5.2.6 中断请求

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

let abortController = new AbortController();

fetch("wikipedia.zip", { signal: abortController.signal }).catch(() => console.log("aborted"));

//10毫秒后中断请求
setTimeout(() => abortController.abort(), 10);

// 已经中断

5.3 Headers对象

Headers 对象是所有外发请求和人站响应头部的容器。每个外发的 Request 实例都包含一个空的 Headers 实例,可以通过 Request.prototype.headers 访问,每个人站 Response 实例也可以通过 Response.prototype.headers 访问包含着响应头部的 Headers 对象。这两个属性都是可修改属性。另外,使用 new Headers() 也可以创建一个新实例。

5.3.1 Headers与Map 的相似之处

Headers 对象与 Map 对象极为相似。这是合理的,因为 HTTP 头部本质上是序列化后的键/值对它们的 JavaScript 表示则是中间接口。Headers 与 Map 类型都有 get()set()has()delete() 等实例方法,如下面的代码所示:

let h = new Headers();
let m = new Map();

// 设置键
h.set("foo", "bar");
m.set("foo", "bar");

//检查键
console.log(h.has("foo")); // true
console.log(m.has("foo")); // true
console.log(h.has("qux")); // false
console.log(m.has("qux")); // false

// 获取值
console.log(h.get("foo")); // bar
console.log(m.get("foo")); // bar

// 更新值
h.set("foo", "baz");
m.set("foo", "baz");

// 取得更新的值
console.log(h.get("foo")); // baz
console.log(m.get("foo")); // baz

// 删除值
h.delete("foo");
m.delete("foo");

// 确定值已经删除
console.log(h.get("foo")); // undefined
console.log(m.get("foo")); // undefined

Headers 和 Map 都可以使用一个可迭代对象来初始化,比如

let seed = [["foo", "bar"]];

let h = new Headers(seed);
let m = new Map(seed);

console.log(h.get("foo")); // bar
console.log(m.get("foo")); // bar

而且,它们也都有相同的 keys()values()entries() 迭代器接口

let seed = [
  ["foo", "bar"],
  ["baz", "qux"]
];

let h = new Headers(seed);
let m = new Map(seed);

console.log(...h.keys()); // foo, baz
console.log(...m.keys()); // foo, baz

console.log(...h.values()); // bar, qux
console.log(...m.values()); // bar, qux

console.log(...h.entries()); //['foo','bar'], ['baz'!'qux']
console.log(...m.entries()); // ['foo', 'bar'], ['baz','qux']

5.3.2 Headers 独有的特性

Headers 并不是与 Map 处处都一样。在初始化 Headers 对象时,也可以使用键/值对形式的对象,而 Map 则不可以

let seed = { foo: "bar" };

let h = new Headers(seed);
console.log(h.get("foo")); // bar

let m = new Map(seed); // TypeError: object is not iterable

一个 HTTP 头部字段可以有多个值,而 Headers 对象通过 append() 方法支持添加多个值。在 Headers 实例中还不存在的头部上调用 append() 方法相当于调用 set() 。后续调用会以逗号为分隔符拼接多个值:

let h = new Headers();

h.append("foo", "bar");
console.log(h.get("foo")); // "bar"

h.append("foo", "baz");
console.log(h.get("foo")); // "bar, baz"

5.3.3 头部护卫

某些情况下,并非所有 HTTP 头部都可以被客户端修改,而 Headers 对象使用护卫来防止不被允许的修改。不同的护卫设置会改变 set()append()delete() 的行为。违反护卫限制会抛出TypeError。

Headers 实例会因来源不同而展现不同的行为,它们的行为由护卫来控制。JavaScript 可以决定 Headers 实例的护卫设置。下表列出了不同的护卫设置和每种设置对应的行为。

护卫 适用情形 限制
none 在通过构造函数创建 Headers 实例时激活
request 在通过构造函数初始化Request 对象,且mode值为非 no-cors 时激活 不允许修改禁止修改的头部(参见MDN 文档中的 forbidden header name 词条 )不允许修改非简单头部(参见 MDN)
request-no-cors 在通过构造函数初始化Request 对象,且mode值为 no-cors 时激活 不允许修改非简单头部(参见 MDN 文档中的simple header 词条 )
response 在通过构造函数初始化 Response 对象时激活 不允许修改禁止修改的响应头部(参见 MDN 文档中的 forbidden response header name词条)
immutable 在通过 error()或 redirect()静态方法初始化 Response 对象时激活 不允许修改任何头部

5.4 REQUEST 对象

顾名思义,Request 对象是获取资源请求的接口。这个接口暴露了请求的相关信息,也暴露了使用请求体的不同方式。

5.4.1 创建Request 对象

可以通过构造函数初始化 Request 对象。为此需要传入一个 input 参数,一般是 URL。

let r = new Request("https://foo.com");
console.loq(r);
// Request {...]

Request 构造数也接收第二个参数一个 init 对象。这个 init 对象与前面介绍的 fetch()init 对象一样。没有在 init 对象中涉及的值则会使用默认值.

// 用所有默认值创建 Request 对象
console.log(new Request(""));

Request {
	bodyUsed:false,
	cache: "default",
	credentials: "same-origin",
	destination:"",
	headers: Headers(),
	integrity:"",
	keepalive: false,
	method: "GET",
	mode: "cors",
	redirect: "follow",
	referrer: "about:client",
	referrerPolicy: "",
	signal: Abortsignal (aborted: false, onabort: null),
	url: "<current URL>"
}

// 用置顶初始值创建 Request 对象
console.log(new Request("https://foo.com", { method: "POST" }));


Request {
	bodyUsed:false,
	cache: "default",
	credentials: "same-origin",
	destination:"",
	headers: Headers(),
	integrity:"",
	keepalive: false,
	method: "POST",
	mode: "cors",
	redirect: "follow",
	referrer: "about:client",
	referrerPolicy: "",
	signal: Abortsignal(aborted: false, onabort: null),
	url: "<current URL>"
}

5.4.2 克隆 Request 对象

Fetch API提供了两种不太一样的方式用于创建 Request 对象的副本: 使用 Request 构造函数和使用 clone() 方法。

Request 实例作为 input 参数传给 Request 构造函数,会得到该请求的一个副本:

let r1 = new Request("https://foo.com");
let r2 = new Request(r1);

console.log(r2.url); // https://foo.com/

如果再传人 init 对象,则 init 对象的值会盖源对象中同名的值

let r1 = new Request("https://foo.com");
let r2 = new Request(r1, { method: "posT" });

console.log(r1.method); // GET
console.log(r2.method); // POST

这种克隆方式并不总能得到一模一样的副本。最明显的是,第一个请求的请求体会被标记为“已使用”

let r1 = new Request("https://foo.com", { method: "POST", body: "foobar" });
let r2 = new Request(r1);

console.log(r1.bodyUsed); // true
console.log(r2.bodyUsed); // false

如果源对象与创建的新对象不同源,则 referrer 属性会被清除。此外,如果源对象的 modenavigate ,则会被转换为 same-origin

第二种克隆 Request 对象的方式是使用 clone() 方法,这个方法会创建一模一样的副本,任何值都不会被覆盖。与第一种方式不同,这种方法不会将任何请求的请求体标记为“已使用”:

let r1 = new Request("https://foo.com", { method: "POST", body: "foobar" });
let r2 = r1.clone();

console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false

如果请求对象的 bodyUsed 属性为 true (即请求体已被读取), 那么上述任何一种方式都不能用来创建这个对象的副本。在请求体被读取之后再克隆会导致抛出 TypeError。

let r = new Request("https://foo.com");
r.clone();
new Request(r); // 没有错误

r.text(); // 设置 bodyUsed为true
r.clone();
// TypeError: Failed to execute 'clone' on 'Request': Request body is already used

new Request(r);
// TypeError: Failed to construct 'Request': Cannot construct a Request with a Request object that has already been used.

5.4.3 在fetch()中使用 Request 对象

fetch() Request 构造函数拥有相同的函数签名并不是巧合。在调用 fetch() 时,可以传人已经创建好的 Request 实例而不是 URL。与 Request 构造函数一样,传给 fetch()init 对象会覆盖传人请求对象的值:

let r = new Request("https://foo.com");
//向foo.com 发送GET请求
fetch(r);
//向foo.com 发送 POST 请求
fetch(r, { method: "posT" });

fetch() 会在内部克隆传人的 Request 对象。与克隆 Request 一样, fetch() 也不能拿请求体已经用过的 Request 对象来发送请求:

let r1 = new Request("https://foo.com", { method: "POST", body: "foobar" });
r.text();
fetch(r);
// TypeError: Cannot construct a Request with a Request object that has already been used.

关键在于,通过 fetch 使用 Request 会将请求体标记为已使用。也就是说,有请求体的 Request 只能在一次 fetch 中使用。(不包含请求体的请求不受此限制。)演示如下:

let r1 = new Request("https://foo.com", { method: "POST", body: "foobar" });

fetch(r);
fetch(r);
// TypeError: Cannot construct a Request with a Request object that has already been used.

要想基于包含请求体的相同 Request 对象多次调用 fetch() ,必须在第一次发送 fetch() 请求前调用 clone() :

let r1 = new Request("https://foo.com", { method: "POST", body: "foobar" });

// 3个都会成功
fetch(r.clone());
fetch(r.clone());
fetch(r);

5.5 RESPONSE 对象

顾名思义, Response 对象是获取资源响应的接口。这个接口暴露了响应的相关信息,也暴露了使用响应体的不同方式。

5.5.1 创建Response 对象

可以通过构造函数初始化 Response 对象且不需要参数。此时响应实例的属性均为默认值,因为它并不代表实际的 HTTP 响应:

// 用所有默认值创建 Request 对象
let r = new Response();
console.log(r)

Response {
	body: (...)
	bodyUsed: false
	headers: Headers ()
	ok: true
	redirected: false
	status: 200
	statusText: "OK"
	type: "default"
	url: ""
}

Response 构造函数接收一个可选的 body 参数。这个 body 可以是 null ,等同于 fetch() 参数 init 中的 body 。还可以接收一个可选的 init 对象,这个对象可以包含下表所列的键和值。

headers 必须是 Headers 对象实例或包含字符串键/值对的常规对象实例
默认为没有键/值对的 Headers 对象
status 表示 HTTP 响应状态码的整数
默认为 200
statusText 表示 HTTP 响应状态的字符串
默认为空字符串

可以像下面这样使用 bodyinit 来构建 Response 对象:

let r = new Response("foobar", {
  status: 418,
  statusText: "I'm a teapot"
});
console.log(r);

Response {
	body: (...)
	bodyUsed: false
	headers: Headers ()
	ok: false
	redirected: false
	status: 418
	statusText: "I'm a teapot"
	type: "default"
	url: ""
}

大多数情况下,产生 Response 对象的主要方式是调用 fetch() ,它返回一个最后会解决为 Response 对象的期约,这个 Response 对象代表实际的 HTTP 响应。下面的代码展示了这样得到的 Response 对象:

fetch("https://foo.com").then(response =>{
	console.log(response);
})

Response {
	body: (...)
	bodyUsed: false
	headers: Headers ()
	ok: true
	redirected: false
	status: 200
	statusText: "OK"
	type: "basic"
	url: ""
}

Response 类还有两个用于生成 Response 对象的静态方法: Response.redirect()Response.error() 。前者接收一个 URL 和一个重定向状态码(301、302、303、307 或308), 返回重定向的 Response 对象:

console.log(Response.redirect('https://foo.com',301));

Response {
	body: (...)
	bodyUsed: false
	headers: Headers ()
	ok: false
	redirected: false
	status: 301
	statusText:""
	type: "default"
	url: ""
}

提供的状态码必须对应重定向,否则会抛出错误

提供的状态码必须对应重定向,否则会抛出错误

Response.redirect("https://foo.com", 200);
// RangeError: Failed to execute 'redirect' on 'Response': Invalid status code

另一个静态方法 Response.error() 用于产生表示网络错误的 Response 对象(网络错误会导致 fetch() 期约被拒绝)。

console.log(Response.error());

Response {
	body: (...)
	bodyUsed: false
	headers: Headers ()
	ok: false
	redirected: false
	status: 0
	statusText:""
	type: "error"
	url: ""
}

5.5.2 读取响应状态信息

Response 对象包含一组只读属性,描述了请求完成后的状态,如下表所示。

属性
headers 响应包含的 Headers 对象
ok 布尔值,表示 HTTP 状态码的含义。200~299 的状态码返回 true,其他状态码返回 false
redirected 布尔值,表示响应是否至少经过一次重定向
status 整数,表示响应的 HTTP 状态码
statusText 字符串,包含对 HTTP 状态码的正式描述。这个值派生自可选的 HTTP Reason-Phrase 字段
因此如果服务器以 Reason-Phrase为由拒绝响应,这个字段可能是空字符串
type 字符串,包含响应类型。可能是下列字符串值之一
basic:表示标准的同源响应
cors:表示标准的跨源响应
error: 表示响应对象是通过 Response.error() 创建的
opaque: 表示 no-cors 的fetch()返回的跨源响应
opaqueredirect:表示对redirect 设置为 manual 的请求的响应
url 包含响应 URL的字符串。对于重定向响应,这是最终的 URL,非重定向响应就是它产生的

以下代码演示了返回 200、302、404和 500 状态码的 URL 对应的响应

fetch("//foo.com").then(response => console.log(response));

Response {
	body: (...)
	bodyUsed: false
	headers: Headers()
	ok: true
	redirected: false
	status: 200
	statusText: "OK"
	type: "basic"
	url: "https://foo.com/"
}
fetch("//foo.com/redirect-me").then(response => console.log(response));

Response {
	body: (...)
	bodyUsed: false
	headers: Headers()
	ok: true
	redirected: true
	status: 200
	statusText: "OK"
	type: "basic"
	url: "https://foo.com/redirected-url/"
}
fetch("//foo.com/does-not-exist").then(response => console.log(response));

Response {
	body: (...)
	bodyUsed: false
	headers: Headers()
	ok: false
	redirected: true
	status: 404
	statusText: "Not Found"
	type: "basic"
	url: "https://foo.com/does-not-exist/"
}
fetch("//foo.com/throws-error").then(response => console.log(response));

Response {
	body: (...)
	bodyUsed: false
	headers: Headers()
	ok: false
	redirected: true
	status: 500
	statusText: "Internal Server Error"
	type: "basic"
	url: "https://foo.com/throws-error/"
}

5.5.3 克隆 Response 对象

克隆 Response 对象的主要方式是使用 clone() 方法,这个方法会创建一个一模一样的副本,不会盖任何值。这样不会将任何请求的请求体标记为已使用

let r1 = new Response("foobar");
let r2 = r1.clone();

console.log(r1.bodyUsed); // false
console.log(r2.bodyUsed); // false

如果响应对象的 bodyUsed 属性为 true(即响应体已被读取),则不能再创建这个对象的副本。在响应体被读取之后再克隆会导致抛出 TypeError。

let r = new Response("foobar");
r.clone(); // 没有错误

r.text(); // 设置bodyUsed为true

r.clone();
// TypeError: Failed to execute 'clone' on 'Response': Response body is already used

有响应体的 Response 对象只能读取一次。( 不包含响应体的 Response 对象不受此限制。) 比如:

let r = new Response("foobar");

r.text().then(console.log); // foobar

r.text().then(console.log);

// TypeError: Failed to execute 'text' on 'Response': body stream is locked

要多次读取包含响应体的同一个 Response 对象,必须在第一次读取前调用 clone():

let r = new Response("foobar");

r.clone().text().then(console.log); // foobar
r.clone().text().then(console.log); // foobar
r.text().then(console.log); // foobar

此外,通过创建带有原始响应体的 Response 实例,可以执行伪克隆操作。关键是这样不会把第一个 Response 实例标记为已读,而是会在两个响应之间共享:

let r1 = new Response("foobar");
let r2 = new Response(r1.body);

console.log(r1.bodyUsed); // false
console.log(r2.bodyused); // false

r2.text().then(console.log); // foobar
r1.text().then(console.log);
// TypeError: Failed to execute 'text' on 'Response': body stream is locked

5.6 REQUEST、RESPONSE 和 BODY的混入

RequestResponse 都使用了 Fetch API 的 Body 混人,以实现两者承担有效载荷的能力。这个混人为两个类型提供了只读的 body 属性( 实现为 Readablestream )、只读的 bodyUsed 布尔值(表示 body 流是否已读) 和一组方法,用于从流中读取内容并将结果转换为某种 JavaScript对象类型。

通常,将 RequestResponse 主体作为流来使用主要有两个原因。一个原因是有效载荷的大小可能会导致网络延迟,另一个原因是流 API本身在处理有效载荷方面是有优势的。除此之外,最好是一次性获取资源主体。

Body 混入提供了5个方法,用于将 Readablestream 转存到缓冲区的内存里,将缓冲区转换为某种 JavaScript 对象类型,以及通过期约来产生结果。在解决之前,期约会等待主体流报告完成及缓冲被解析。这意味着客户端必须等待响应的资源完全加载才能访问其内容。

5.6.1 Body.text()

Body.text() 方法返回期约,解决为将缓冲区转存得到的 UTF-8 格式字符串。下面的代码展示了在 Response 对象上使用 Body.text() :

fetch("https://foo.com")
  .then(response => response.text())
  .then(console.log);

// <!doctype html><htm1 lang="en">
// <head>
// <meta charset="utf-8">
// ...

以下代码展示了在 Request 对象上使用 Body.text() :

let request = new Request("https://foo.com", { method: "posT", body: "barbazqux" });

request.text().then(console.log); // barbazqux

5.6.2 Body.json()

Body.json() 方法返回期约,解决为将缓冲区转存得到的 JSON。下面的代码展示了在 Response对象上使用 Body.json() :

fetch("https://foo.com/foo.json")
  .then(response => response.ison())
  .then(console.log);
// ("foo": "bar")

以下代码展示了在 Request 对象上使用 Body.json():

let request = new Request("https://foo.com", { method: "POST", body: JSONN.stringify({ bar: "baz" }) });

request.json().then(console.log);
// {bar: 'baz')

5.6.3 Body.formData()

浏览器可以将 FormData 对象序列化/反序列化为主体。例如,下面这个 FormData 实例:

let myFormData = new FormData();
myFormData.append("foo", "bar");

在通过 HTTP 传送时,WebKit浏览器会将其序列化为下列内容

-----WebKitFormBoundarydR902kOzE6nbN7eR
Content-Disposition: form-data; name="foo"
bar
------WebKitFormBoundarydR9Q2kOzE6nbN7eR--

Body.formData() 方法返回期约,解决为将缓冲区转存得到的 FormData 实例。下面的代码展示了在 Response 对象上使用 Body.formData() :

fetch("https://foo.com/form-data")
  .then(response => response.formData())
  .then(formData => console.log(formData.get("foo")));

// bar

以下代码展示了在 Request 对象上使用 Body.formData() :

let myFormData = new FormData();
myFormData.append("foo", "bar");
let request = new Request("https://foo.com", { method: "POST", body: myFormData });

request.formData().then(formData => console.log(formData.get("foo")));

// bar

5.6.4 Body.arrayBuffer()

有时候,可能需要以原始二进制格式查看和修改主体。为此,可以使用 Body.arrayBuffer() 将主体内容转换为 ArrayBuffer 实例。 Body.arrayBuffer() 方法返回期约,解决为将缓冲区转存得到的 ArrayBuffer 实例。下面的代码展示了在 Response 对象上使用 Body.arrayBuffer():

fetch("https://foo.com")
  .then(response => response.arrayBuffer())
  .then(console.log);

// ArrayBuffer(...) {)

以下代码展示了在 Request 对象上使用 Body.arrayBuffer() :

let request = new Request("https://foo.com", { method: "POST", body: "abcdefg" });

// 以整数形式打印二进制编码的字符串
request.arrayBuffer().then(buf => console.log(new Int8Array(buf)));

// Int8Array(7) [97,98,99,100,101,102,103]

5.6.5 Body.blob()

有时候,可能需要以原始二进制格式使用主体,不用查看和修改。为此,可以使用 Body.blob()将主体内容转换为 Blob 实例。 Body.blob() 方法返回期约,解决为将缓冲区转存得到的 Blob 实例.

下面的代码展示了在 Response 对象上使用 Body.blob() :

fetch("https://foo.com")
  .then(response => response.blob())
  .then(console.log);

// Blob(...) {size:..., type: "...")

以下代码展示了在 Request 对象上使用 Body.blob() :

let request = new Request("https://foo.com", { method: "POST", body: "abcdefg" });

request.blob().then(console.log);

// Blob(7) (size: 7, type: "text/plain;charset=utf-8")

5.6.6 一次性流

因为 Body 混人是构建在 ReadableStream 之上的,所以主体流只能使用一次。这意味着所有主体混入方法都只能调用一次,再次调用就会抛出错误。

fetch("https://foo.com").then(response => response.blob().then(() => response.blob()));
// TypeError: Failed to execute 'blob' on 'Response': body stream is locked

let request = new Request("https://foo.com", { method: "POST", body: "foobar" });
request.blob().then(() => request.blob());
// TypeError: Failed to execute 'blob' on 'Request': body stream is locked

即使是在读取流的过程中,所有这些方法也会在它们被调用时给 ReadableStream 加锁,以阻止其他读取器访问:

fetch("https://foo.com").then(response => {
  response.blob(); // 第一次调用给流加锁
  response.blob(); //第二次调用再次加锁会失败
});
// TypeError: Failed to execute 'blob' on 'Response': body stream is locked

let request = new Request("https://foo.com", { method: "POST", body: "foobar" });
request.blob(); // 第一次调用给流加锁
request.blob(); // 第二次调用再次加锁会失败
// TypeError: Failed to execute 'blob' on 'Request': body stream is locked

作为 Body 混人的一部分, bodyUsed 布尔值属性表示 ReadableStream 是否已摄受(disturbed),意思是读取器是否已经在流上加了锁。这不一定表示流已经被完全读取。下面的代码演示了这个属性:

let request = new Request("https://foo.com", { method: "POST", body: "foobar" });

let response = new Response("foobar");

console.log(request.bodyUsed); // false
console.log(response.bodyUsed); // false

request.text().then(console.log); // foobar
response.text().then(console.log); // foobar

console.log(request.bodyUsed); // true
console.log(response.bodyUsed); // true

5.6.7 使用 ReadableStream 主体

JavaScript 编程逻辑很多时候会将访问网络作为原子操作,比如请求是同时创建和发送的,响应数据也是以统一的格式一次性暴露出来的。这种约定隐藏了底层的混乱,让涉及网络的代码变得很清晰。从 TCP/IP 角度来看,传输的数据是以分块形式抵达端点的,而且速度受到网速的限制。接收端点会为此分配内存,并将收到的块写人内存。Fetch API 通过 ReadableStream 支持在这些块到达时就实时读取和操作这些数据。

正如Stream API所定义的, ReadableStream 暴露了 getReader() 方法,用于产生 ReadableStreamDefaultReader ,这个读取器可以用于在数据到达时异步获取数据块。数据流的格式是 Uint8Array

下面的代码调用了读取器的 read() 方法,把最早可用的块打印了出来:

fetch("https://fetch.spec.whatwg.org/")
  .then(response => response.body)
  .then(body => {
    let reader = body.getReader();

    console.log(reader); // ReadablestreamDefaultReader (}

    reader.read().then(console.log);
  });

// { value: Uint8Array{), done: false ]

在随着数据流的到来取得整个有效载荷,可以像下面这样递归调用 read() 方法:

fetch("https://fetch.spec.whatwg.org/")
  .then(response => response.body)
  .then(body => {
    let reader = body.getReader();

    function processNextChunk({ value, done }) {
      if (done) {
        return;
      }

      console.log(value);

      return reader.read().then(processNextChunk);
    }

    return reader.read().then(processNextChunk);
  });

// { value: Uint8Array{), done: false ]
// { value: Uint8Array{), done: false ]
// { value: Uint8Array{), done: false ]
// ...

异步函数非常适合这样的 fetch() 操作。可以通过使用 async/await 将上面的递归调用打平,

fetch("https://fetch.spec.whatwg.org/")
  .then(response => response.body)
  .then(async body => {
    let reader = body.getReader();

    while (true) {
      let { value, done } = await reader.read();

      if (done) {
        break;
      }

      console.log(value);
    }
  });

// { value: Uint8Array{), done: false ]
// { value: Uint8Array{), done: false ]
// { value: Uint8Array{), done: false ]
// ...

另外, read() 方法也可以直接封装到 Iterable 接口中。因此就可以在 for-await-of 循环中方便地实现这种转换:

fetch("https://fetch.spec.whatwg.org/")
  .then(response => response.body)
  .then(async body => {
    let reader = body.getReader();

    let asyncIterable = {
      [Symbol.asyncIterator]() {
        return {
          next() {
            return reader.read();
          }
        };
      }
    };

    for await (chunk of asyncIterable) {
      console.log(chunk);
    }
  });

// { value: Uint8Array{), done: false ]
// { value: Uint8Array{), done: false ]
// { value: Uint8Array{), done: false ]
// ...

通过将异步逻辑包装到一个生成器函数中,还可以进一步简化代码。而且,这个实现通过支持只读取部分流也变得更稳健。如果流因为耗尽或错误而终止,读取器会释放锁,以允许不同的流读取器继续操作:

async function* streamGenerator(stream) {
  const reader = stream.getReader();
  try {
    while (true) {
      const { value, done } = await reader.read();

      if (done) {
        break;
      }

      yield value;
    }
  } finally {
    reader.releaseLock();
  }
}

fetch("https://fetch.spec.whatwg.org/")
  .then(response => response.body)
  .then(async body => {
    for await (chunk of streamGenerator(body)) {
      console.log(chunk);
    }
  });

在这些例子中,当读取完 Uint8Array 块之后,浏览器会将其标记为可以被垃圾回收。对于需要在不连续的内存中连续检查大量数据的情况,这样可以节省很多内存空间。

不连续的内存中连续检查大量数据的情况,这样可以节省很多内存空间。缓冲区的大小,以及浏览器是否等待缓冲区被填充后才将其推到流中,要根据 JavaScript 运行时的实现。浏览器会控制等待分配的缓冲区被填满,同时会尽快将缓冲区数据(有时候可能未填充数据)发送到流。

不同浏览器中分块大小可能不同,这取决于带宽和网络延迟。此外,浏览器如果决定不等待网络。也可以将部分填充的缓冲区发送到流。。最终,我们的代码要准备好处理以下情况:

  • 不同大小的 uint8Array 块;
  • 部分填充的 uint8Array 块;
  • 块到达的时间间隔不确定。

默认情况下,块是以 Uint8Array 格式抵达的。因为块的分割不会考虑编码,所以会出现某些值作为多字节字符被分散到两个连续块中的情况。手动处理这些情况是很麻烦的,但很多时候可以使用 Encoding API 的可插拔方案。

要将 Uint8Array 转换为可读文本,可以将缓冲区传给 TextDecoder ,返回转换后的值。通过设置 stream:true ,可以将之前的缓冲区保留在内存,从而让跨越两个块的内容能够被正确解码:

let decoder = new TextDecoder();

async function* streamGenerator(stream) {
  const reader = stream.getReader();
  try {
    while (true) {
      const { value, done } = await reader.read();

      if (done) {
        break;
      }

      yield value;
    }
  } finally {
    reader.releaseLock();
  }
}

fetch("https://fetch.spec.whatwg.org/")
  .then(response => response.body)
  .then(async body => {
    for await (chunk of streamGenerator(body)) {
      console.log(decoder.decode(chunk, { stream: true }));
    }
  });

// <!doctype html><html lang="en"> ...
// whether a <a data-link-type="dfn" href="#concept-header" ...
// result to <var>rangeValue</var> ...
// ...

因为可以使用 ReadableStream 创建 Response 对象,所以就可以在读取流之后,将其通过管道导人另一个流。然后在这个新流上再使用 Body 的方法,如 text() 。这样就可以随着流的到达实时检查和操作流内容。

下面的代码展示了这种双流技术:

fetch("https://fetch.spec.whatwg.org/")
  .then(response => response.body)
  .then(async body => {
    const reader = body.getReader();

    // 创建第二个流

    return new ReadableStream({
      async start(controller) {
        try {
          while (true) {
            let { value, done } = await reader.read();

            if (done) {
              break;
            }

            // 将主体流的块推到第二个流
            controller.enqueue(value);
          }
        } finally {
          controller.close();
          reader.releaseLock();
        }
      }
    });
  })
  .then(secondaryStream => new Response(secondaryStream))
  .then(response => response.text())
  .then(console.log);

// <!doctype html><html lang="en"><head><meta charset="utf-8"> ..


6 Beacon API


为了把尽量多的页面信息传到服务器,很多分析工具需要在页面生命周期中尽量晚的时候向服务器发送遥测或分析数据。因此,理想的情况下是通过浏览器的,unload 事件发送网络请求。这个事件表示用户要离开当前页面,不会再生成别的有用信息了。

在 unload 事件触发时,分析工具要停止收集信息并把收集到的数据发给服务器。这时候有一个问题因为 unload 事件对浏览器意味着没有理由再发送任何结果未知的网络请求(因为页面都要被销毁了)。例如,在unload 事件处理程序中创建的任何异步请求都会被浏览器取消。为此,异步 XMLHttpRequest或 fetch ()不适合这个任务。分析工具可以使用同步 xMLHttpRequest 强制发送请求,但这样做会导
致用户体验问题。浏览器会因为要等待 unload 事件处理程序完成而延迟导航到下一个页面。

为解决这个问题,W3C 引人了补充性的 Beacon API。这个 API 给navigator 对象增加了一个sendBeacon()方法。这个简单的方法接收一个 URL 和一个数据有效载荷参数,并会发送一个 POST请求。可选的数据有效载荷参数有ArrayBufferView、Blob、DoMstring、FormData 实例。如果请求成功进入了最终要发送的任务队列,则这个方法返回true,否则返回 false。

可以像下面这样使用这个方法:

// 发送POST请求
// URL: 'https://example.com/analytics-reporting-url'
// 请求负载:"{foo:"bar"]"

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

这个方法虽然看起来只不过是 POST 请求的一个语法糖,但它有几个重要的特性。

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

常用于页面性能监控、异常日志记录、用户行为跟踪(埋点)



7 Web Socket


Web Socket(套接字)的目标是通过一个长时连接实现与服务器全双工、双向的通信。在 JavaScrip中创建 Web Socket时,一个 HTTP 请求会发送到服务器以初始化连接。服务器响应后,连接使用 HTTP 的 Upgrade 头部从 HTTP协议切换到 Web Socket 协议。这意味着 Web Socket不能通过标准 HTTP 服务器实现,而必须使用支持该协议的专有服务器。

因为 Web Socket 使用了自定义协议,所以 URL方案(scheme) 稍有变化:不能再使用 http://https:/ ,而要使用 ws://wss:// 。前者是不安全的连接,后者是安全连接。在指定 Web Socket URL时,必须包含 URL 方案,因为将来有可能再支持其他方案。

使用自定义协议而非 HTTP 协议的好处是,客户端与服务器之间可以发送非常少的数据,不会对 HTTP 造成任何负担。使用更小的数据包让 Web Socket 非常适合带宽和延迟问题比较明显的移动应用。使用自定义协议的缺点是,定义协议的时间比定义 JavaScript API要长。Web Socket得到了所有主流览器支持。

7.1 API

要创建一个新的 Web Socket,就要实例化一个 websocket 对象并传人提供连接的 URL

let socket = new WebSocket("ws://www.example.com/server.php");

注意,必须给 WebSocket 构造函数传人一个绝对 URL。同源策略不适用于 Web Socket ,因此可以打开到任意站点的连接。至于是否与来自特定源的页面通信,则完全取决于服务器。(在握手阶段就可以确定请求来自哪里。)

浏览器会在初始化 WebSocket 对象之后立即创建连接。与 XHR 类似,WebSocket 也有一个 readyState 属性表示当前状态。不过,这个值与 XHR中相应的值不一样。

  • Websocket.OPENING(0):连接正在建立。
  • Websocket.OPEN(1):连接已经建立。
  • Websocket.CLOSING(2):连接正在关闭。
  • Websocket.CLOSE(3):连接已经关闭。

WebSocket 对象没有 readystatechange 事件,而是有与上述不同状态对应的其他事件 readyState 值从 0 开始。

任何时候都可以调用 close() 方法关闭 Web Socket 连接:

socket.close();

调用 close() 之后, readyState 立即变为2(连接正在关闭),并会在关闭后变为3(连接已经关闭)。


7.2 发送和接受数据

打开 Web Socket 之后,可以通过连接发送和接收数据。

要向服务器发送数据,使用 send() 方法并传人一个字符串、ArrayBuffer或Blob,如下所示:

let socket = new WebSocket("ws://www.example.com/server.php");

let stringData = "Hello world!";
let arrayBufferData = Uint8Array.from(["f", "o", "o"]);
let blobData = new Blob(["f", "o", "o"]);

socket.send(stringData);
socket.send(arrayBufferData.buffer);
socket.send(blobData);

服务器向客户端发送消息时,Websocket 对象上会触发 message 事件。这个 message 事件与其他消息协议类似,可以通过 event.data 属性访问到有效载荷:

socket.onmessage = function (event) {
  let data = event.data; //对数据执行某些操作
};

与通过 send() 方法发送的数据类似, event.data 返回的数据也可能是 ArrayBuffer 或Blob。这由 WebSocket 对象的 binaryType 属性决定,该属性可能是"blob"或"arraybuffer"。


7.3 其他事件

WebSocket 对象在连接生命周期中有可能触发3个其他事件

事件 描述
open 在连接成功建立时触发
error 在发生错误时触发。连接无法存续
close 在连接关闭时触发。

websocket 对象不支持 DOM Level 2 事件监听器,因此需要使用 DOM Level 0 风格的事件处理程序来监听这些事件:

let socket = new Websocket("ws://www.example.com/server.php");

socket.onopen = function () {
  alert("Connection established.");
};
socket.onerror = function () {
  alert("Connection error.");
};
socket.onclose = function () {
  alert("Connection closed.");
};

在这些事件中,只有 close 事件的 event 对象上有额外信息。这个对象上有 3 个额外属性: wasClean 、 code 和 reason 。其中, wasClean 是一个布尔值,表示连接是否干净地关闭; code 是一个来自服务器的数值状态码;reason 是一个字符串,包含服务器发来的消息。可以将这些信息显示给用户或记录到日志:

socket.onclose = function (event) {
  console.log(`as clean? ${event.wasclean} Code=${event.code} Reason=${event.reason}`);
};


8 安全

探讨 Ajax安全的文章已经有了很多,事实上也出版了很多专门讨论这个话题的书。大规模 Ajax应用程序需要考虑的安全问题非常多,但在通用层面上一般需要考虑以下几个问题。

首先,任何 Ajax 可以访问的 URL,也可以通过浏览器或服务器访问,例如下面这个 URL:

/getuserinfo.php?id=23

请求这个 URL,可以假定返回 ID 为 23 的用户信息。访问者可以将 23 改为 24或 56,甚至其他任何值。 getuserinfo.php 文件必须知道访问者是否拥有访问相应数据的权限。否则,服务器就会大门敞开泄露所有用户的信息。

在未授权系统可以访问某个资源时,可以将其视为跨站点请求伪造(CSRF,cross-site request forgery )攻击。未授权系统会按照处理请求的服务器的要求伪装自己。Ajax 应用程序,无论大小,都会受到 CSRF攻击的影响,包括无害的漏洞验证攻击和恶意的数据盗窃或数据破坏攻击。

关于安全防护 Ajax 相关 URL 的一般理论认为,需要验证请求发送者拥有对资源的访问权限。可以通过如下方式实现。

  • 要求通过 SSL访问能够被 Ajax 访问的资源。
  • 要求每个请求都发送一个按约定算法计算好的令牌(token ).

注意,以下手段对防护CSRF 攻击是无效的。

  • 要求 POST 而非 GET 请求(很容易修改请求方法)。
  • 使用来源 URL 验证来源(来源 URL 很容易伪造)。
  • 基于 cookie 验证(同样很容易伪造)。
posted @   wanglei1900  阅读(12)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
历史上的今天:
2023-01-14 【vue-router】动态组件中通过路由参数来多次调用同一个页面中遇到的坑
2023-01-14 前端导出页面为PDF
点击右上角即可分享
微信分享提示